从 2.x 迁移到 3.0
此版本应修复 Socket.IO 库的大多数不一致之处,并为最终用户提供更直观的行为。这是多年来社区反馈的结果。感谢所有参与的人!
TL;DR: 由于几个重大更改,v2 客户端将无法连接到 v3 服务器(反之亦然)
更新: 从 Socket.IO 3.1.0 开始,v3 服务器现在能够与 v2 客户端通信。更多信息 如下。但是,v3 客户端仍然无法连接到 v2 服务器。
有关低级详细信息,请参阅
以下是更改的完整列表
- io.set() 已删除
- 不再隐式连接到默认命名空间
- Namespace.connected 已重命名为 Namespace.sockets,现在是 Map
- Socket.rooms 现在是 Set
- Socket.binary() 已删除
- Socket.join() 和 Socket.leave() 现在是同步的
- Socket.use() 已删除
- 中间件错误现在将发出一个 Error 对象
- 在 Manager 查询选项和 Socket 查询选项之间添加明确的区分
- Socket 实例将不再转发其 Manager 发出的事件
- Namespace.clients() 已重命名为 Namespace.allSockets(),现在返回一个 Promise
- 客户端包
- 不再使用 "pong" 事件来检索延迟
- ES 模块语法
emit()
链不再可能- 房间名称不再强制转换为字符串
配置
更合理的默认值
maxHttpBufferSize
的默认值已从100MB
降低到1MB
。- WebSocket permessage-deflate 扩展 现在默认情况下已禁用
- 您现在必须明确列出允许的域(有关 CORS,请参见 下文)
withCredentials
选项现在在客户端默认情况下为false
CORS 处理
在 v2 中,Socket.IO 服务器会自动添加必要的标头以允许 跨域资源共享 (CORS)。
这种行为虽然方便,但在安全性方面并不理想,因为它意味着所有域都可以访问您的 Socket.IO 服务器,除非使用 origins
选项另行指定。
因此,从 Socket.IO v3 开始
- CORS 现在默认情况下已禁用
origins
选项(用于提供授权域列表)和handlePreflightRequest
选项(用于编辑Access-Control-Allow-xxx
标头)已替换为cors
选项,该选项将转发到 cors 包。
选项的完整列表可以在 此处 找到。
之前
const io = require("socket.io")(httpServer, {
origins: ["https://example.com"],
// optional, useful for custom headers
handlePreflightRequest: (req, res) => {
res.writeHead(200, {
"Access-Control-Allow-Origin": "https://example.com",
"Access-Control-Allow-Methods": "GET,POST",
"Access-Control-Allow-Headers": "my-custom-header",
"Access-Control-Allow-Credentials": true
});
res.end();
}
});
之后
const io = require("socket.io")(httpServer, {
cors: {
origin: "https://example.com",
methods: ["GET", "POST"],
allowedHeaders: ["my-custom-header"],
credentials: true
}
});
默认情况下不再使用 cookie
在以前的版本中,默认情况下会发送一个 io
cookie。此 cookie 可用于启用粘性会话,这在您有多个服务器并启用了 HTTP 长轮询时仍然需要(更多信息 此处)。
但是,在某些情况下不需要此 cookie(即单服务器部署、基于 IP 的粘性会话),因此现在必须明确启用它。
之前
const io = require("socket.io")(httpServer, {
cookieName: "io",
cookieHttpOnly: false,
cookiePath: "/custom"
});
之后
const io = require("socket.io")(httpServer, {
cookie: {
name: "test",
httpOnly: false,
path: "/custom"
}
});
所有其他选项(域、maxAge、sameSite 等)现在都受支持。有关选项的完整列表,请参见 此处。
API 更改
以下是列出的非向后兼容更改。
io.set() 已删除
此方法在 1.0 版本中已弃用,并保留以实现向后兼容性。现在已删除。
它已被中间件替换。
之前
io.set("authorization", (handshakeData, callback) => {
// make sure the handshake data looks good
callback(null, true); // error first, "authorized" boolean second
});
之后
io.use((socket, next) => {
var handshakeData = socket.request;
// make sure the handshake data looks good as before
// if error do this:
// next(new Error("not authorized"));
// else just call next
next();
});
不再隐式连接到默认命名空间
此更改会影响多路复用功能(我们在 Socket.IO 中称为命名空间)的用户。
在以前的版本中,即使客户端请求访问另一个命名空间,它也会始终连接到默认命名空间 (/
)。这意味着为默认命名空间注册的中间件将被触发,这可能非常令人惊讶。
// client-side
const socket = io("/admin");
// server-side
io.use((socket, next) => {
// not triggered anymore
});
io.on("connection", socket => {
// not triggered anymore
})
io.of("/admin").use((socket, next) => {
// triggered
});
此外,我们现在将引用 "主" 命名空间而不是 "默认" 命名空间。
Namespace.connected 已重命名为 Namespace.sockets,现在是 Map
connected
对象(用于存储连接到给定命名空间的所有 Socket)可用于从其 ID 检索 Socket 对象。它现在是 ES6 Map。
之前
// get a socket by ID in the main namespace
const socket = io.of("/").connected[socketId];
// get a socket by ID in the "admin" namespace
const socket = io.of("/admin").connected[socketId];
// loop through all sockets
const sockets = io.of("/").connected;
for (const id in sockets) {
if (sockets.hasOwnProperty(id)) {
const socket = sockets[id];
// ...
}
}
// get the number of connected sockets
const count = Object.keys(io.of("/").connected).length;
之后
// get a socket by ID in the main namespace
const socket = io.of("/").sockets.get(socketId);
// get a socket by ID in the "admin" namespace
const socket = io.of("/admin").sockets.get(socketId);
// loop through all sockets
for (const [_, socket] of io.of("/").sockets) {
// ...
}
// get the number of connected sockets
const count = io.of("/").sockets.size;
Socket.rooms 现在是 Set
rooms
属性包含 Socket 当前所在的房间列表。它是一个对象,现在是 ES6 Set。
之前
io.on("connection", (socket) => {
console.log(Object.keys(socket.rooms)); // [ <socket.id> ]
socket.join("room1");
console.log(Object.keys(socket.rooms)); // [ <socket.id>, "room1" ]
});
之后
io.on("connection", (socket) => {
console.log(socket.rooms); // Set { <socket.id> }
socket.join("room1");
console.log(socket.rooms); // Set { <socket.id>, "room1" }
});
Socket.binary() 已删除
binary
方法可用于指示给定事件不包含任何二进制数据(以便跳过库执行的查找并提高某些条件下的性能)。
它已被提供您自己的解析器的能力所取代,该能力是在 Socket.IO 2.0 中添加的。
之前
socket.binary(false).emit("hello", "no binary");
之后
const io = require("socket.io")(httpServer, {
parser: myCustomParser
});
有关示例,请参见 socket.io-msgpack-parser。
Socket.join() 和 Socket.leave() 现在是同步的
异步性是 Redis 适配器的前几个版本所必需的,但现在不再是这种情况。
作为参考,适配器是一个对象,它存储 Socket 和 房间 之间的关联关系。有两个官方适配器:内存中适配器(内置)和基于 Redis 发布-订阅机制 的 Redis 适配器。
之前
socket.join("room1", () => {
io.to("room1").emit("hello");
});
socket.leave("room2", () => {
io.to("room2").emit("bye");
});
之后
socket.join("room1");
io.to("room1").emit("hello");
socket.leave("room2");
io.to("room2").emit("bye");
注意: 自定义适配器可能会返回一个 Promise,因此前面的示例变为
await socket.join("room1");
io.to("room1").emit("hello");
Socket.use() 已删除
socket.use()
可用作通配符监听器。但它的 API 并不真正直观。它被 socket.onAny() 替换。
更新: Socket.use()
方法已在 socket.io@3.0.5
中恢复。
之前
socket.use((packet, next) => {
console.log(packet.data);
next();
});
之后
socket.onAny((event, ...args) => {
console.log(event);
});
中间件错误现在将发出一个 Error 对象
error
事件已重命名为 connect_error
,发出的对象现在是实际的 Error
之前
// server-side
io.use((socket, next) => {
next(new Error("not authorized"));
});
// client-side
socket.on("error", err => {
console.log(err); // not authorized
});
// or with an object
// server-side
io.use((socket, next) => {
const err = new Error("not authorized");
err.data = { content: "Please retry later" }; // additional details
next(err);
});
// client-side
socket.on("error", err => {
console.log(err); // { content: "Please retry later" }
});
之后
// server-side
io.use((socket, next) => {
const err = new Error("not authorized");
err.data = { content: "Please retry later" }; // additional details
next(err);
});
// client-side
socket.on("connect_error", err => {
console.log(err instanceof Error); // true
console.log(err.message); // not authorized
console.log(err.data); // { content: "Please retry later" }
});
在 Manager 查询选项和 Socket 查询选项之间添加明确的区分
在以前的版本中,query
选项在两个不同的位置使用
- 在 HTTP 请求的查询参数中 (
GET /socket.io/?EIO=3&abc=def
) - 在
CONNECT
数据包中
让我们以以下示例为例
const socket = io({
query: {
token: "abc"
}
});
在幕后,io()
方法中发生了以下情况
const { Manager } = require("socket.io-client");
// a new Manager is created (which will manage the low-level connection)
const manager = new Manager({
query: { // sent in the query parameters
token: "abc"
}
});
// and then a Socket instance is created for the namespace (here, the main namespace, "/")
const socket = manager.socket("/", {
query: { // sent in the CONNECT packet
token: "abc"
}
});
这种行为会导致奇怪的行为,例如当 Manager 被重用于另一个命名空间(多路复用)时
// client-side
const socket1 = io({
query: {
token: "abc"
}
});
const socket2 = io("/my-namespace", {
query: {
token: "def"
}
});
// server-side
io.on("connection", (socket) => {
console.log(socket.handshake.query.token); // abc (ok!)
});
io.of("/my-namespace").on("connection", (socket) => {
console.log(socket.handshake.query.token); // abc (what?)
});
因此,Socket 实例的 query
选项在 Socket.IO v3 中重命名为 auth
// plain object
const socket = io({
auth: {
token: "abc"
}
});
// or with a function
const socket = io({
auth: (cb) => {
cb({
token: "abc"
});
}
});
// server-side
io.on("connection", (socket) => {
console.log(socket.handshake.auth.token); // abc
});
注意: Manager 的 query
选项仍然可以用于向 HTTP 请求添加特定的查询参数。
Socket 实例将不再转发其 Manager 发出的事件
在以前的版本中,Socket 实例会发出与底层连接状态相关的事件。这将不再是这种情况。
您仍然可以访问 Manager 实例(socket 的 io
属性)上的这些事件
之前
socket.on("reconnect_attempt", () => {});
之后
socket.io.on("reconnect_attempt", () => {});
以下是 Manager 发出的事件的更新列表
名称 | 描述 | 以前(如果不同) |
---|---|---|
open | 成功(重新)连接 | - |
error | (重新)连接失败或在成功连接后发生错误 | connect_error |
close | 断开连接 | - |
ping | ping 数据包 | - |
packet | 数据包 | - |
reconnect_attempt | 重新连接尝试 | reconnect_attempt & reconnecting |
reconnect | 成功重新连接 | - |
重连错误 | 重连失败 | - |
重连失败 | 所有尝试后重连失败 | - |
以下是 Socket 发出的事件更新列表
名称 | 描述 | 以前(如果不同) |
---|---|---|
连接 | 成功连接到命名空间 | - |
connect_error | 连接失败 | error |
断开连接 | 断开连接 | - |
最后,以下是您在应用程序中无法使用的保留事件的更新列表
connect
(在客户端使用)connect_error
(在客户端使用)disconnect
(在两端使用)disconnecting
(在服务器端使用)newListener
和removeListener
(EventEmitter 保留事件)
socket.emit("connect_error"); // will now throw an Error
Namespace.clients() 已重命名为 Namespace.allSockets(),现在返回一个 Promise
此函数返回连接到此命名空间的套接字 ID 列表。
之前
// all sockets in default namespace
io.clients((error, clients) => {
console.log(clients); // => [6em3d4TJP8Et9EMNAAAA, G5p55dHhGgUnLUctAAAB]
});
// all sockets in the "chat" namespace
io.of("/chat").clients((error, clients) => {
console.log(clients); // => [PZDoMHjiu8PYfRiKAAAF, Anw2LatarvGVVXEIAAAD]
});
// all sockets in the "chat" namespace and in the "general" room
io.of("/chat").in("general").clients((error, clients) => {
console.log(clients); // => [Anw2LatarvGVVXEIAAAD]
});
之后
// all sockets in default namespace
const ids = await io.allSockets();
// all sockets in the "chat" namespace
const ids = await io.of("/chat").allSockets();
// all sockets in the "chat" namespace and in the "general" room
const ids = await io.of("/chat").in("general").allSockets();
注意:此函数(并且仍然是)由 Redis 适配器支持,这意味着它将返回所有 Socket.IO 服务器上的套接字 ID 列表。
客户端捆绑包
现在有 3 个不同的捆绑包
名称 | 大小 | 描述 |
---|---|---|
socket.io.js | 34.7 kB gzip | 未压缩版本,带有 debug |
socket.io.min.js | 14.7 kB min+gzip | 生产版本,不带 debug |
socket.io.msgpack.min.js | 15.3 kB min+gzip | 生产版本,不带 debug 并且带有 msgpack 解析器 |
默认情况下,所有这些都由服务器在 /socket.io/<name>
上提供。
之前
<!-- note: this bundle was actually minified but included the debug package -->
<script src="/socket.io/socket.io.js"></script>
之后
<!-- during development -->
<script src="/socket.io/socket.io.js"></script>
<!-- for production -->
<script src="/socket.io/socket.io.min.js"></script>
不再有用于检索延迟的“pong”事件
在 Socket.IO v2 中,您可以在客户端监听 pong
事件,其中包含上次健康检查往返行程的持续时间。
由于心跳机制的逆转(更多信息 此处),此事件已被删除。
之前
socket.on("pong", (latency) => {
console.log(latency);
});
之后
// server-side
io.on("connection", (socket) => {
socket.on("ping", (cb) => {
if (typeof cb === "function")
cb();
});
});
// client-side
setInterval(() => {
const start = Date.now();
// volatile, so the packet will be discarded if the socket is not connected
socket.volatile.emit("ping", () => {
const latency = Date.now() - start;
// ...
});
}, 5000);
ES 模块语法
ECMAScript 模块语法现在类似于 Typescript 语法(参见 下面)。
之前(使用默认导入)
// server-side
import Server from "socket.io";
const io = new Server(8080);
// client-side
import io from 'socket.io-client';
const socket = io();
之后(使用命名导入)
// server-side
import { Server } from "socket.io";
const io = new Server(8080);
// client-side
import { io } from 'socket.io-client';
const socket = io();
emit()
链不再可能
emit()
方法现在与 EventEmitter.emit()
方法签名匹配,并返回 true
而不是当前对象。
之前
socket.emit("event1").emit("event2");
之后
socket.emit("event1");
socket.emit("event2");
房间名称不再强制转换为字符串
我们现在在内部使用 Maps 和 Sets 而不是普通对象,因此房间名称不再隐式强制转换为字符串。
之前
// mixed types were possible
socket.join(42);
io.to("42").emit("hello");
// also worked
socket.join("42");
io.to(42).emit("hello");
之后
// one way
socket.join("42");
io.to("42").emit("hello");
// or another
socket.join(42);
io.to(42).emit("hello");
新功能
其中一些新功能可能会移植回 2.4.x
分支,具体取决于用户的反馈。
通配符监听器
此功能的灵感来自 EventEmitter2 库(为了不增加浏览器捆绑包大小,该库没有直接使用)。
它适用于服务器端和客户端
// server
io.on("connection", (socket) => {
socket.onAny((event, ...args) => {});
socket.prependAny((event, ...args) => {});
socket.offAny(); // remove all listeners
socket.offAny(listener);
const listeners = socket.listenersAny();
});
// client
const socket = io();
socket.onAny((event, ...args) => {});
socket.prependAny((event, ...args) => {});
socket.offAny(); // remove all listeners
socket.offAny(listener);
const listeners = socket.listenersAny();
易失性事件(客户端)
易失性事件是允许在低级传输尚未准备好时(例如,当 HTTP POST 请求已挂起时)丢弃的事件。
此功能已在服务器端可用。它也可能在客户端有用,例如当套接字未连接时(默认情况下,数据包会缓冲直到重新连接)。
socket.volatile.emit("volatile event", "might or might not be sent");
带有 msgpack 解析器的官方捆绑包
现在将提供带有 socket.io-msgpack-parser 的捆绑包(在 CDN 上或由服务器在 /socket.io/socket.io.msgpack.min.js
上提供)。
优点
- 带有二进制内容的事件作为 1 个 WebSocket 帧发送(而不是使用默认解析器时的 2 个以上)
- 包含大量数字的有效负载应该更小
缺点
- 不支持 IE9 (https://caniuse.cn/mdn-javascript_builtins_arraybuffer)
- 捆绑包大小略大
// server-side
const io = require("socket.io")(httpServer, {
parser: require("socket.io-msgpack-parser")
});
客户端不需要额外的配置。
其他
Socket.IO 代码库已重写为 TypeScript
这意味着 npm i -D @types/socket.io
不再需要。
服务器
import { Server, Socket } from "socket.io";
const io = new Server(8080);
io.on("connection", (socket: Socket) => {
console.log(`connect ${socket.id}`);
socket.on("disconnect", () => {
console.log(`disconnect ${socket.id}`);
});
});
客户端
import { io } from "socket.io-client";
const socket = io("/");
socket.on("connect", () => {
console.log(`connect ${socket.id}`);
});
纯 javascript 显然仍然完全受支持。
正式放弃对 IE8 和 Node.js 8 的支持
IE8 在 Sauce Labs 平台上不再可测试,并且需要大量工作才能为很少的用户(如果有的话)提供支持,因此我们放弃了对它的支持。
此外,Node.js 8 现在 EOL。请尽快升级!
如何升级现有的生产部署
- 首先,使用
allowEIO3
设置为true
更新服务器(在socket.io@3.1.0
中添加)
const io = require("socket.io")({
allowEIO3: true // false by default
});
注意:如果您使用 Redis 适配器来 在节点之间广播数据包,则必须将 socket.io-redis@5
与 socket.io@2
以及 socket.io-redis@6
与 socket.io@3
一起使用。请注意,这两个版本都是兼容的,因此您可以逐个更新每个服务器(不需要大爆炸)。
- 然后,更新客户端
此步骤实际上可能需要一些时间,因为一些客户端可能仍然在缓存中保留 v2 客户端。
您可以使用以下方法检查连接的版本
io.on("connection", (socket) => {
const version = socket.conn.protocol; // either 3 or 4
});
这与 HTTP 请求中 EIO
查询参数的值匹配。
- 最后,一旦所有客户端都更新完毕,将
allowEIO3
设置为false
(这是默认值)
const io = require("socket.io")({
allowEIO3: false
});
将 allowEIO3
设置为 false
后,v2 客户端现在将在连接时收到 HTTP 400 错误(不支持的协议版本
)。
已知迁移问题
stream_1.pipeline 不是函数
TypeError: stream_1.pipeline is not a function
at Function.sendFile (.../node_modules/socket.io/dist/index.js:249:26)
at Server.serve (.../node_modules/socket.io/dist/index.js:225:16)
at Server.srv.on (.../node_modules/socket.io/dist/index.js:186:22)
at emitTwo (events.js:126:13)
at Server.emit (events.js:214:7)
at parserOnIncoming (_http_server.js:602:12)
at HTTPParser.parserOnHeadersComplete (_http_common.js:116:23)
此错误可能是由于您的 Node.js 版本造成的。 pipeline 方法是在 Node.js 10.0.0 中引入的。
错误 TS2416:类型 'Namespace' 中的属性 'emit' 不可分配给基类型 'EventEmitter' 中的相同属性。
node_modules/socket.io/dist/namespace.d.ts(89,5): error TS2416: Property 'emit' in type 'Namespace' is not assignable to the same property in base type 'EventEmitter'.
Type '(ev: string, ...args: any[]) => Namespace' is not assignable to type '(event: string | symbol, ...args: any[]) => boolean'.
Type 'Namespace' is not assignable to type 'boolean'.
node_modules/socket.io/dist/socket.d.ts(84,5): error TS2416: Property 'emit' in type 'Socket' is not assignable to the same property in base type 'EventEmitter'.
Type '(ev: string, ...args: any[]) => this' is not assignable to type '(event: string | symbol, ...args: any[]) => boolean'.
Type 'this' is not assignable to type 'boolean'.
Type 'Socket' is not assignable to type 'boolean'.
emit()
方法的签名在版本 3.0.1
中已修复 (提交)。
- 在发送大型有效负载(> 1MB)时,客户端断开连接
这可能是由于 maxHttpBufferSize
的默认值现在为 1MB
。当收到大于此值的包时,服务器会断开客户端连接,以防止恶意客户端使服务器过载。
您可以在创建服务器时调整该值
const io = require("socket.io")(httpServer, {
maxHttpBufferSize: 1e8
});
跨源请求被阻止:同源策略不允许读取 xxx/socket.io/?EIO=4&transport=polling&t=NMnp2WI 处的远程资源。(原因:缺少 CORS 标头“Access-Control-Allow-Origin”。)
从 Socket.IO v3 开始,您需要显式启用 跨源资源共享 (CORS)。文档可以在 此处 找到。
未捕获的 TypeError:packet.data 未定义
您似乎正在使用 v3 客户端连接到 v2 服务器,这是不可能的。请参阅 以下部分。
对象文字只能指定已知属性,并且 'extraHeaders' 不存在于类型 'ConnectOpts' 中
由于代码库已重写为 TypeScript(更多信息 此处),因此不再需要 @types/socket.io-client
,实际上它会与来自 socket.io-client
包的类型定义冲突。
- 跨源上下文中缺少 cookie
如果您前端没有从与后端相同的域提供服务,则现在需要显式启用 cookie
服务器
import { Server } from "socket.io";
const io = new Server({
cors: {
origin: ["https://front.domain.com"],
credentials: true
}
});
客户端
import { io } from "socket.io-client";
const socket = io("https://backend.domain.com", {
withCredentials: true
});
参考