起因
聊天室类型的项目中,是需要判断一次成功的连接是初次连接,还是意外断线后的重连。
以前用的版本是 socket.io 库 version-2.x
client API中,socket 上是有 reconnect 事件的。
现在升级成 version-4.x ,发现 sokcet 上没这个事件了
Socket.IO 利用 WebSocket 协议建立通信。WebSockets 允许通过单个 TCP 连接实现双向(全双工)通信通道。这意味着一旦建立了连接,客户端(如网络浏览器)和服务器都可以实时相互发送数据,而无需重复打开新的连接。这非常适合需要持续交换数据的应用,如即时聊天应用或在线游戏
提供了namespace、Manager等概念
【Client】中 socket 和 Manager 之间的关系,这里讲的更清楚。
github.com/socketio/so…
在Socket.io中,"socket"是指一个客户端与服务器之间的实时通信连接。它是一个抽象的概念,代表了一个持久化的双向通信通道,可以通过该通道进行实时的数据传输和事件触发。
主要特点:
- 跨平台支持多种语言,Node、PY、Java 等
- 自动回退机制:如果WebSocket不可用,Socket.IO会自动回退到其他协议(如XHR轮询、JSONP轮询)。
- 命名空间
- 支持房间(Rooms)功能,可以实现群组通信。
emit 和 广播
// server-side
socket.emit('xxx', params) // 给 连接的client 发个事件
nsp.emit('xxx', params) // 在【命名空间】内广播
// 重点!!
serverIO.emit('xxx', params) // 在【主命名空间】内发广播,也就是在【/】广播
// 等价于
serverIO.of("/").emit("hello")
命名空间 Namespace
我理解为同一个连接中的「不同频道」,或【Server-side】的路由path
Socket.IO 中的命名空间提供了一种将应用程序划分为不同通信通道的方法。
想在同一应用程序中支持不同类型的交互或数据交换,而又不想让它们相互干扰时,这就很有帮助。
// Server-side
// 在服务器上设置了一个命名空间
const nsp = io.of('/my-namespace'); // Create a new namespace called '/my-namespace' nsp.on('connection', function(socket) {
console.log('someone connected to the namespace');
});
// Client-side
const socket = io('/my-namespace');
const socket = io("ws://example.com:8080/my-namespace");
// Connect to the server namespace '/my-namespace'
假如客户端连接时 namespace 为/demo,而服务端发送消息emit(namespace="/")指定的命名空间为默认的/,那这个消息是否会发给客户端?答案是会。因为前面说到,每个客户端默认加入到了/中,所以,服务端的消息肯定会发给客户端的,但是客户端接收到消息会检查namespace是否与其connect时的namespace一致,如果不一致,虽然接收到了消息但是并不会触发客户端的操作。
房间 room
【房间 room】只存在于【server-side】,不存在于【client-side】,毕竟客户端只处理自己这条连接。
【房间 room】能够更进一步的组织【命名空间 namespace】内的信息发送方式。
使用场景为:
不想让命名空间内的所有人都参与,而只想与特定的用户组通信。此时,使用【房间 room】可以向特定的客户端子集广播消息。
socket.rooms : Set<Room>- 此 socket 加入了哪些 room
socket.adapter.rooms : Map <Room, Set<SocketId> >- 此 Namespace 中,每个 room 里面都有哪些 socket
socket.adapter.sids : Map<SocketId, Set<Room>>- 此 Namespace 中,每个 socket 都加入进了哪些 room
在 server-side ,每个 socket 成功建立连接后,
namespace 中,会自动创建一个以此 socket.id 为名的 room
并且,此 socket 会自动加入这个以自己id为名的 room
【房间 room】这个概念是定义在【Namespace】下层的。
一个【房间 room】是只属于某个【Namespace】的 ,不能跨 【Namespace】的。
绝不存在一个 socket 能加入另一个命名空间的房间
// server-side
// 例子:有两个【命名空间 namespace】,各自都只连接了1个 client
const NSP1 = io.of('/nsp1');
NSP1.on('connection', socket =>{
socket.join(`nsp1-r-${socket.id}`);
socket.join(`r-01`);
console.log(socket.rooms);
// set(3) , 表示:此 socket 加入了 3 个 room
console.log(socket.adapter.rooms);
// set(3),表示:此【命名空间 namespace】中有 3 个 room
console.log(socket.adapter === socket.nsp.adapter); // true
});
const NSP2 = io.of('/nsp2');
NSP2.on('connection', socket =>{
socket.join(`nsp2-r-${socket.id}`);
socket.join(`r-01`);
console.log(socket.rooms);
console.log(socket.adapter.rooms);
console.log(socket.adapter === socket.nsp.adapter);
});
// NSP1、NSP2 这两个【命名空间】中,都各自有 3 个room,并且每个 room 中都只有1个 socket
// 这说明 NSP1的 r-01 和 NSP2的 r-01 是两个房间,只是名字一样而已。
是可以的
但后果是,离开后,用 socket.emit() 就无法给【C端】发送事件了
例子1
// server-side
socket.emit('hello', params); // client 对应事件会触发
socket.leave(socket.id); // 离开自己 socketId 的房间
socket.emit('hello', params); // client 对应事件不会触发了。
例子2
// server-side
// 有两个【C端】,分别是A和B
// 让 socket_b 离开 socket_b.id 的房间,然后加入 socket_a.id 房间
// 此时再 socket_a.emit()
const nspSocketsMap = chatRoomNSP.sockets;
if(nspSocketsMap.size > 1){
const socket_A_id = [...nspSocketsMap.keys()][0]
socket.leave(socket.id); // socket_b 离开自己的id的room
socket.join(socket_A_id);
}
else{
// 让 socket_A 用 socket.emit() 给自己的 client 发事件
setTimeout(function(){
socket.emit('hello', params);
},20000);
}
// 结果是:client_A 和 client_B 都收到了这个 hello 事件
说明,socket.emit()给自己的对端client发事件,本质上是通过 socket.id 的那个 room 发广播,只不过一般情况下这个room里只有自己。
socket.emit() 等价于 socket.to(自己的socketId).emit()
server.to(room)- 是进入【主命名空间】的某房间。
- 等价于
server.of('/').to()
namespace.to(room)socket.to(room)- 与上面【namespace】的区别是,发广播时,是排除 socket 自己的。
- 也就是说,是发给房间内除自己外的所有人。
直到目前版本 【socket.io v4.7】
API中没有给出:直接销毁一个房间,然后房间内的所有 socket 都自动被踢出
当下的做法:房间被清空了,里面没有socket了,此房间就会自动销毁。
io.of("/").adapter.on("delete-room", (room, id) => {} ) 会触发
断开连接
client 主动断开
浏览器关闭
直接关闭浏览器,发送的是 websocket 的标准CLOSE消息,opcode为8。
socket.io服务端处理方式基本一致,由于这种情况下并没有发送socket.io的关闭消息41,socket.io的关闭操作需要等到engine.io触发的_handle_eio_disconnect()中处理,这就是前一节中为什么engine.io服务器后面还要多调用一次 _handle_eio_disconnect()的原因所在。
问题:
- 「S端」获取所有 socket 。明明有属性 .sockets,为什么 还有方法 fetchSockets 还是个 异步的
判断重连
无论是「第一次连接」还是后续的「断线重连」,触发的都是 【connect】事件。
那如何判断某次【connect】事件的触发,到底是「第一次连接」还是「断线重连」?
有两种方式
方式一: 「S端」有个地方保存了「当前已建立的连接」,无论是自己弄个 Map 或是数据库在线用户表。 「C端」建立连接时,带上参数表明身份,比如 userId。 「S端」在【connect】事件中拿到这个身份,然后去「当前已建立的连接」中查找。 若已存在,就表明是「断线重连」。若不存在,就是「第一次连接」,并把这个身材加入进去。
用这种方式, 其实是由我们自己来进行判断的,是在「业务层面」实现的。 每次【connect】事件中的 socket.id 都是新的,这表明在 「socketIO 库的层面」本质上是一次全新的连接。
方式二:socket.recovered
这是「socketIO 库层面」提供的方式。
这个属性生效的前提,是要在「S端」端配置 connectionStateRecovery。
若不配置,
- 「C端」的 socket.recovered 永远是 undefined
- 「S端」的 socket.recovered 永远是 false