跳至主要内容
版本: 4.x

从 2.x 迁移到 3.0

此版本应修复 Socket.IO 库的大多数不一致之处,并为最终用户提供更直观的行为。这是多年来社区反馈的结果。感谢所有参与的人!

TL;DR: 由于几个重大更改,v2 客户端将无法连接到 v3 服务器(反之亦然)

更新: 从 Socket.IO 3.1.0 开始,v3 服务器现在能够与 v2 客户端通信。更多信息 如下。但是,v3 客户端仍然无法连接到 v2 服务器。

有关低级详细信息,请参阅

以下是更改的完整列表

配置

更合理的默认值

  • 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
}
});

在以前的版本中,默认情况下会发送一个 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断开连接-
pingping 数据包-
packet数据包-
reconnect_attempt重新连接尝试reconnect_attempt & reconnecting
reconnect成功重新连接-
重连错误重连失败-
重连失败所有尝试后重连失败-

以下是 Socket 发出的事件更新列表

名称描述以前(如果不同)
连接成功连接到命名空间-
connect_error连接失败error
断开连接断开连接-

最后,以下是您在应用程序中无法使用的保留事件的更新列表

  • connect(在客户端使用)
  • connect_error(在客户端使用)
  • disconnect(在两端使用)
  • disconnecting(在服务器端使用)
  • newListenerremoveListener(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.js34.7 kB gzip未压缩版本,带有 debug
socket.io.min.js14.7 kB min+gzip生产版本,不带 debug
socket.io.msgpack.min.js15.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 个以上)
  • 包含大量数字的有效负载应该更小

缺点

// 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@5socket.io@2 以及 socket.io-redis@6socket.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
});

参考