从 3.x 迁移到 4.0
4.0.0 版本添加了相当多的新功能,详细信息为 如下,但它也包含一些 API 重大更改(因此是重大改进)。
¥The 4.0.0 release adds quite a lot of new features, which are detailed below, but it also contains a few API breaking changes (hence the major bump).
请注意,这些重大更改仅影响服务器端的 API。Socket.IO 协议本身没有更新,因此 v3 客户端将能够访问 v4 服务器,反之亦然。此外,兼容模式(allowEIO3: true
)在 Socket.IO v2 客户端和 Socket.IO v4 服务器之间仍然可用。
¥Please note that these breaking changes only impact the API on the server side. The Socket.IO protocol itself was not updated, so a v3 client will be able to reach a v4 server and vice-versa. Besides, the compatibility mode (allowEIO3: true
) is still available between a Socket.IO v2 client and a Socket.IO v4 server.
以下是完整的更改列表:
¥Here is the complete list of changes:
重大变化
¥Breaking changes
io.to()
现在是不可变的
¥io.to()
is now immutable
以前,向给定房间广播(通过调用 io.to()
)会改变 io 实例,这可能会导致令人惊讶的行为,例如:
¥Previously, broadcasting to a given room (by calling io.to()
) would mutate the io instance, which could lead to surprising behaviors, like:
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()
(或任何其他广播修饰符)现在将返回一个不可变的实例。
¥Calling io.to()
(or any other broadcast modifier) will now return an immutable instance.
示例:
¥Examples:
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
option
更新了 wsEngine
选项的格式以消除以下错误:
¥The format of the wsEngine
option was updated in order to get rid of the following error:
Critical dependency: the request of a dependency is an expression
将服务器与 webpack 打包在一起时。
¥when bundling the server with webpack.
前:
¥Before:
const io = require("socket.io")(httpServer, {
wsEngine: "eiows"
});
后:
¥After:
const io = require("socket.io")(httpServer, {
wsEngine: require("eiows").Server
});
配置
¥Configuration
确保与 Swift v15 客户端的兼容性
¥Ensure compatibility with Swift v15 clients
在版本 16.0.0 之前,Swift 客户端不会在 HTTP 请求中包含 EIO
查询参数,Socket.IO v3 服务器默认会推断出 EIO=4
。
¥Before version 16.0.0, the Swift client would not include the EIO
query parameter in the HTTP requests, and the Socket.IO v3 server would infer EIO=4
by default.
这就是为什么 Swift 客户端 v15 无法连接到服务器,即使启用了兼容模式 (allowEIO3: true
),除非你显式指定查询参数:
¥That's why a Swift client v15 was not able to connect to the server, even when the compatibility mode was enabled (allowEIO3: true
), unless you explicitly specified the query param:
let manager = SocketManager(socketURL: URL(string: "http://localhost:8080")!, config: [
.log(true),
.connectParams(["EIO": "3"])
])
let socket = manager.defaultSocket
如果不包含 EIO
查询参数,Socket.IO v4 服务器现在将推断 EIO=3
。
¥The Socket.IO v4 server will now infer EIO=3
if the EIO
query param is not included.
增加了 pingTimeout
的默认值
¥The default value of pingTimeout
was increased
pingTimeout
(在 心跳机制 中使用)的默认值在 socket.io@2.1.0
(2018 年 3 月)中从 60000 更新为 5000。
¥The default value of pingTimeout
(used in the heartbeat mechanism) value was updated from 60000 to 5000 in socket.io@2.1.0
(March 2018).
当时的推断:
¥The reasoning back then:
一些用户在服务器端和客户端断开连接之间遇到了长时间的延迟。"disconnect" 事件在浏览器中需要很长时间才能触发,可能是由于计时器被延迟。因此发生了变化。
¥Some users experienced long delays between disconnection on the server-side and on the client-side. The "disconnect" event would take a long time to fire in the browser, probably due to a timer being delayed. Hence the change.
话虽这么说,当通过慢速网络发送大负载时,当前值(5s)会导致意外断开连接,因为它阻止了客户端和服务器之间交换乒乓数据包。当同步任务阻塞服务器超过 5 秒时,也会发生这种情况。
¥That being said, the current value (5s) caused unexpected disconnections when a big payload was sent over a slow network, because it prevents the ping-pong packets from being exchanged between the client and the server. This can also happen when a synchronous task blocks the server for more than 5 seconds.
因此,新值(20 秒)似乎在快速断开连接检测和对各种延迟的容忍度之间取得了良好的平衡。
¥The new value (20s) thus seems like a good balance between quick disconnection detection and tolerance to various delays.
新特性
¥New features
允许在广播时排除特定房间
¥Allow excluding specific rooms when broadcasting
感谢 塞巴斯蒂安·玛丽尼森 的出色工作,你现在可以在广播时排除特定房间:
¥Thanks to the awesome work of Sebastiaan Marynissen, you can now exclude a specific room when broadcasting:
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()
¥Allow to pass an array to io.to()
to()
方法现在接受房间数组。
¥The to()
method now accepts an array of rooms.
前:
¥Before:
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(/* ... */);
后:
¥After:
io.to(["room1", "room2", "room3"]).emit(/* ... */);
socket.to(["room1", "room2", "room3"]).emit(/* ... */);
附加实用方法
¥Additional utility methods
添加了一些(期待已久的)方法:
¥Some (long-awaited) methods were added:
socketsJoin
:使匹配的套接字实例加入指定的房间¥
socketsJoin
: makes the matching socket instances join the specified rooms
// 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
:使匹配的套接字实例离开指定的房间¥
socketsLeave
: makes the matching socket instances leave the specified rooms
// 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
:使匹配的套接字实例断开连接¥
disconnectSockets
: makes the matching socket instances disconnect
// 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
:返回匹配的套接字实例¥
fetchSockets
: returns the matching socket instances
// 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 类的子集:
¥The sockets
variable in the example above is an array of objects exposing a subset of the usual Socket class:
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(/* ... */);
}
这些方法与广播具有相同的语义,并且应用相同的过滤器:
¥Those methods share the same semantics as broadcasting, and the same filters apply:
io.of("/admin").in("room1").except("room2").local.disconnectSockets();
这使得 "管理" 命名空间的所有 Socket 实例
¥Which makes all Socket instances of the "admin" namespace
在 "room1" 房间(
in("room1")
或to("room1")
)¥in the "room1" room (
in("room1")
orto("room1")
)除 "room2"(
except("room2")
)中的¥except the ones in "room2" (
except("room2")
)并且仅在当前 Socket.IO 服务器 (
local
) 上¥and only on the current Socket.IO server (
local
)
断开。
¥disconnect.
类型事件
¥Typed events
感谢 Maxime Kjaer 的出色工作,TypeScript 用户现在可以输入客户端和服务器之间发送的事件。
¥Thanks to the awesome work of Maxime Kjaer, TypeScript users can now type the events sent between the client and the server.
首先,声明每个事件的签名:
¥First, you declare the signature of each event:
interface ClientToServerEvents {
noArg: () => void;
basicEmit: (a: number, b: string, c: number[]) => void;
}
interface ServerToClientEvents {
withAck: (d: string, cb: (e: number) => void) => void;
}
现在你可以在客户端使用它们:
¥And you can now use them on the client side:
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 现在应该正确推断每个参数的类型:
¥Your IDE should now properly infer the type of each argument:
同样在服务器端(ServerToClientEvents
和 ClientToServerEvents
颠倒):
¥Similarly on the server side (the ServerToClientEvents
and ClientToServerEvents
are reversed):
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
。
¥By default, the events are untyped and the arguments will be inferred as any
.
autoUnref
选项
¥autoUnref
option
最后,感谢 KC 埃尔布 的出色工作,添加了 autoUnref
选项。
¥And finally, thanks to the awesome work of KC Erb, the autoUnref
option was added.
将 autoUnref
设置为 true(默认值:false)时,如果事件系统中没有其他活动计时器/TCP 套接字(即使客户端已连接),Socket.IO 客户端将允许程序退出:
¥With autoUnref
set to true (default: false), the Socket.IO client will allow the program to exit if there is no other active timer/TCP socket in the event system (even if the client is connected):
const socket = io({
autoUnref: true
});
注意:此选项仅适用于 Node.js 客户端。
¥Note: this option only applies to Node.js clients.
已知的迁移问题
¥Known migration issues
cannot get emit of undefined
下面的表达式:
¥The following expression:
socket.to("room1").broadcast.emit(/* ... */);
曾在 Socket.IO v3 中工作,但现在被认为无效,因为 broadcast
标志无用,因为 to("room1")
方法已经将 Socket 实例置于广播模式。
¥was working in Socket.IO v3 but is now considered invalid, as the broadcast
flag is useless because the to("room1")
method already puts the Socket instance in broadcasting mode.
// 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(/* ... */);