socket.io 笔记

171 阅读6分钟

起因

聊天室类型的项目中,是需要判断一次成功的连接是初次连接,还是意外断线后的重连。

以前用的版本是 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】可以向特定的客户端子集广播消息。

room相关的属性\color{darkorange}{与 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能离开以自己id为名的房间么?\color{darkorange}{socket 能离开以自己id为名的房间么?}
是可以的
但后果是,离开后,用 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()

API中,几个.to(<room>)的区别\color{darkorange}{API中,几个 .to(< room >) 的区别}

  • server.to(room)
    • 是进入【主命名空间】的某房间。
    • 等价于server.of('/').to()
  • namespace.to(room)
  • socket.to(room)
    • 与上面【namespace】的区别是,发广播时,是排除 socket 自己的。
    • 也就是说,是发给房间内除自己外的所有人。

如何销毁一个房间?\color{darkorange}{如何销毁一个房间?}
直到目前版本 【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

参考

baijiahao.baidu.com/s?id=170926…

juejin.cn/post/705881…