socket.io

1,433 阅读23分钟

TCP/IP、UDP、Socket

TCP/IP、UDP

TCP/IP(Transmission Control Protocol/Internet Protocol)即传输控制协议/网间协议,是一个工业标准的协议集,它是为广域网(WANs)设计的。

UDP(User Data Protocol,用户数据报协议)是与TCP协议相对应的协议。它和TCP协议都是属于TCP/IP协议族中的一种。

协议的关系如下图:

image.png

TCP/IP协议族包括传输层、网络层、链路层。

Socket

HTTP协议是一种单向请求-响应协议,客户端发送请求后,服务器才会响应并返回相应的数据。在传统的 HTTP请求 中,客户端需要主动发送请求才能获取服务器上的资源,而且每次请求都需要重新建立连接,这种方式在实时通信和持续获取资源的场景下效率较低。

Socket 提供了实时的双向通信能力,可以实时地传输数据。客户端和服务器之间的通信是即时的,数据的传输和响应几乎是实时完成的,不需要轮询或定时发送请求。

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。

image.png

socket通信流程如下:

image.png

服务端先初始化Socket,然后与端口绑定(bind),对端口进行监听(listen),调用accept阻塞,等待客户端连接。在这时如果有个客户端初始化一个Socket,然后连接服务器(connect),如果连接成功,这时客户端与服务器端的连接就建立了。客户端发送数据请求,服务器端接收请求并处理请求,然后把回应数据发送给客户端,客户端读取数据,最后关闭连接,一次交互结束。

TCP报文

image.png

  • 端口号:每个 TCP 报文段都包含源端口和目的端口的端口号,用于寻找发送端和接收端应用进程。这两个值加上 IP 首部中的源端 IP 地址和目的端 IP 地址就可以确定一个唯一的 TCP 连接。
  • 序号:作用是用于将失序的数据重新排列。TCP 会隐式地对字节流中的每个字节进行编号,而 TCP 报文段的序号被设置为其数据部分的第一个字节的编号。序号是 32 bit 的无符号数,取值范围是0到2的32 次方-1。
  • 确认序号:接收方在接收到数据后,会回复确认报文,其中包含确认序号,作用就是告诉发送方自己接收到了哪些数据,下一次数据从哪里开始发。因此,确认序号应当是上次已成功收到数据字节序号加 1。只有 ACK 标志为 1 时确认序号字段才有效。
  • 首部长度:首部中的选项部分的长度是可变的,因此首部的长度也是可变的,所以需要这个字段来明确表示首部的长度,这个字段占 4 bit,4 位的二进制数最大可以表示为2的4次方-1也就是15,而首部长度是以 4 个字节为一个单位的,因此首部最大长度是 15 * 4 = 60 字节。
  • 保留字段:占 6 位,目前默认值为0。
  • 控制位:一共有 6 个标志位,它们表示的意义如下:
    • URG:紧急序号,值为 1 时,紧急指针生效
    • ACK:确认序号,值为 1 时,确认序号生效
    • PSH:接收方应尽快将这个报文段交给应用层
    • RST:发送端遇到问题,想要重建连接
    • SYN :同步序号,用于发起一个连接
    • FIN :结束序号,发送端要求关闭连接
  • 窗口大小:TCP的流量控制由连接的每一端通过声明的窗口大小来提供,标识接收方可接受的数据字节数。起始于确认序号字段指明的值,代表接收端希望接收的数据序号。窗口大小是一个 16 bit 字段,单位是字节,因而窗口大小最大为2的16次方-1也就是 65535 字节。
  • 检验和:用于验证数据完整性,也就是确保数据未被修改。检验和覆盖了整个 TCP 报文段,包括 TCP 首部和 TCP 数据,发送端根据特定算法对整个报文段计算出一个检验和,接收端会进行计算并验证。
  • 紧急指针:当 URG紧急序号,值为 1时,此字段生效。紧急指针的值与序号的值相加为紧急数据的最后一个字节位置,是TCP发送端向另一端发送紧急数据的一种方式。
  • 选项:是可选字段,最常见的可选字段是最长报文大小
  • 有效数据部分:这部分也不是必须的,比如在建立和关闭 TCP 连接的阶段,双方交换的报文段就只包含 TCP 首部

TCP三次握手

tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:

  1. 第一次握手是客户端向服务器发送报文,其中的SYN标志位的值为 1,表示这是一个用于请求发起连接的报文段,其中的序号字段seq被设置为初始序号x,TCP 连接双方均可随机选择初始序号。发送完报文段之后,客户端进入 SYN-SENT 状态,等待服务器的确认。
  2. 第二次握手是服务器在收到客户端的连接请求后,向客户端发送报文,其中 ACK 标志位设置为 1,表示对客户端做出应答,其确认序号字段ack生效,该字段值为 x + 1,也就是从客户端收到的报文段的序号加一,代表服务器期望下次收到客户端的数据的序号。此外,报文段的 SYN 标志位也设置为1,代表这同时也是一个用于发起连接的报文段,序号 seq 设置为服务器初始序号y。发送完报文段后,服务器进入 SYN-RECEIVED 状态。
  3. 第三次握手是客户端在收到报文段后,向服务器发送报文段,其 ACK 标志位为1,代表对服务器做出应答,确认序号字段 ack 为 y + 1,序号字段 seq 为 x + 1。此报文段发送完毕后,双方都进入 ESTABLISHED 状态,表示连接已建立。

image.png

  • 第一次握手: 客户端向服务器端发送报文
    证明客户端的发送能力正常
  • 第二次握手:服务器端接收到报文并向客户端发送报文
    证明服务器端的接收能力、发送能力正常
  • 第三次握手:客户端向服务器发送报文
    证明客户端的接收能力正常

三次握手是防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误和避免网络资源的浪费。如果双方两次握手即可建立连接的情况下,假设客户端发送 A 报文段请求建立连接,由于网络原因造成 A 暂时无法到达服务器,服务器接收不到请求报文段就不会返回确认报文段,客户端在长时间得不到应答的情况下重新发送请求报文段 B,这次 B 顺利到达服务器,服务器随即返回确认报文并进入连接状态,客户端在收到B报文段的确认报文后也进入连接状态,双方建立连接并传输数据,之后正常断开连接。此时姗姗来迟的 A 报文段才到达服务器,服务器随即返回确认报文并进入连接状态,但是已经进入 CLOSED 状态的客户端无法再接受确认报文段,这将导致服务器长时间单方面等待,造成资源浪费。所以三次握手才能让双方均确认自己和对方的发送和接收能力都正常

TCP四次挥手

建立一个连接需要三次握手,而终止一个连接要经过四次握手。因为一个 TCP 连接是全双工通信,每个方向必须单独地进行关闭。原则就是当一方完成它的数据发送任务后就能发送一个 FIN结束序号来终止这个方向连接。当一端收到一个 FIN,它必须通知应用层另一端已经终止了数据传送。理论上客户端和服务器都可以发起主动关闭,但是更多的情况下是客户端主动发起。

四次挥手详细过程如下:

  1. 第一次挥手:客户端向服务器发送关闭连接的报文段,FIN 标志位的值为1,请求关闭连接,并停止发送数据。序号字段 seq = x (x等于之前发送的所有数据的最后一个字节的序号加一),然后客户端会进入 FIN-WAIT-1 状态,等待来自服务器的确认报文。
  2. 第二次挥手:服务器收到报文后,发回确认报文,ACK = 1, 确认序号ack = x + 1,并带上自己的序号seq = y,然后服务器就进入 CLOSE-WAIT 状态。服务器还会通知上层的应用程序对方已经释放连接,此时 TCP 处于半关闭状态,也就是说客户端已经没有数据要发送了,但是服务器还可以发送数据,客户端也还能够接收。
  3. 第三次挥手:客户端收到服务器的报文后,随即进入 FIN-WAIT-2 状态,此时还能收到来自服务器的数据,直到收到服务器的结束报文。服务器发送完所有数据后,会向客户端发送结束报文,FIN标志位的值为1,ACK标志位的值为1,确认序号ack = x + 1,并带上自己的序号seq = z,随后服务器进入 LAST-ACK 状态,等待来自客户端的确认报文段。
  4. 第四次挥手:客户端收到服务器的报文后,向服务器发送报文,ACK标志位的值为1,确认序号ack = z + 1,并带上自己的序号seq = x + 1随后进入 TIME-WAIT 状态,等待2倍的报文段最大存活时间,常用值有30秒、1分钟和2分钟。如无特殊情况,客户端会进入关闭状态。服务器在接收到客户端的报文后会立刻进入关闭状态,由于没有等待时间,一般而言,服务器比客户端更早进入关闭状态。

image.png

服务器在收到客户端的结束报文后,可能还有一些数据要传输,所以不能马上关闭连接,但是会做出应答,返回报文的`ACK``标志位的值为的 ,接下来可能会继续发送数据,在数据发送完后,服务器会向客户端发送结束报文,表示数据已经发送完毕,请求关闭连接,然后客户端再做出应答,因此一共需要四次挥手。

客户端进入TIME-WAIT 状态,等待2倍的报文段最大存活时间是为了防止服务器没有收到客户端发送的第四次挥手的报文,服务器在接收不到 ACK 的情况下会一直重发结束报文。

最大报文段生存时间,即任何TCP报文在网络中存在的最大时长,如果超过这个时间,这个TCP报文就会被丢弃。 2MSL,即两个最大报文段生存时间。

TIME_WAIT状态是2MSL的时长原因是客户端不知道服务器是否能收到ACK应答报文,服务器如果没有收到报文,会一直重发结束报文,考虑最坏的一种情况是第四次挥手的ACK包的最大生存时长(MSL)和服务器重发的结束报文的最大生存时长(MSL)=2MSL

socket.io

socket.io 是一个基于事件驱动的实时通信库,可以在客户端和服务器之间建立持久连接,使得双向实时通信成为可能。

主要特点包括:

  • 实时性: socket.io构建在 WebSocket 协议之上,使用了 WebSocket 连接来实现实时通信。WebSocket 是一种双向通信协议,相比HTTP 请求-响应模型,它可以实现更快速、低延迟的数据传输。
  • 事件驱动: socket.io使用事件驱动的编程模型。服务器和客户端可以通过触发事件来发送和接收数据。这种基于事件的通信模式使得开发者可以轻松地构建实时的应用程序,例如聊天应用、实时协作工具等。
  • 跨平台支持: socket.io可以在多个平台上使用,包括浏览器、服务器和移动设备等。它提供了对多种编程语言和框架的支持,如 JavaScript、Node.js、Python、Java 等,使得开发者可以在不同的环境中构建实时应用程序。
  • 容错性: socket.io具有容错能力,当 WebSocket 连接不可用时,它可以自动降级到其他传输机制,如 HTTP 长轮询。这意味着即使在不支持 WebSocket 的环境中,socket.io仍然可以实现实时通信。
  • socket.io支持水平扩展,可以将应用程序扩展到多个服务器,并实现事件的广播和传递。这使得应用程序可以处理大规模的并发连接,并实现高可用性和高性能。

socket.io 的使用

node.js安装socket.io库

npm i socket.io

socket.io官网

服务端

http可以和socket进行搭配使用,http服务需要提供一个端口号供socket.io使用。

socket.io服务器API如下:

new Server(httpServer[, options]),接受两个参数,第一个参数是http服务器,第二个参数是一个配置对象

import http from 'http';
import {Server} from 'socket.io';
// 创建http服务器
const sever = http.createServer();
const io = new Server(sever,{
    cors:true, // 允许跨域
});
// socket.io是事件驱动模型
io.on('connection',socket => {
    console.log('连接成功');
    // 服务端接收客户端的消息
    socket.on('login', data => {
        // 第一个参数login是监听的事件名称,第二个参数data是接收的消息
        console.log(data)
    });
    // 服务端给客户端发送消息
    socket.emit('data','登录成功'); // 第一个参数是事件名称,后面的参数是要发送的消息作为参数传递
});
sever.listen(8089,() => {
    console.log('8089 port is running');
});

客户端

从版本4.3.0开始,ESM 捆绑包也可用:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>聊天室</title>
</head>

<body>

</body>
<!-- 通过CSDN引入socket.io -->
<script type="module">
    import { io } from "https://cdn.socket.io/4.7.5/socket.io.esm.min.js";
    const socket = io('ws://localhost:8089'); // ws连接的地址
    console.log(socket);
    // 监听事件
    socket.on('connect', () => {
        console.log('连接成功');
        // 客户端给服务端发送消息
        socket.emit('login', '123456'); // 第一个参数是事件名称,后面都是需要发送的消息作为参数传递
        // 客户端接收服务端的消息
        socket.on('data', data => {
            // 第一个参数data是监听的事件名称,第二个参数data是接收的消息
            console.log(data);
        });
    })
</script>

</html>

也可以从socket.io-client包中导入:

// ES modules
import { io } from "socket.io-client";

// CommonJS
const { io } = require("socket.io-client");

image.png

socket 代表的是当前的 client 和 server 间建立的这个连接,其中的 id 属性可以用于标识出这一连接,从而 server 可以向特定的用户发送消息。

事件驱动

Socket.IO API 可以在一侧发出事件并在另一侧注册侦听器

发送事件

基本的 emit

// 服务端发送数据
io.on("connection", (socket) => {
  socket.emit("hello", "world");
});
// 客户端接收数据
socket.on("hello", (arg) => {
  console.log(arg); // world
});

全双工通信,也适用于另一个方向

// 客户端发送数据
socket.emit("hello", "world");
// 服务端接收数据
io.on("connection", (socket) => {
  socket.on("hello", (arg) => {
    console.log(arg); // world
  });
});

可以发送任意数量的参数,并且支持所有可序列化的数据结构,包括像Buffer这样的二进制对象。

// 服务端发送数据
import http from 'http';
import {Server} from 'socket.io';
// 创建http服务器
const sever = http.createServer();
const io = new Server(sever,{
    cors:true, // 允许跨域
});
// socket.io是事件驱动模型
io.on('connection',socket => {
    console.log('连接成功');
    // 服务端给客户端发送消息
    // 第一个参数是事件名称,后面的参数是要发送的消息作为参数传递
    socket.emit('message',1,'2',true,{cipher:'暗号TiAmo',list:['a',123]},Buffer.from([6])); 
});
sever.listen(8089,() => {
    console.log('8089 port is running');
});
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>聊天室</title>
</head>

<body>

</body>
<!-- 通过CSDN引入socket.io -->
<script type="module">
    // 客户端接收数据
    import { io } from "https://cdn.socket.io/4.7.5/socket.io.esm.min.js";
    const socket = io('ws://localhost:8089'); // ws连接的地址
    // 监听事件
    socket.on('connect', () => {
        console.log('连接成功');
        // 客户端接收服务端的消息
        socket.on('message', (arg1,arg2,arg3,arg4,arg5) => {
            // 第一个参数data是监听的事件名称,第二个参数data是接收的消息
            console.log(arg1,arg2,arg3,arg4,arg5);
        });
    })
</script>

</html>

image.png

无需JSON.stringify(),它会自动完成。

// 错误
socket.emit("hello", JSON.stringify({ name: "John" }));

// 正确
socket.emit("hello", { name: "John" });

Date对象将被转换为字符串表示形式,例如1970-01-01T00:00:00.000Z

// 服务端
io.on('connection',socket => {
    console.log('连接成功');
    // 服务端给客户端发送消息
    // 第一个参数是事件名称,后面的参数是要发送的消息作为参数传递
    socket.emit('message',new Date()); 
});
// 客户端
socket.on('connect', () => {
        console.log('连接成功');
        // 客户端接收服务端的消息
        socket.on('message', (arg) => {
            // 第一个参数data是监听的事件名称,第二个参数data是接收的消息
            console.log(arg);
        });
    })

image.png

Map 和 Set的数据类型必须根据自身需求手动序列化成想要的格式

// 服务端
io.on('connection',socket => {
    console.log('连接成功');
    // 服务端给客户端发送消息
    // 创建Map对象初始传入参数是二维数组,new Map([["key1", "value1"], ["key2", "value2"]]);
    const map = new Map([['name','gloria'],['age',18]]);
    const set = new Set();
    set.add(1);
    set.add({a:678,b:890});
    socket.emit('message',map,set); 
});
// 客户端
socket.on('connect', () => {
        console.log('连接成功');
        // 客户端接收服务端的消息
        socket.on('message', (arg1,arg2) => {
            // 第一个参数data是监听的事件名称,第二个参数data是接收的消息
            console.log(arg1);
            console.log(arg2);
        });
    })

接收到的数据是两个空对象

image.png

// 服务端
io.on('connection', socket => {
    console.log('连接成功');
    // 服务端给客户端发送消息
    // 创建Map对象初始传入参数是二维数组,new Map([["key1", "value1"], ["key2", "value2"]]);
    const map = new Map([['name', 'gloria'], ['age', 18]]);
    const set = new Set();
    set.add(1);
    set.add({ a: 678, b: 890 });
    // Map 实例的 entries() 方法返回一个新的 map 迭代器对象,该对象包含了此 map 中的每个元素的 [key, value] 对,按插入顺序排列。
    console.log(map.entries());
    console.log(set.keys());
    // 手动序列化
    const serializedMap = [...map.entries()];
    const serializedSet = [...set.keys()];
    socket.emit('message', serializedMap, serializedSet);
});
// 客户端
socket.on('connect', () => {
        console.log('连接成功');
        // 客户端接收服务端的消息
        socket.on('message', (arg1,arg2) => {
            // 第一个参数data是监听的事件名称,第二个参数data是接收的消息
            console.log(arg1);
            console.log(arg2);
        });
    })

image.png

image.png

emit的回调

在某些情况下,可能需要更经典的请求-响应 API。在 Socket.IO 中,此功能称为确认,可以添加一个回调作为emit()的最后一个参数,一旦对方确认事件,就会调用此回调。

// 服务端
io.on("connection", (socket) => {
  socket.on("update item", (arg1, arg2, callback) => {
    console.log(arg1); // 1
    console.log(arg2); // { name: "updated" }
    callback({
      status: "ok"
    });
  });
});
// 客户端
socket.emit("update item", "1", { name: "updated" }, (response) => {
  console.log(response.status); // ok
});

image.png

image.png

emit超时

从 Socket.IO v4.4.0 开始,可以为每个发射分配超时:

socket.timeout(5000).emit("my-event", (err, response) => {
  if (err) {
    // 另一方在给定的延迟中没有确认该事件
  } else {
    console.log(response);
  }
});

发送易失性的消息

易失性事件是在底层连接未准备好时不会发送的事件,可以用于删除某些消息。socket.io会跟踪它接收到的消息,如果客户端错过了一条消息,它将再次发送。如果不希望这种开销(额外的工作),就可以发送易失性消息,但是客户端可能会错过一条消息。易失性消息不是不一致的,而是可以被丢弃的,这意味着当丢弃它们时,它们可能会被无序地接收。

// 服务端
io.on('connection', socket => {
    console.log('连接成功');
    // 服务端接收客户端发送的消息
    socket.on("ping", (count) => {
        console.log(count);
      });
});
// 客户端
socket.on('connect', () => {
        console.log('连接成功');
        // 客户端向服务端发送消息
        let count = 0;
        setInterval(() => {
            // socket.volatile.emit("ping", ++count);
            socket.emit("ping", ++count);
        }, 1000);
})

服务器重新启动,客户端自动重新连接并发送其缓冲的事件:

image.png

添加易失性事件

// 服务端
io.on('connection', socket => {
    console.log('连接成功');
    // 服务端接收客户端发送的消息
    socket.on("ping", (count) => {
        console.log(count);
      });
});
// 客户端
socket.on('connect', () => {
        console.log('连接成功');
        // 客户端向服务端发送消息
        let count = 0;
        setInterval(() => {
            socket.volatile.emit("ping", ++count);
            // socket.emit("ping", ++count);
        }, 1000);
})

image.png

监听事件

socket.on(eventName, listener):将侦听器函数添加到名为eventName的事件的侦听器数组的末尾。

socket.on("details", (...args) => {
  // ...
});

socket.once(eventName, listener):为名为eventName的事件添加一次性监听函数,listener回调函数只会执行一次。

socket.once("details", (...args) => {
  // 该函数只会执行一次
});

socket.off(eventName, listener):从名为eventName的事件的侦听器数组中移除指定的侦听器。

const listener = (...args) => {
  console.log(args);
}

// 添加侦听器函数
socket.on("details", listener);

// 移除侦听器函数
socket.off("details", listener);

socket.removeAllListeners([eventName]):删除所有侦听器,或指定eventName的侦听器。

// 删除指定eventName的侦听器
socket.removeAllListeners("details");
// 删除所有侦听器
socket.removeAllListeners();

从 Socket.IO v3 开始,受EventEmitter2库启发的新 API 允许声明 Catch-all 侦听器。此功能在客户端和服务器上均可用。

socket.onAny(listener):添加一个监听器,当任何事件发出时将被触发。

// 服务端
io.on('connection', socket => {
    console.log('连接成功');
    // 服务端接收客户端发送的消息
    socket.onAny((eventName, ...args) => {
        // eventName是事件名称,...args是事件的所有参数
        console.log(eventName);
        console.log(...args);
    });
    socket.emit('msg',{msg:'消息'},2345)
});
// 客户端
 socket.on('connect', () => {
        console.log('连接成功');
        // 客户端向服务端发送消息
        socket.emit("ping", 1, [2, 4, 5]);
        socket.onAny((eventName, ...args) => {
            console.log(eventName);
            console.log(...args);
        });
})

image.png

image.png

socket.prependAny(listener):添加一个监听器,当任何事件发出时将被触发。侦听器被添加到侦听器数组的开头。

socket.prependAny((eventName, ...args) => {
  // ...
});

socket.offAny([listener]):删除所有catch-all侦听器或给定的侦听器。

const listener = (eventName, ...args) => {
  console.log(eventName, args);
}

socket.onAny(listener);

// 删除给定的catch-all侦听器
socket.offAny(listener);

// 删除所有catch-all侦听器
socket.offAny();

验证

对事件参数进行验证,超出了 Socket.IO 库的范围。

但node.js生态系统中有许多验证的用例,其中包括:

const Joi = require("joi");

const userSchema = Joi.object({
  username: Joi.string().max(30).required(),
  email: Joi.string().email().required()
});

io.on("connection", (socket) => {
  socket.on("create user", (payload, callback) => {
    if (typeof callback !== "function") {
      // socket.disconnect()手动断开socket连接
      return socket.disconnect();
    }
    const { error, value } = userSchema.validate(payload);
    if (error) {
      return callback({
        status: "KO",
        error
      });
    }
    // 可以使用验证过的值做点事情,然后调用回调函数
    callback({
      status: "OK"
    });
  });

});

错误处理

Socket.IO 库中目前没有内置的错误处理,必须自行捕获任何可能在侦听器中引发的错误。

io.on("connection", (socket) => {
  socket.on("list items", async (callback) => {
    try {
      const items = await findItems();
      callback({
        status: "OK",
        items
      });
    } catch (e) {
      callback({
        status: "NOK"
      });
    }
  });
});

广播事件

Socket.IO 使向所有连接的客户端发送事件变得容易,广播是仅服务器功能。

给所有连接的客户端广播

image.png

io.emit("hello", "world");

当前断开连接(或正在重新连接)的客户端将不会收到该事件。

除发送者外的所有连接的客户端广播

image.png

io.on("connection", (socket) => {
  socket.broadcast.emit("hello", "world");
});

使用socket.emit("hello", "world"),不带broadcast标志会将事件也发送到客户端 A。

socket.io 的Namespace命名空间和room房间

命名空间是一种通信通道,允许通过单个共享连接(也称为“多路复用”)拆分应用程序的逻辑,使用不同的 Namespace,可以将不同功能的连接隔离开来,提高代码的可维护性。

image.png

每个命名空间都有自己的:

  • 事件处理程序
  • 房间
  • 中间件

在 Socket.io 里房间与命名空间都能实现 websocket 的多路复用,但是它们有一定的区别。

当 websocket 连接后,socket 会属于某个房间,还会属于某个命名空间。socket 与 room,namespace 的关系就像是个人,房间,房子的关系。如下图,每个 Namespace 里会有很多 Room,Room 里又会有很多 socket。

image.png

Namespace命名空间

连接的时候,使用路径名来指定命名空间。在没有指定命名空间下,默认会使用 / 作为命名空间。如果要想指定命名空间,则需要在客户端指定路径名,比如:/news,这样就指明进入的是 /news 命名空间。

<!-- 客户端 -->
<!-- 通过CSDN引入socket.io -->
<script type="module">
    // 客户端接收数据
    import { io } from "https://cdn.socket.io/4.7.5/socket.io.esm.min.js";
    // 同源
    const socket = io(); // 等同于io("/"), 默认会使用 `/` 作为命名空间
    const newsSocket = io("/news"); // 进入的是news命名空间
    // 跨域
    const socket = io('ws://localhost:8089'); // ws连接的地址,等同于io("ws://localhost:8089/"), 默认会使用 `/` 作为命名空间
    const newsSocket = io("ws://localhost:8089/news"); // 进入的是news命名空间
</script>
<script type="module">
    // 客户端接收数据
    import { io } from "https://cdn.socket.io/4.7.5/socket.io.esm.min.js";
    const socket = io("ws://localhost:8089"); // 进入的是/命名空间
    const newsSocket = io("ws://localhost:8089/news"); // 进入的是news命名空间
    // 监听事件
    socket.on('connect', () => {
        console.log('默认命名空间连接成功');
        socket.on('msg',arg => {
            console.log(arg)
        })
    });
    newsSocket.on('connect', () => {
        console.log('news命名空间连接成功');
    });
    newsSocket.on('hi', (arg) => {
        console.log(arg)
    });
</script>

在服务端里对应的处理,则需要使用 of

import http from 'http';
import { Server } from 'socket.io';
// 创建http服务器
const sever = http.createServer();
// io 在创建时,它就会被指派到默认的命名空间 `/`,那么它的广播只限于在 `/` 里的 socket 才收到,其他空间里是收不到消息的。
const io = new Server(sever, {
    cors: true, // 允许跨域
});
// 默认命名空间
io.on('connection', (socket) => {
    console.log('连接成功');
    socket.emit('msg', 666);
});
// news 命令空间
const news = io.of('/news');
news.on('connection', (socket) => {
    console.log('someone connected');
    // 只在本命名空间发送消息
    socket.emit('hi', 'everyone!');
});
sever.listen(8089, () => {
    console.log('8089 port is running');
});

image.png

io 在创建时,它就会被指派到默认的命名空间 /,那么它的广播只限于在 / 里的 socket 才收到,其他空间里是收不到消息的。

room房间

对于房间的进入与离开,可以使用 join 与 leave

io.on('connection', (socket) => {
    // 把 socket 扔进 room1 房间里
    socket.join('room1');
    // 再把 socket 赶出 room1 房间
    socket.leave('room1');
});

每个房间只属于某个命名空间,因此可以收听同一个命令空间的消息。而不同的房间之间是隔离的,它们不能接收不同房间的消息。

使用 to/in (它们是一样的)来对某个房间进行广播消息:

io.on('connection', (socket) => {
    socket.to('room1').to('room2').emit('hello');
    io.to('room1').emit('some event');
});

当连接时,默认会指派到一个唯一的房间,也就是用 socket.id 来命名的房间。这样的做法是让每个 socket 待在自己的房间里不受到其他人影响。

这样可以轻松地向其他 socket 广播消息:

io.on('connection', (socket) => {
    // id 是某个 socket.id,相当于去到他的房间里叫他
    socket.on('say to someone', (id, msg) => {
        socket.broadcast.to(id).emit('my message', msg);
    }
});

socket 可以进入多个房间接收信息,相当于一个QQ账号可以在 QQ 上加入多个群一样。可以使用 rooms 来查看,当前 socket 所在的房间。

io.on('connection', (socket) => {
    socket.join('room 237', () => {
        let rooms = Objects.keys(socket.rooms);
        console.log(rooms); // [ <socket.id>, 'room 237' ]
    });
});

利用命名空间与房间的特性,可以实现群聊与私聊:

  • 对于群聊,只需要把每个 socket 扔进同一个房间即可。
  • 对于私聊,只需要把两个 socket 扔进同一个房间即可。