Skip to content

socket 的应用

一、websocket

1、websocket 概述

1.1 websocket 是什么?

  • WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。

1.2 为什么要用 websocket?

HTTP 协议有一个缺陷:通信只能由客户端单向发起,这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。我们只能使用"轮询"(每隔一段时候,就发出一个询问,了解服务器有没有新的信息)。最典型的场景就是聊天室。 轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。

1.3 wensocket 有什么用?

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。 在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

1.4 特点

WebSocket 协议在 2008 年诞生,2011 年成为国际标准。所有浏览器都已经支持了。 它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种

其他特点包括:

  • (1)建立在 TCP 协议之上,服务器端的实现比较容易。

  • (2)与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

  • (3)数据格式比较轻量,性能开销小,通信高效。

  • (4)可以发送文本,也可以发送二进制数据。

  • (5)没有同源限制,客户端可以与任意服务器通信(只要服务端支持 socket 通信即可)。

  • (6)协议标识符是 ws(如果加密,则为 wss),服务器网址就是 URL。

    • 例如:ws://example.com:80/some/path

2、websocket 的使用

2.1 服务端 websocket(Nodejs)

要在 nodejs 中使用 websocket,有很多种方案,比如:nodejs-websocket,socket.io,这里先介绍 nodejs-websocket 这个第三方库。 nodejs-websocket 的 github 地址:https://github.com/sitegui/nodejs-websocket

2.1.1、安装 nodejs-websocket 模块

bash
npm install nodejs-websocket --save

2.1.2、编写 nodejs 服务端代码

js
const ws = require("nodejs-websocket");

ws.createServer((conn) => {
  conn.on("text", (str) => {
    console.log("接收到的String:" + str);
    conn.sendText(`客户端,这是你给我发的消息: 【${str}】,我收到了`);
  });

  conn.on("close", (code, reason) => {
    console.log("连接关闭:", code, reason);
  });

  conn.on("error", (err) => {
    console.log("wobsocket错误", err);
  });
}).listen(3000, () => {
  console.log("socket已连接,正在监听客户端消息.....");
});

2.1.3、nodejs-websocket 的 API

这里仅仅标注了很少一部分,建议参考如下地址:

服务端 API 使用请参考nodejs-websocket 文档 API 中文介绍参考:https://www.jianshu.com/p/f0baf93a3795

const ws = require("nodejs-websocket");

ws:引入 nodejs-websocket 后的主要对象

ws 上面的 方法描述
ws.createServer([options], [callback])创建并返回一个 server 对象
ws.connect(URL, [options], [callback])创建一个 connect 对象,一般由客户端链接服务端 websocket 服务时创建
ws.setBinaryFragmentation(bytes)设置传输二进制文件的最小尺寸,默认 512kb
setMaxBufferLength设置传输二进制文件的最大尺寸,默认 2M

Server:通过 ws.createServer 创建

方法描述
server.listen(port, [host], [callback])传入端口和主机地址后,开启一个 websocket 服务
server.close([callback])关闭 websocket 服务
server.connections返回包含所有 connection 的数组,可以用来广播所有消息
js
// 服务端广播
function broadcast(server, msg) {
  server.connections.forEach(function (conn) {
    conn.sendText(msg);
  });
}

2.2 客户端 websocket

参考 MDN 由于所有浏览器都已经支持了 websocket,所以客户端一般情况下没必要使用 websocket 的库,直接使用浏览器提供的对象即可实现 websocket 链接。

2.2.1、客户端的 API

(1)创建 websocket 实例 WebSocket 对象作为一个构造函数,用于新建 WebSocket 实例。

js
var ws = new WebSocket(url[, protocols]);

解释:

  • url: 指定连接的 URL
  • protocol: 可选的,指定了可接受的子协议
  • 执行此语句之后,客户端就会与服务器进行连接

例如:

javascript
var ws = new WebSocket("ws://127.0.0.1:3000");

实例对象的所有属性和方法清单,https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket

(2)websocket 实例上的属性

实例上的属性通过 websocket 实例 ws 直接调用,例如:ws.readyState

一些常用属性如下:

websocket属性取值取值描述其他
ws.readyState0表示正在连接等价与 WebSocket.CONNECTING
1表示连接成功,可以通信了等价与 WebSocket.OPEN
2表示连接正在关闭等价与 WebSocket.CLOSING
3表示连接已经关闭,或者打开连接失败等价与 WebSocket.CLOSED
ws.bufferedAmount只读属性未发送至服务器的字节数。已被 send() 放入正在队列中等待传输, 但是还没有发出的 UTF-8 文本字节数。

一些常用方法

ws 是 Websocket 的实例化对象

方法描述
ws.send()用于向服务器发送数据
ws.close()用于关闭当前链接。

一些常用事件

以下事件是 Websocket 的实例化对象 ws 拥有的事件 使用 addEventListener() 或 直接使用 on 事件绑定方式,例如:ws.onopen=fn、ws.onmessage=fn、..., 如果要指定多个回调函数,请用ws.addEventListener('message',fn)

事件描述
open连接建立时触发
message客户端接收服务端数据时触发
error通信发生错误时触发
close连接关闭时触发

2.2.2、客户端示例代码

js
if (window.WebSocket) {
  //当设备支持websocket时,开始建立客户端链接
  var ws = new WebSocket("ws://127.0.0.1:3000");
  // 连接建立时触发
  ws.onopen = function (e) {
    console.log("连接服务器成功");
    ws.send("今天天气真好"); //主动向服务端发起消息
  };
  // 接收到服务端数据时触发
  ws.onmessage = function (e) {
    //在这里,e表示服务端发送而来的内容
    console.info("服务端说==>:", e.data);
  };
  //当链接被服务端关闭时触发
  ws.onclose = function (e) {
    console.log("服务器关闭");
  };
  // 当服务端通信发生错误时触发
  ws.onerror = function () {
    console.log("连接出错");
  };
} else console.info("你的设备不支持websocket!");

2.3 演示结果

image.png

3、websocket 实现简单聊天室

聊天室思路:

  • 客户端发送数据到服务器,然后服务器遍历所有的聊天室中的连接,将消息广播出去,聊天室内所有客户端接收服务端发来的消息,更新到页面上,呈现广播文本。
  • 一对一聊天更简单,服务端不需要广播遍历了,只需要将消息转发给特定的用户即可。

3.1 客户端

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Websocket</title>
</head>
<body>
    <h1>Chat Room</h1>
    <input type="text" id="sendTxt">
    <button id="sendBtn">发送</button>
    <script>
        // 用于更新消息到页面上
        function showMessage(str, type) {
            var div = document.createElement("div");
            div.innerHTML = str;
            if(type == "enter") {
                div.style.color = "blue"; 
            } else if(type == "leave") {
                div.style.color = "red";
            }
            document.body.appendChild(div);
        }
        // 创建websocket实例
        var websocket = new WebSocket("ws://127.0.0.1:3003/");
        
        // Socket.onopen事件  连接建立时触发
        websocket.onopen = function() {
            console.log("websocket open");
            document.getElementById("sendBtn").onclick = function() {
                var txt = document.getElementById("sendTxt").value;
                if(txt) {
                    websocket.send(txt);
                }
            }
        }
        // 连接关闭时触发
        websocket.onclose = function() {
            console.log('websocket close');
        }
        // 客户端接收服务端数据时触发 e是MessageEvent
        websocket.onmessage = function(e) {
            console.log(e.data);
            var mes = JSON.parse(e.data);
            showMessage(mes.data, mes.type);
        }
    </script>
</body>
</html>

3.2 服务端

js
// 引入express(用于承载html的静态资源服务)
const express = require("express");
const app = express();
// 引入服务端核心的websocket模块
const ws = require("nodejs-websocket");
// 托管客户端html静态资源
app.use(express.static("./static"));
// websocket端口
const port = 3003;
const hostname = "127.0.0.1";
// http服务器监听80端口
app.listen(80, () => {
  console.log("服务器启动成功,prot:" + 80);
});

// 客户端计数器
var clientCount = 0;

// 创建一个server对象, ws是引入nodejs-websocket后的主要对象
var server = ws
  .createServer(function (conn) {
    console.log("New connection");
    clientCount++; // 有用户连接进来,计数器+1
    conn.nickname = "user" + clientCount; // 每个用户的昵称不同
    /* 
     服务器发送给客户端数据用sendText()必须是字符串,我们可以使用
     JSON.stringify()方法将JavaScript对象转换为字符串 
    */
    var mes = {};
    mes.user = conn.nickname;
    mes.type = "enter";
    mes.data = conn.nickname + " comes in";
    broadcast(JSON.stringify(mes)); // 刚播xxx用户线

    // 收到文本时触发,str是收到的文本字符串
    conn.on("text", function (str) {
      console.log("Received " + str);
      var mes = {};
      mes.user = conn.nickname;
      mes.type = "message";
      mes.data = conn.nickname + " says: " + str;
      broadcast(JSON.stringify(mes)); // 把客户端收到的消息广播给每一位用户
    });
    // 连接关闭时触发
    conn.on("close", function (code, reason) {
      console.log("Connection closed");
      var mes = {};
      mes.user = conn.nickname;
      mes.type = "leave";
      mes.data = conn.nickname + " left";
      broadcast(JSON.stringify(mes)); // 广播xxx用户下线
    });
    // 发生错误时触发,如果握手无效,也会发出响应
    conn.on("error", function (err) {
      console.log("handle err");
      console.log(err);
    });
  })
  .listen(port); // ws服务监听端口

console.log(
  "websocket server listening on port: " + port + ", on hostname: " + hostname
);

// server.connections返回包含所有connection的数组,可以用来广播所有消息
// 服务端广播
function broadcast(msg) {
  server.connections.forEach(function (conn) {
    conn.sendText(msg);
  });
}

3.3 效果展示

image.png

二、Socket.io

Socket.IO 是一个库,可以在浏览器和服务器之间实现实时、双向和基于事件的通信。

1、Socket.io 与 websocket 的关系

  • (1)WebSocket 是 HTML5 最新提出的规范,虽然主流浏览器都已经支持,但仍然可能有不兼容的情况,为了兼容所有浏览器,给程序员提供一致的编程体验,SocketIO 将 WebSocket、AJAX 和其它的通信方式全部封装成了统一的通信接口,
  • (2)也就是说,我们在使用 SocketIO 时,不用担心兼容问题,底层会自动选用最佳的通信方式。
  • (3)因此说,WebSocket 是 SocketIO 的一个子集。
  • (4)Socket.io 会自动根据浏览器从 WebSocket、AJAX 长轮询、Iframe 流等等各种方式中选择最佳的方式来实现网络实时应用,非常方便和人性化,而且支持的浏览器最低达 IE5.5,应该可以满足绝大部分需求了。

2、socket.io 的组成

  • (1)服务端
    • 用于集成(或挂载)到 Node.JS HTTP 服务器:socket.io
  • (2)客户端
    • 加载到浏览器中的客户端:socket.io-client

开发环境下, socket.io 会自动提供客户端。 当我们在生产环境下使用时,我们就应该单独的使用 socket 客户端模块了,而不是共用服务端的 socket 模块。

3、scoket.io 的官方文档

英文文档:https://socket.io/get-started/chat/(推荐) 中文文档:https://www.w3cschool.cn/socket/socket-1olq2egc.html

4、socket.io 的使用

4.1 服务端基本使用

服务端以 express 为例

(1)创建服务端 express 项目,安装 socket.io

js
mkdir scoket-demo
cd ./scoket-demo
npm init -y
npm i express socket.io -S

(2)编写 socke.io 服务端代码

js
var express = require("express");
var app = express();
var http = require("http");
//将express模块实例作为回调构建http模块实例
var server = http.Server(app);
// 通过传入 server (HTTP 服务器) 对象初始化了 socket.io 的一个实例
var io = require("socket.io")(server); // 这句话写上时候,socket.io就会把客户端(js文件)挂载到服务器

server.listen(81);

app.get("/", function (req, res) {
  // 浏览器访问http://127.0.0.1:81,将看到页面index.html
  res.sendFile(__dirname + "/index.html");
});
/* 
  监听 connection 事件来接收 sockets
(前端每次执行一次io()方法就就会发起一次socket请求)
*/
io.on("connection", function (socket) {
  // 只要进入这个回调函数,就说明客户端已经连接进来了
  // 给客户端发送消息
  socket.emit("news", { hello: "world" });
  socket.on("my other event", function (data) {
    console.log("客户端发来的消息:", data);
  });
  //如果是断开socket请求,就会触发下面的代码
  socket.on("disconnect", function () {
    console.log("连接断开了...");
  });
});

const io = new Server(server, {}); 这句话写上,socketio 就已经把客户端挂在到服务器, 验证: 访问http://127.0.0.1:3000/socket.io/socket.io.js 但是只能用于开发环境,生产环境需要单独下载引入 image.png

4.2 客户端基本使用

开发环境下, socket.io 会自动提供客户端。省去了下载

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <!-- 这样就加载了 socket.io-client。 socket.io-client 暴露了一个 io 全局变量,然后连接服务器。 -->
    <script src="/socket.io/socket.io.js"></script>
    <script>
      //   var socket = io.connect('http://localhost:81'); // 要解决跨域
      // 请注意我们在调用 io() 时没有指定任何 URL,因为它默认将尝试连接到提供当前页面的主机。
      var socket = io(); // 写上这一句,我们的客户端就已经脸上服务器了
      // 监听服务器推送的消息
      socket.on("news", function (data) {
        console.log("服务器推送的消息:", data);
        socket.emit("my other event", { my: "hello 服务器" });
      });
    </script>
  </body>
</html>

var socket = io(); 写上这一句,我们的客户端就已经脸上服务器了 验证:访问http://127.0.0.1:3000 > image.png

4.3 运行效果

image.png

5、socket.io 常用的 API

5.1 创建服务(node 服务)

5.1.0 基础 API

下面两种方式都可以创建服务

js
const io = require('socket.io')(httpServer[, options]);

或者

js
const Server = require('socket.io');
const io = new Server(httpServer[, options]);

(1)、new Server(httpServer[, options]):基于 http 服务器创建 scoket (2)、new Server(port[, options]):创建独立的 socket (3)、new Server(options)基础 API 参数详解 更多 API 建议参考英文官网,中文的翻译出来有些出入:https://socket.io/docs/v4/server-api/

参数说明-
httpServer (http.Server)要绑定的服务器-
port (Number)要侦听的端口
options (Object)path (String)捕获路径的名称(默认:"/socket.io" )前后端需要一致(可理解为路由)
serveClient (Boolean)是否提供客户端文件(默认:true )
adapter (适配器)适配器使用 参考:socket.io-adapter,配合redis使用的
origins (String)允许的originins(默认:*
parser (Parser)解析器使用。默认为Parser socket.io附带的实例
pingTimeout (Number)有多少ms没有乒乓包考虑连接close(60000)
pingInterval (Number)发送新的ping packet(25000)之前有多少ms
transports (Array <String>传输以允许连接到(['polling', 'websocket'])

注意

  • 1、pingTimeoutpingInterval两个参数将在客户端知道服务器不再可用之前影响延迟
    • 例如,如果底层 TCP 连接由于网络问题而未正确关闭,则 pingTimeout + pingInterval 在获取 disconnect 事件之前,客户端可能必须等待最多 ms 。
  • 2、transports 参数顺序很重要。默认情况下,首先建立长轮询连接,然后如果可能,升级到 WebSocket。使用['websocket']方法如果无法打开 WebSocket 连接,则不会有后备。

5.1.1 创建独立的 socket 服务(有自己的端口)

js
const { Server } = require("socket.io");
const io = new Server({
  /* options */
});
io.on("connection", (socket) => {
  // ...
});
io.listen(3000);

您还可以将端口作为第一个参数传递

js
const { Server } = require("socket.io");
const io = new Server(3000, {
  /* options */
});
io.on("connection", (socket) => {
  console.log(".......");
});

5.1.2 使用 http 服务器

不会创建单独的 ws 服务,而是使用 http 服务承载 ws 服务

  • (1)原生 nodejs
js
// 原生nodejs服务
const { createServer } = require("http");
const { Server } = require("socket.io");
const httpServer = createServer();
const io = new Server(httpServer, {
  /* options */
});
io.on("connection", (socket) => {
  // ...
});
httpServer.listen(3000);
  • (2)Express
js
// Express服务
const express = require("express");
const { createServer } = require("http");
const { Server } = require("socket.io");
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
  /* options */
});
io.on("connection", (socket) => {
  // ...
});

httpServer.listen(3000);
  • (3)Koa
js
// Koa服务
const Koa = require("koa");
const { createServer } = require("http");
const { Server } = require("socket.io");

const app = new Koa();
const httpServer = createServer(app.callback());
const io = new Server(httpServer, {
  /* options */
});
io.on("connection", (socket) => {
  // ...
});

httpServer.listen(3000);

5.2 发送消息emit方法

语法:socket.emit(eventName [,... args] [,ack]) 返回返回 Socket 描述:将一个事件发送到由字符串名称标识的 socket。可以包括任何其他参数。支持所有可序列化的数据结构,包括 Buffer。 客户端服务端方法都一样

5.2.1 服务端发送消息

js
const { Server } = require("socket.io");
const io = new Server({ /* options */ });
io.on("connection", (socket) => {
  // 发送给当前客户端
  socket.emit('news', 'can you hear me?', 1, 2, 'abc');
  // 发送给所有客户端,除了发送者
  socket.broadcast.emit('broadcast', 'hello friends!');
  // 发送给同在 'game' 房间的所有客户端,除了发送者
  socket.to('game').emit('nice game', "let's play a game");
  // 发送给同在 'game1' 或 'game2' 房间的所有客户端,除了发送者
  socket.to('game1').to('game2').emit('nice game', "let's play a game (too)");
  // 发送给同在 'game' 房间的所有客户端,包括发送者
  io.in('game').emit('big-announcement', 'the game will start soon');
  // 发送给同在 'myNamespace' 命名空间下的所有客户端,包括发送者
  io.of('myNamespace').emit('bigger-announcement', 'the tournament will start soon');
  // 发送给指定 socketid 的客户端(私密消息)
  socket.to(<socketid>).emit('hey', 'I just met you');
   // 包含回执的消息
  socket.emit('question', 'do you think so?', function (answer) {});
  // 不压缩,直接发送
  socket.compress(false).emit('uncompressed', "that's rough");
  // 如果客户端还不能接收消息,那么消息可能丢失
  socket.volatile.emit('maybe', 'do you really need it?');
  // 发送给当前 node 实例下的所有客户端(在使用多个 node 实例的情况下)
  io.local.emit('hi', 'my lovely babies');
});
io.listen(3000);

5.2.2 客户端发送消息

html
<body>
  <script src="/socket.io/socket.io.js"></script>
  <!-- 或者 生产环境 -->
  <script
    src="https://cdn.socket.io/4.4.0/socket.io.min.js"
    integrity="sha384-1fOn6VtTq3PWwfsOrk45LnYcGosJwzMHv+Xh/Jx5303FVOXzEnw0EpLv30mtjmlj"
    crossorigin="anonymous"
  ></script>
  <script>
    // 这样就加载了 socket.io-client。 socket.io-client 暴露了一个 io 全局变量,然后连接服务器。
    //请注意我们在调用 io() 时没有指定任何 URL,因为它默认将尝试连接到提供当前页面的主机。
    window.onload = function () {
      var socket = io("http://localhost:3000");
      var form = document.querySelector("form");
      var val = document.querySelector("#m");
      form.onsubmit = function () {
        socket.emit("news", val.value);
        val.value = "";
        return false;
      };
    };
  </script>
  <ul id="messages"></ul>
  <form action="">
    <input id="m" autocomplete="off" /><button>Send</button>
  </form>
</body>

5.3 接收消息 on方法

语法:socket.on(flag,callback) flag 是事件的名称,前后端需一致 客户端服务端方法都一样

5.3.1 服务端接收消息

js
const { Server } = require("socket.io");
const io = new Server({
  /* options */
});
io.on("connection", (socket) => {
  // socket监听客户端推送的消息
  socket.on("news", function (msg) {
    console.log("message: " + msg);
  });
});
io.listen(3000);

5.3.2 客户端接收消息

html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script src="/socket.io/socket.io.js"></script>
    <!-- 或者 生产环境-->
    <script
      src="https://cdn.socket.io/4.4.0/socket.io.min.js"
      integrity="sha384-1fOn6VtTq3PWwfsOrk45LnYcGosJwzMHv+Xh/Jx5303FVOXzEnw0EpLv30mtjmlj"
      crossorigin="anonymous"
    ></script>
    <script>
      // 这样就加载了 socket.io-client。 socket.io-client 暴露了一个 io 全局变量,然后连接服务器。
      //请注意我们在调用 io() 时没有指定任何 URL,因为它默认将尝试连接到提供当前页面的主机。
      window.onload = function () {
        var socket = io("http://localhost:3000");
        var form = document.querySelector("form");
        var val = document.querySelector("#m");
        form.onsubmit = function () {
          //接收服务器发来的消息
          socket.on("news", function (data) {
            console.log(data);
          });
        };
      };
    </script>
    <ul id="messages"></ul>
    <form action="">
      <input id="m" autocomplete="off" /><button>Send</button>
    </form>
  </body>
</html>

5.4 文件的发送与接收

5.4.1 发送文件的两种方式

  • 方式一:使用 ajax 形式将文件发送到服务端,服务端进行保存后,将地址转发给客户端,然后对应客户端就能显示我们的图片了。
  • 方式二:使用 H5 的新 api【FileReader】读取文件(图片),然后转换为 base64 编码的形式进行发送到客户端,由于是 base64 字符串的形式,所以服务端收到后就可以直接发送到对应客户端。

这里介绍【方式二】,以图片为例

5.4.2 客户端发送图片

js
let Imginput = document.getElementById("...");
let file = Imginput.files[0]; //得到该图片
let fileReader = new FileReader(); //创建一个FileReader对象,进行下一步的操作
fileReader.readAsDataURL(file); //通过readAsDataURL读取图片
fileReader.onload = function () {
  //读取完毕会自动触发,读取结果保存在result中
  let data = { img: this.result };
  socket.emit("sendImg", data);
};

5.4.3 服务端接收与转发

js
socket.on("sendImg", (data) => {
  data.id = socket.id;
  io.emit("receiveImg", data);
});

5.4.4 客户端接收

js
socket.on("receiveImg", (data) => {
  let img = data.img; // 接收到的图片
});

5.5 表情包的发送与接收

对于表情,我们当做文件进行发送就可以了,为了更快速的发送表情,提高性能,我们可以创建一个表情包,里面有很多表情图片。

每个客户端的本地都具有相关的表情包,然后每个表情有相关的对应代码,我们只需要把对应代码发送给别人即可,然后好友收到后会在本地解析此代码作为表情图片进行显示。

如果要将表情混在文本中间发送的话,我们需要写一些逻辑进行判断处理

6、聊天室的实现

6.1 单人聊天

思路:

  • 1、当用户连接上服务器时,就把此用户保存在一个对象中,以用户名为键(用户名值不应该给汉字,除非做转码处理),以 socket.id 为值。
  • 2、然后用户 a 要给用户 b 发消息时,在后端通过 b 的用户名找到 b 的 socket 的 id, 从而将用户 a 的消息发给 b. b 发消息给 a 也是一样的方式。

在实现私人聊天时应该注意如下事项

  • 1、各种变量属性等命名时尽量给上一个独特的后缀,以避免和其他名字冲突从而出现 bug.
  • 2、在这里使用 socket.io 返回数据时,名字尽量不要出现 username,否则可能会出现异常。在使用 socket.io 做事件监听时,事件名不要写成 connect,会发生冲突。
  • 3、对象的键不应该给汉字,否则在很多时候都会出现异常。

6.2 群聊

效果

image.png

码云仓库地址

https://gitee.com/li_jia_hong123/scoket.io-express

目录结构

image.png

服务端核心代码

  • 服务端核心代码 index.js
js
const express = require("express");
const { Server } = require("socket.io");
const path = require("path");
const http = require("http");
const { userInRoom, userLevelRoom, getRoomUser } = require("./utils");

// 实例化express应用
const app = express();

// 声明一个端口
const PORT = process.env.SERVER_PORT || 3000;

// 托管静态资源
app.use(express.static(path.join(__dirname, "public")));

// 要将socket.io挂载到服务器,需要用到原生http模块创建服务,并将请求交给express处理
const server = http.createServer(app);

// 初始化scoket.io, 这句话写上,socketio就已经把客户端挂在到服务器,
// 验证: 访问http://127.0.0.1:3000/socket.io/socket.io.js
const io = new Server(server, {});

//1. 监听连接
io.on("connection", (socket) => {
  /* 
    进到这个回调函数,说明连接已经建立了
    并且,没个连接进来的客户端,都会有一个唯一的客户端标识ID,用来区分同名的用户,获取这个ID只需 socket.id 即可获取
  */

  var id = socket.id; // 每一个连接唯一标识

  // 2. 监听用户加入房间
  socket.on("inRoom", ({ name, room }) => {
    // 3. 将用户加入房间
    socket.join(room);
    // 4. 将加入进来的用户存起来
    userInRoom({ id, name, room });
    // 5. 广播 将该房间内用户列表推送给当前房间内所有人,注意to(room)就限了只会给当前房间内的用户推送消息
    // 这里用的是全局的io对象 io.emit而不是socket.emit来发送消息,因为socket只会发送给某个人
    io.to(room).emit("getusers", getRoomUser(room));
    // 广播 欢迎xxx进入房间
    io.to(room).emit("systemMsg", { name, room });
    // 将进入房间的用户的Id返回给用户自己
    socket.to(room).emit("ownInfo", { id, name, room });
    // 监听客户端发来的消息
    socket.on("chatMsg", (msg) => {
      // 将用户发来的消息广播出去(广播给房间内所有用户)
      io.to(room).emit("chatMsg", msg);
    });
    // 断开连接时候会触发(比如:客服端离开)
    socket.on("disconnect", () => {
      // 用户掉线或离开,将用户从服务器用户列表删除
      userLevelRoom(id);
      // 给该房间内用户推送房前房间内所有用户列表
      socket.leave(room);
      io.to(room).emit("getusers", getRoomUser(room));
    });
  });
});

// 监听端口
server.listen(PORT, "0.0.0.0", () => {
  console.info("服务启动成功,运行端口:" + PORT);
});
  • 服务端工具方法 utils.js
js
let users = []; // 用于存储用户

//用户加入房间的方法
function userInRoom(userObj) {
  users.push(userObj);
  return users;
}

// 用户离开房间的方法
function userLevelRoom(id) {
  let index = users.findIndex((item) => item.id === id);
  if (index !== -1) {
    users.splice(index, 1);
  }
  return users;
}

// 获取某个房间的所有用户
function getRoomUser(room) {
  return users.filter((item) => item.room === room);
}

module.exports = {
  userInRoom,
  userLevelRoom,
  getRoomUser,
};
  • 用的一些依赖包 package.json
json
{
  "name": "scoket-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "nodemon ./index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "express": "^4.17.1",
    "socket.io": "^4.4.0"
  },
  "devDependencies": {
    "nodemon": "^2.0.15"
  }
}

客户端代码

客户端思路:

  • 客户端用了 bootstrap 布局页面,做了简单的适配

  • 在登陆页面,填入姓名和选择房间,登录后,将姓名和房间放到 URL 上,跳转房间 room.html 页面,在 room.html 页面取到姓名和房间,连接 socket 后,首先 emit 给 socket 服务器端,服务端会记录人员,并加入 room,就可以聊天了, 主要方法 emit 和 on

  • 登陆页面 index.html

html
<!-- login.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>聊天室 | 登录</title>
    <!-- 引入bootstrap样式 -->
    <!-- CSS only -->
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
      crossorigin="anonymous"
    />
    <link rel="stylesheet" href="index.css" />
  </head>

  <body class="bg-light d-flex justify-content-center">
    <div class="login-container shadow-lg  bg-body rounded mt-5">
      <div
        class="login-head p-3 text-center  bg-success bg-gradient text-light"
      >
        聊一聊
      </div>
      <div class="login-body p-3 border-bottom">
        <form action="#" id="loginForm">
          <div class="mb-3">
            <label for="name" class="form-label">姓 名</label>
            <input
              type="text"
              class="form-control"
              id="name"
              placeholder="请输入姓名"
            />
          </div>
          <div class="mb-3">
            <label for="room" class="form-label">房 间</label>
            <select class="form-select" id="room">
              <option value="" selected>请选择房间</option>
              <option value="Javascript">Javascript</option>
              <option value="Html">Html</option>
              <option value="Css">Css</option>
            </select>
          </div>
          <div class="d-grid gap-2 mt-5">
            <button type="submit" class="btn btn-success">登 录</button>
          </div>
        </form>
      </div>
    </div>
  </body>
  <!-- 引入bootstrap的js -->
  <!-- JavaScript Bundle with Popper -->
  <script
    src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
    crossorigin="anonymous"
  ></script>

  <script>
    var loginForm = document.getElementById("loginForm");
    loginForm.addEventListener("submit", (e) => {
      e.preventDefault();
      let name = document.getElementById("name").value;
      let room = document.getElementById("room").value;
      if (!name.trim()) {
        alert("姓名必填!");
        return;
      }
      if (!room) {
        alert("请选择房间!");
        return;
      }
      window.location.replace(
        `/room.html?name=${encodeURI(name)}&room=${encodeURI(room)}`,
        "_self"
      );
    });
  </script>
</html>
  • 聊天室页面 room.html
html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>聊天室</title>
    <!-- 引入bootstrap样式 -->
    <!-- CSS only -->
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3"
      crossorigin="anonymous"
    />
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.7.2/font/bootstrap-icons.css"
    />
    <link rel="stylesheet" href="index.css" />
  </head>

  <body class="bg-light d-flex justify-content-center">
    <div class="container room mt-5 card">
      <div class="row">
        <div
          class="d-flex justify-content-between aline-item-center py-2 bg-secondary text-light"
        >
          <div class="left">聊一聊</div>
          <a
            href="javascript:;"
            class="right text-light text-decoration-none"
            id="outPage"
          >
            退出<i class="bi bi-arrow-right-short"></i>
          </a>
        </div>
      </div>
      <div class="row">
        <div
          class="room-list col-lg-3 col-md-3 col-sm-12 col-xs-12 d-flex flex-column"
        >
          <div class="fs-4 py-2 border-bottom">
            <span id="roomName"></span>
            <span style="font-size:13px;"
              >在线人数( <span id="inlineCount">100</span> )</span
            >
          </div>
          <div class="overflow-auto">
            <ul class="flex-grow-1 mb-2" id="userList"></ul>
          </div>
        </div>
        <div class="col-lg-9 col-md-9 col-sm-12 col-xs-12 new-list-container">
          <div class="welcome bg-success text-white bg-opacity-75">
            欢迎张三加入房间
          </div>
          <div class="overflow-auto news-content" id="msg-container">
            <ul id="msgContent"></ul>
          </div>
          <div class="row">
            <div class="input-group mb-2">
              <input
                type="text"
                class="form-control"
                placeholder="请输入内容"
                id="inputContent"
              />
              <button
                class="btn btn-outline-secondary"
                type="button"
                id="submitBtn"
              >
                <i class="bi bi-send"></i> 发 送
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  </body>
  <!-- 引入客户端js -->
  <script src="/socket.io/socket.io.js"></script>
  <!-- 引入bootstrap的js -->
  <script
    src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"
    integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p"
    crossorigin="anonymous"
  ></script>
  <!-- moment -->
  <script src="http://cdn.staticfile.org/moment.js/2.24.0/moment-with-locales.js"></script>
  <!-- 引入自己的js -->
  <script src="/main.js"></script>
</html>
  • 聊天室 js 文件 main.js
js
// 写上这一句话,客户端就已经连上服务器了,验证:http://127.0.0.1:3000/room.html,查看network,已经发送了一个ws请求了
const socket = io();

const inputContent = document.getElementById("inputContent");
const submitBtn = document.getElementById("submitBtn");
const msgContainer = document.getElementById("msg-container");
const msgContent = document.getElementById("msgContent");
const inlineD = document.getElementById("inlineCount");
const userListD = document.getElementById("userList");
const roomNameD = document.getElementById("roomName");
const outPageD = document.getElementById("outPage");

// 获取URL里面的姓名和房间
let queryString = window.location.search;
queryString = queryString.slice(1);
let obj = queryString.split("&").reduce((pre, cur) => {
  let [key, value] = cur.split("=");
  pre[key] = decodeURI(value);
  return pre;
}, {});

// 进入房间前将姓名和房间号发给服务器
socket.emit("inRoom", obj);

// 监听服务器返回的用户列表
socket.on("getusers", (users) => {
  inlineD.innerHTML = users.length || 0;
  renderUserList(users);
});

// 监听系统消息: xxx进入房间
socket.on("systemMsg", (user) => {
  roomNameD.innerText = user.room;
  let welcomeD = document.querySelector(".welcome");
  welcomeD.innerHTML = `欢迎【${user.name}】加入房间`;
  welcomeD.style.display = "block";
  setTimeout(() => {
    welcomeD.style.display = "none";
  }, 600);
});

socket.on("ownInfo", (own) => {
  window.sessionStorage.setItem("userId", own.id);
});

// 接收用户消息
socket.on("chatMsg", (msg) => {
  renderMsg(msg);
});

// 发消息
submitBtn.addEventListener("click", sendMsg);

inputContent.addEventListener("keyup", (e) => {
  var event = e || window.event;
  if (event.key === "Enter" || event.Code === "Enter" || event.keyCode === 13) {
    sendMsg();
  }
});

function sendMsg() {
  let msg = inputContent.value;
  if (!msg.trim()) {
    alert("请输入内容");
    return;
  }
  // 发消息
  const { name, room } = obj;
  let id = window.sessionStorage.getItem("userId");
  socket.emit("chatMsg", { id, name, room, msg, time: Date.now() });
  inputContent.value = "";
}

// 渲染消息message
function renderMsg(message) {
  let userid = window.sessionStorage.getItem("userId");
  console.log(userid, message);
  const { id, name, room, msg, time } = message;
  if (userid === id) {
    // 我自己
    msgContent.innerHTML += `
    <li class="news-item d-flex flex-column align-items-end">
        <div class="w-75">
            <div class="time pt-1 ml-1 text-end mx-2">
                <span class="d-inline-block mx-2">${moment(time).format(
                  "YYYY-MM-DD HH:mm:ss"
                )}</span><span class="text-danger">我</span>
            </div>
            <p class="text-end">
                <span
                    class="text-start mx-2 bg-body mt-2 text-break shadow-lg d-inline-block p-2 rounded"
                    style="background-color:#9eea6a!important;">
                    ${msg}
                </span>
            </p>
        </div>
    </li>
    `;
  } else {
    // 别人
    msgContent.innerHTML += `
    <li class="news-item">
        <div class="w-75">
            <div class="time pt-1 ml-1 mx-2">
                <span class="d-inline-block me-2"><span class="text-danger">${name}</span></span></span><span>${moment(
      time
    ).format("YYYY-MM-DD HH:mm:ss")}</span>
            </div>
            <p class="bg-body mt-2 mx-2 shadow-lg text-break d-inline-block p-2 rounded">${msg}</p>
        </div>
    </li>
    `;
  }
  msgContainer.scrollTo(0, msgContent.scrollHeight);
}

// 渲染用户列表
function renderUserList(users) {
  let str = "";
  users.forEach((item) => (str += `<li class="p-1">${item.name}</li>`));
  userListD.innerHTML = str;
}

// 用户退出
outPageD.addEventListener("click", (e) => {
  e.preventDefault();
  window.location.replace("/index.html", "_self");
});
  • 全局样式文件 index.css
css
ul,
li {
  padding: 0;
  margin: 0;
  list-style: none;
}

.new-list-container {
  position: relative;
}
.new-list-container .welcome {
  position: absolute;
  top: 15px;
  left: 50%;
  transform: translateX(-50%);
  font-size: 13px;
  border-radius: 5px;
  padding: 5px;
  display: none;
}

.login-container {
  width: 450px;
  justify-content: space-between;
}
.room-list {
  border-bottom: 1px solid #ccc;
}
.news-content {
  height: 70vh;
}
.time {
  font-size: 12px;
}

@media (max-width: 575px) {
  .login-container {
    width: 90%;
  }
}

@media (min-width: 992px) {
  .room-list {
    border-right: 1px solid #ccc;
    border-bottom: none;
  }
}

7、参考资料

https://socket.io/get-started/chat/ (英文)

https://www.w3cschool.cn/socket/socket-ulbj2eii.html (中文)

其他:

https://blogzl.com/pub/7-nodejs/Nodejs实践教程/8. SocketIO实践教程.html#客户端-1

https://blog.csdn.net/NEUQ_zxy/article/details/77531126

https://www.jb51.net/article/57090.htm