从 3.x 迁移到 4.0
4.0.0 版本添加了许多新功能,这些功能在 下面 有详细说明,但它也包含一些 API 更改(因此主版本号增加)。
请注意,这些更改仅影响服务器端的 API。Socket.IO 协议本身没有更新,因此 v3 客户端 **将** 能够连接到 v4 服务器,反之亦然。此外,兼容模式 (allowEIO3: true
) 在 Socket.IO v2 客户端和 Socket.IO v4 服务器之间仍然可用。
以下是更改的完整列表
重大更改
io.to()
现在是不可变的
以前,向给定房间广播(通过调用 io.to()
)会改变 io 实例,这会导致意外的行为,例如
io.to("room1");
io.to("room2").emit(/* ... */); // also sent to room1
// or with async/await
io.to("room3").emit("details", await fetchDetails()); // random behavior: maybe in room3, maybe to all clients
调用 io.to()
(或任何其他广播修饰符)现在将返回一个不可变的实例。
示例
const operator1 = io.to("room1");
const operator2 = operator1.to("room2");
const operator3 = socket.broadcast;
const operator4 = socket.to("room3").to("room4");
operator1.emit(/* ... */); // only to clients in "room1"
operator2.emit(/* ... */); // to clients in "room1" or in "room2"
operator3.emit(/* ... */); // to all clients but the sender
operator4.emit(/* ... */); // to clients in "room3" or in "room4" but the sender
wsEngine
选项
为了消除以下错误,wsEngine
选项的格式已更新
关键依赖项:依赖项的请求是一个表达式
在使用 webpack 打包服务器时。
之前
const io = require("socket.io")(httpServer, {
wsEngine: "eiows"
});
之后
const io = require("socket.io")(httpServer, {
wsEngine: require("eiows").Server
});
配置
确保与 Swift v15 客户端兼容
在 16.0.0 版本之前,Swift 客户端不会在 HTTP 请求中包含 EIO
查询参数,Socket.IO v3 服务器会默认推断 EIO=4
。
这就是为什么 Swift 客户端 v15 无法连接到服务器的原因,即使启用了兼容模式 (allowEIO3: true
),除非您显式指定查询参数
let manager = SocketManager(socketURL: URL(string: "http://localhost:8080")!, config: [
.log(true),
.connectParams(["EIO": "3"])
])
let socket = manager.defaultSocket
Socket.IO v4 服务器现在将在不包含 EIO
查询参数的情况下推断 EIO=3
。
pingTimeout
的默认值已增加
pingTimeout
(用于 心跳机制)的默认值在 socket.io@2.1.0
(2018 年 3 月)中从 60000 更新为 5000。
当时的推理
一些用户在服务器端和客户端断开连接之间经历了长时间的延迟。“断开连接”事件在浏览器中需要很长时间才能触发,可能是由于计时器延迟。因此有了这个改变。
话虽如此,当前值(5 秒)会导致在通过慢速网络发送大量有效负载时意外断开连接,因为它会阻止客户端和服务器之间交换 ping-pong 数据包。当同步任务阻塞服务器超过 5 秒时,也会发生这种情况。
因此,新值(20 秒)似乎是在快速断开连接检测和对各种延迟的容忍度之间取得了良好的平衡。
新功能
允许在广播时排除特定房间
感谢 Sebastiaan Marynissen 的出色工作,您现在可以在广播时排除特定房间
io.except("room1").emit(/* ... */); // to all clients except the ones in "room1"
io.to("room2").except("room3").emit(/* ... */); // to all clients in "room2" except the ones in "room3"
socket.broadcast.except("room1").emit(/* ... */); // to all clients except the ones in "room1" and the sender
socket.except("room1").emit(/* ... */); // same as above
socket.to("room4").except("room5").emit(/* ... */); // to all clients in "room4" except the ones in "room5" and the sender
允许将数组传递给 io.to()
to()
方法现在接受房间数组。
之前
const rooms = ["room1", "room2", "room3"];
for (const room of rooms) {
io.to(room);
}
// broadcast to clients in "room1", "room2" or "room3"
// WARNING !!! this does not work anymore in v4, see the breaking change above
io.emit(/* ... */);
之后
io.to(["room1", "room2", "room3"]).emit(/* ... */);
socket.to(["room1", "room2", "room3"]).emit(/* ... */);
其他实用程序方法
添加了一些(期待已久)的方法
socketsJoin
:使匹配的套接字实例加入指定的房间
// make all Socket instances join the "room1" room
io.socketsJoin("room1");
// make all Socket instances of the "admin" namespace in the "room1" room join the "room2" room
io.of("/admin").in("room1").socketsJoin("room2");
socketsLeave
:使匹配的套接字实例离开指定的房间
// make all Socket instances leave the "room1" room
io.socketsLeave("room1");
// make all Socket instances of the "admin" namespace in the "room1" room leave the "room2" room
io.of("/admin").in("room1").socketsLeave("room2");
disconnectSockets
:使匹配的套接字实例断开连接
// make all Socket instances disconnect
io.disconnectSockets();
// make all Socket instances of the "admin" namespace in the "room1" room disconnect
io.of("/admin").in("room1").disconnectSockets();
// this also works with a single socket ID
io.of("/admin").in(theSocketId).disconnectSockets();
fetchSockets
:返回匹配的套接字实例
// return all Socket instances of the main namespace
const sockets = await io.fetchSockets();
// return all Socket instances of the "admin" namespace in the "room1" room
const sockets = await io.of("/admin").in("room1").fetchSockets();
// this also works with a single socket ID
const sockets = await io.in(theSocketId).fetchSockets();
上面示例中的 sockets
变量是一个对象数组,它公开了一部分通常的 Socket 类
for (const socket of sockets) {
console.log(socket.id);
console.log(socket.handshake);
console.log(socket.rooms);
socket.emit(/* ... */);
socket.join(/* ... */);
socket.leave(/* ... */);
socket.disconnect(/* ... */);
}
这些方法共享与广播相同的语义,并且相同的过滤器适用
io.of("/admin").in("room1").except("room2").local.disconnectSockets();
这使得“admin”命名空间的所有 Socket 实例
- 在“room1”房间中(
in("room1")
或to("room1")
) - 除了在“room2”中的那些(
except("room2")
) - 并且仅在当前 Socket.IO 服务器上(
local
)
断开连接。
类型化事件
感谢 Maxime Kjaer 的出色工作,TypeScript 用户现在可以对客户端和服务器之间发送的事件进行类型化。
首先,您声明每个事件的签名
interface ClientToServerEvents {
noArg: () => void;
basicEmit: (a: number, b: string, c: number[]) => void;
}
interface ServerToClientEvents {
withAck: (d: string, cb: (e: number) => void) => void;
}
现在您可以在客户端使用它们
import { io, Socket } from "socket.io-client";
const socket: Socket<ServerToClientEvents, ClientToServerEvents> = io();
socket.emit("noArg");
socket.emit("basicEmit", 1, "2", [3]);
socket.on("withAck", (d, cb) => {
cb(4);
});
您的 IDE 现在应该正确推断每个参数的类型
类似地,在服务器端(ServerToClientEvents
和 ClientToServerEvents
被反转)
import { Server } from "socket.io";
const io = new Server<ClientToServerEvents, ServerToClientEvents>(3000);
io.on("connection", (socket) => {
socket.on("noArg", () => {
// ...
});
socket.on("basicEmit", (a, b, c) => {
// ...
});
socket.emit("withAck", "42", (e) => {
console.log(e);
});
});
默认情况下,事件是无类型的,参数将被推断为 any
。
autoUnref
选项
最后,感谢 KC Erb 的出色工作,添加了 autoUnref
选项。
将 autoUnref
设置为 true(默认值:false),Socket.IO 客户端将允许程序在事件系统中没有其他活动计时器/TCP 套接字的情况下退出(即使客户端已连接)
const socket = io({
autoUnref: true
});
注意:此选项仅适用于 Node.js 客户端。
已知迁移问题
无法获取未定义的 emit
以下表达式
socket.to("room1").broadcast.emit(/* ... */);
在 Socket.IO v3 中有效,但现在被认为无效,因为 broadcast
标志是无用的,因为 to("room1")
方法已经将 Socket 实例置于广播模式。
// VALID
socket.broadcast.emit(/* ... */); // to all clients but the sender
socket.to("room1").emit(/* ... */); // to clients in "room1" but the sender
// VALID (but useless 'broadcast' flag)
socket.broadcast.to("room1").emit(/* ... */);
// INVALID
socket.to("room1").broadcast.emit(/* ... */);