Skip to main content
Version: 4.x

从 2.x 迁移到 3.0

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

¥This release should fix most of the inconsistencies of the Socket.IO library and provide a more intuitive behavior for the end users. It is the result of the feedback of the community over the years. A big thanks to everyone involved!

TL;DR:due to several breaking changes, a v2 client will not be able to connect to a v3 server (and vice versa)

更新:从 套接字 IO 3.1.0 开始,v3 服务器现在可以与 v2 客户端进行通信。更多信息在 如下。但 v3 客户端仍然无法连接到 v2 服务器。

¥Update: As of Socket.IO 3.1.0, the v3 server is now able to communicate with v2 clients. More information below. A v3 client is still not be able to connect to a v2 server though.

有关底层详细信息,请参阅:

¥For the low-level details, please see:

以下是完整的更改列表:

¥Here is the complete list of changes:

配置

¥Configuration

健全的默认值

¥Saner default values

  • maxHttpBufferSize 的默认值从 100MB 减少到 1MB

    ¥the default value of maxHttpBufferSize was decreased from 100MB to 1MB.

  • WebSocket 许可放气扩展 现在默认禁用

    ¥the WebSocket permessage-deflate extension is now disabled by default

  • 你现在必须明确列出允许的域(对于 CORS,请参阅 如下

    ¥you must now explicitly list the domains that are allowed (for CORS, see below)

  • withCredentials 选项现在在客户端默认为 false

    ¥the withCredentials option now defaults to false on the client side

CORS 处理

¥CORS handling

在 v2 中,Socket.IO 服务器自动添加必要的标头以允许 跨域资源共享 (CORS)。

¥In v2, the Socket.IO server automatically added the necessary headers to allow Cross-Origin Resource Sharing (CORS).

这种行为虽然方便,但在安全性方面并不是很好,因为它意味着所有域都可以访问你的 Socket.IO 服务器,除非使用 origins 选项另行指定。

¥This behavior, while convenient, was not great in terms of security, because it meant that all domains were allowed to reach your Socket.IO server, unless otherwise specified with the origins option.

这就是为什么,从 Socket.IO v3 开始:

¥That's why, as of Socket.IO v3:

  • CORS 现在默认处于禁用状态

    ¥CORS is now disabled by default

  • origins 选项(用于提供授权域列表)和 handlePreflightRequest 选项(用于编辑 Access-Control-Allow-xxx 标头)被 cors 选项替换,该选项将转发到 cors 包。

    ¥the origins option (used to provide a list of authorized domains) and the handlePreflightRequest option (used to edit the Access-Control-Allow-xxx headers) are replaced by the cors option, which will be forwarded to the cors package.

完整的选项列表可以在 此处 中找到。

¥The complete list of options can be found here.

前:

¥Before:

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

后:

¥After:

const io = require("socket.io")(httpServer, {
cors: {
origin: "https://example.com",
methods: ["GET", "POST"],
allowedHeaders: ["my-custom-header"],
credentials: true
}
});

¥No more cookie by default

在以前的版本中,默认发送 io cookie。此 cookie 可用于启用粘性会话,当你有多个服务器并启用 HTTP 长轮询时,仍然需要启用粘性会话(更多信息 此处)。

¥In previous versions, an io cookie was sent by default. This cookie can be used to enable sticky-session, which is still required when you have several servers and HTTP long-polling enabled (more information here).

但是,在某些情况下不需要此 cookie(即单服务器部署、基于 IP 的粘性会话),因此现在必须显式启用它。

¥However, this cookie is not needed in some cases (i.e. single server deployment, sticky-session based on IP) so it must now be explicitly enabled.

前:

¥Before:

const io = require("socket.io")(httpServer, {
cookieName: "io",
cookieHttpOnly: false,
cookiePath: "/custom"
});

后:

¥After:

const io = require("socket.io")(httpServer, {
cookie: {
name: "test",
httpOnly: false,
path: "/custom"
}
});

现在支持所有其他选项(domain、maxAge、sameSite...)。请参阅 此处 了解完整的选项列表。

¥All other options (domain, maxAge, sameSite, ...) are now supported. Please see here for the complete list of options.

API 变更

¥API change

下面列出了非向后兼容的更改。

¥Below are listed the non backward-compatible changes.

io.set() 已删除

¥io.set() is removed

此方法在 1.0 版本中已弃用,并保留以实现向后兼容性。现在它已被删除。

¥This method was deprecated in the 1.0 release and kept for backward-compatibility. It is now removed.

它被中间件取代。

¥It was replaced by middlewares.

前:

¥Before:

io.set("authorization", (handshakeData, callback) => {
// make sure the handshake data looks good
callback(null, true); // error first, "authorized" boolean second
});

后:

¥After:

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

不再隐式连接到默认命名空间

¥No more implicit connection to the default namespace

此更改会影响多路复用功能(我们在 Socket.IO 中称为命名空间)的用户。

¥This change impacts the users of the multiplexing feature (what we call Namespace in Socket.IO).

在以前的版本中,客户端始终会连接到默认命名空间 (/),即使它请求访问另一个命名空间也是如此。这意味着为默认名称空间注册的中间件被触发,这可能会令人非常惊讶。

¥In previous versions, a client would always connect to the default namespace (/), even if it requested access to another namespace. This meant that the middlewares registered for the default namespace were triggered, which may be quite surprising.

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

此外,我们现在将引用 "main" 命名空间而不是 "default" 命名空间。

¥Besides, we will now refer to the "main" namespace instead of the "default" namespace.

Namespace.connected 已重命名为 Namespace.sockets,现在是一个 Map

¥Namespace.connected is renamed to Namespace.sockets and is now a Map

connected 对象(用于存储连接到给定命名空间的所有 Socket)可用于从其 id 检索 Socket 对象。现在是 ES6 映射

¥The connected object (used to store all the Socket connected to the given Namespace) could be used to retrieve a Socket object from its id. It is now an ES6 Map.

前:

¥Before:

// 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;

后:

¥After:

// 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

¥Socket.rooms is now a Set

rooms 属性包含 Socket 当前所在的房间列表。它曾经是一个对象,现在是一个 ES6 Set

¥The rooms property contains the list of rooms the Socket is currently in. It was an object, it is now an ES6 Set.

前:

¥Before:

io.on("connection", (socket) => {

console.log(Object.keys(socket.rooms)); // [ <socket.id> ]

socket.join("room1");

console.log(Object.keys(socket.rooms)); // [ <socket.id>, "room1" ]

});

后:

¥After:

io.on("connection", (socket) => {

console.log(socket.rooms); // Set { <socket.id> }

socket.join("room1");

console.log(socket.rooms); // Set { <socket.id>, "room1" }

});

Socket.binary() 被删除

¥Socket.binary() is removed

binary 方法可用于指示给定事件不包含任何二进制数据(以便跳过库完成的查找并提高某些条件下的性能)。

¥The binary method could be used to indicate that a given event did not contain any binary data (in order to skip the lookup done by the library and improve performance in certain conditions).

它被 Socket.IO 2.0 中添加的提供你自己的解析器的功能所取代。

¥It was replaced by the ability to provide your own parser, which was added in Socket.IO 2.0.

前:

¥Before:

socket.binary(false).emit("hello", "no binary");

后:

¥After:

const io = require("socket.io")(httpServer, {
parser: myCustomParser
});

例如,请参见 socket.io-msgpack-parser

¥Please see socket.io-msgpack-parser for example.

Socket.join() 和 Socket.leave() 现在是同步的

¥Socket.join() and Socket.leave() are now synchronous

Redis 适配器的第一个版本需要异步性,但现在情况已不再如此。

¥The asynchronicity was needed for the first versions of the Redis adapter, but this is not the case anymore.

作为参考,适配器是一个存储套接字和 房间 之间关系的对象。有两个官方适配器:内存适配器(内置)和基于 Redis 发布-订阅机制Redis 适配器

¥For reference, an Adapter is an object that stores the relationships between Sockets and Rooms. There are two official adapters: the in-memory adapter (built-in) and the Redis adapter based on Redis pub-sub mechanism.

前:

¥Before:

socket.join("room1", () => {
io.to("room1").emit("hello");
});

socket.leave("room2", () => {
io.to("room2").emit("bye");
});

后:

¥After:

socket.join("room1");
io.to("room1").emit("hello");

socket.leave("room2");
io.to("room2").emit("bye");

注意:自定义适配器可能会返回 Promise,因此前面的示例变为:

¥Note: custom adapters may return a Promise, so the previous example becomes:

await socket.join("room1");
io.to("room1").emit("hello");

Socket.use() is removed

socket.use() 可以用作包罗万象的监听器。但它的 API 并不是很直观。已被 socket.onAny() 取代。

¥socket.use() could be used as a catch-all listener. But its API was not really intuitive. It is replaced by socket.onAny().

UPDATE:Socket.use() 方法在 socket.io@3.0.5 恢复。

¥UPDATE: the Socket.use() method was restored in socket.io@3.0.5.

前:

¥Before:

socket.use((packet, next) => {
console.log(packet.data);
next();
});

后:

¥After:

socket.onAny((event, ...args) => {
console.log(event);
});

中间件错误现在将触发 Error 对象

¥A middleware error will now emit an Error object

error 事件被重命名为 connect_error,并且触发的对象现在是一个实际的错误:

¥The error event is renamed to connect_error and the object emitted is now an actual Error:

前:

¥Before:

// 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" }
});

后:

¥After:

// 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 查询选项之间的明显区别

¥Add a clear distinction between the Manager query option and the Socket query option

在以前的版本中,query 选项用在两个不同的地方:

¥In previous versions, the query option was used in two distinct places:

  • 在 HTTP 请求的查询参数中 (GET /socket.io/?EIO=3&abc=def)

    ¥in the query parameters of the HTTP requests (GET /socket.io/?EIO=3&abc=def)

  • CONNECT 数据包中

    ¥in the CONNECT packet

让我们看下面的例子:

¥Let's take the following example:

const socket = io({
query: {
token: "abc"
}
});

在幕后,以下是 io() 方法中发生的事情:

¥Under the hood, here's what happened in the io() method:

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 被另一个名称空间重用时(多路复用):

¥This behavior could lead to weird behaviors, for example when the Manager was reused for another namespace (multiplexing):

// 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

¥That's why the query option of the Socket instance is renamed to auth in Socket.IO v3:

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

注意:仍然可以使用管理器的 query 选项来向 HTTP 请求添加特定的查询参数。

¥Note: the query option of the Manager can still be used in order to add a specific query parameter to the HTTP requests.

Socket 实例将不再转发其 Manager 触发的事件

¥The Socket instance will no longer forward the events emitted by its Manager

在以前的版本中,Socket 实例触发与底层连接状态相关的事件。情况将不再如此。

¥In previous versions, the Socket instance emitted the events related to the state of the underlying connection. This will not be the case anymore.

你仍然可以访问 Manager 实例(套接字的 io 属性)上的这些事件:

¥You can still have access to those events on the Manager instance (the io property of the socket) :

前:

¥Before:

socket.on("reconnect_attempt", () => {});

后:

¥After:

socket.io.on("reconnect_attempt", () => {});

以下是管理器触发的事件的更新列表:

¥Here is the updated list of events emitted by the Manager:

名称描述以前(如果不同)
打开成功(重新)连接*
错误(重新)连接失败或连接成功后出现错误连接错误
关闭断开*
ping 数据包*
数据包*
重新连接尝试重新连接尝试reconnect_attempt 和重新连接*
重新连接重连成功*
重新连接错误重连失败*
重新连接失败所有尝试后重新连接失败*

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

¥Here is the updated list of events emitted by the Socket:

名称描述以前(如果不同)
connect成功连接到命名空间*
连接错误连接失败错误
disconnect断开*

最后,这是你不能在应用中使用的保留事件的更新列表:

¥And finally, here's the updated list of reserved events that you cannot use in your application:

  • connect(在客户端使用)

    ¥connect (used on the client-side)

  • connect_error(在客户端使用)

    ¥connect_error (used on the client-side)

  • disconnect(双面使用)

    ¥disconnect (used on both sides)

  • disconnecting(在服务器端使用)

    ¥disconnecting (used on the server-side)

  • newListenerremoveListener(事件触发器 预留事件

    ¥newListener and removeListener (EventEmitter reserved events)

socket.emit("connect_error"); // will now throw an Error

Namespace.clients() 已重命名为 Namespace.allSockets() 并且现在返回 Promise

¥Namespace.clients() is renamed to Namespace.allSockets() and now returns a Promise

该函数返回连接到该命名空间的套接字 ID 列表。

¥This function returns the list of socket IDs that are connected to this namespace.

前:

¥Before:

// 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]
});

后:

¥After:

// 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 列表。

¥Note: this function was (and still is) supported by the Redis adapter, which means that it will return the list of socket IDs across all the Socket.IO servers.

客户端打包包

¥Client bundles

现在有 3 个不同的打包包:

¥There are now 3 distinct bundles:

名称尺寸描述
套接字.io.js34.7 kB 压缩未缩小版本,带有 debug
socket.io.min.js最小 14.7 kB+gzip量产版,不含 debug
socket.io.msgpack.min.js最小 15.3 kB+gzip量产版,不带 debug,带 消息包解析器

默认情况下,所有这些都由位于 /socket.io/<name> 的服务器提供服务。

¥By default, all of them are served by the server, at /socket.io/<name>.

前:

¥Before:

<!-- note: this bundle was actually minified but included the debug package -->
<script src="/socket.io/socket.io.js"></script>

后:

¥After:

<!-- during development -->
<script src="/socket.io/socket.io.js"></script>
<!-- for production -->
<script src="/socket.io/socket.io.min.js"></script>

不再有 "pong" 事件用于检索延迟

¥No more "pong" event for retrieving latency

在 Socket.IO v2 中,你可以在客户端监听 pong 事件,其中包括上次健康检查往返的持续时间。

¥In Socket.IO v2, you could listen to the pong event on the client-side, which included the duration of the last health check round-trip.

由于心跳机制的逆转(更多信息 此处),该事件已被删除。

¥Due to the reversal of the heartbeat mechanism (more information here), this event has been removed.

前:

¥Before:

socket.on("pong", (latency) => {
console.log(latency);
});

后:

¥After:

// 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 模块语法

¥ES modules syntax

ECMAScript 模块语法现在类似于 Typescript 语法(参见 如下)。

¥The ECMAScript modules syntax is now similar to the Typescript one (see below).

之前(使用默认导入):

¥Before (using default import):

// server-side
import Server from "socket.io";

const io = new Server(8080);

// client-side
import io from 'socket.io-client';

const socket = io();

之后(使用命名导入):

¥After (with named import):

// 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() chains are not possible anymore

emit() 方法现在与 EventEmitter.emit() 方法签名匹配,并返回 true 而不是当前对象。

¥The emit() method now matches the EventEmitter.emit() method signature, and returns true instead of the current object.

前:

¥Before:

socket.emit("event1").emit("event2");

后:

¥After:

socket.emit("event1");
socket.emit("event2");

房间名称不再被强制字符串

¥Room names are not coerced to string anymore

我们现在在内部使用映射和集合而不是普通对象,因此房间名称不再隐式强制为字符串。

¥We are now using Maps and Sets internally instead of plain objects, so the room names are not implicitly coerced to string anymore.

前:

¥Before:

// mixed types were possible
socket.join(42);
io.to("42").emit("hello");
// also worked
socket.join("42");
io.to(42).emit("hello");

后:

¥After:

// one way
socket.join("42");
io.to("42").emit("hello");
// or another
socket.join(42);
io.to(42).emit("hello");

新特性

¥New features

根据用户的反馈,其中一些新功能可能会向后移植到 2.4.x 分支。

¥Some of those new features may be backported to the 2.4.x branch, depending on the feedback of the users.

捕获所有监听器

¥Catch-all listeners

此功能的灵感来自 EventEmitter2 库(不直接使用该库,以免增加浏览器包大小)。

¥This feature is inspired from the EventEmitter2 library (which is not used directly in order not to increase the browser bundle size).

它适用于服务器端和客户端:

¥It is available for both the server and the client sides:

// 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();

易失性事件(客户端)

¥Volatile events (client)

易失性事件是在底层传输尚未准备好时允许删除的事件(例如,当 HTTP POST 请求已挂起时)。

¥A volatile event is an event that is allowed to be dropped if the low-level transport is not ready yet (for example when an HTTP POST request is already pending).

该功能已经在服务器端可用。它在客户端也可能很有用,例如当套接字未连接时(默认情况下,数据包将被缓冲直到重新连接)。

¥This feature was already available on the server-side. It might be useful on the client-side as well, for example when the socket is not connected (by default, packets are buffered until reconnection).

socket.volatile.emit("volatile event", "might or might not be sent");

使用 msgpack 解析器的官方打包包

¥Official bundle with the msgpack parser

现在将提供包含 socket.io-msgpack-parser 的打包包(在 CDN 上或由 /socket.io/socket.io.msgpack.min.js 的服务器提供服务)。

¥A bundle with the socket.io-msgpack-parser will now be provided (either on the CDN or served by the server at /socket.io/socket.io.msgpack.min.js).

优点:

¥Pros:

  • 具有二进制内容的事件作为 1 个 WebSocket 帧发送(而不是使用默认解析器发送 2 个以上)

    ¥events with binary content are sent as 1 WebSocket frame (instead of 2+ with the default parser)

  • 数字较多的有效负载应该较小

    ¥payloads with lots of numbers should be smaller

缺点:

¥Cons:

// server-side
const io = require("socket.io")(httpServer, {
parser: require("socket.io-msgpack-parser")
});

客户端无需额外配置。

¥No additional configuration is needed on the client-side.

杂项

¥Miscellaneous

Socket.IO 代码库已重写为 TypeScript

¥The Socket.IO codebase has been rewritten to TypeScript

这意味着不再需要 npm i -D @types/socket.io

¥Which means npm i -D @types/socket.io should not be needed anymore.

服务器:

¥Server:

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

客户端:

¥Client:

import { io } from "socket.io-client";

const socket = io("/");

socket.on("connect", () => {
console.log(`connect ${socket.id}`);
});

显然,纯 JavaScript 仍然得到完全支持。

¥Plain javascript is obviously still fully supported.

正式放弃对 IE8 和 Node.js 8 的支持

¥Support for IE8 and Node.js 8 is officially dropped

IE8 不再可在 Sauce Labs 平台上进行测试,并且需要极少数用户(如果有的话?)付出大量努力,因此我们将放弃对其的支持。

¥IE8 is no longer testable on the Sauce Labs platform, and requires a lot of efforts for very few users (if any?), so we are dropping support for it.

此外,Node.js 8 现在是 EOL。请尽快升级!

¥Besides, Node.js 8 is now EOL. Please upgrade as soon as possible!

如何升级现有生产部署

¥How to upgrade an existing production deployment

  • 首先,更新服务器,将 allowEIO3 设置为 true(在 socket.io@3.1.0 中添加)

    ¥first, update the servers with allowEIO3 set to true (added in 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 结合使用。请注意,两个版本都是兼容的,因此你可以逐一更新每个服务器(不需要大爆炸)。

¥Note: If you are using the Redis adapter to broadcast packets between nodes, you must use socket.io-redis@5 with socket.io@2 and socket.io-redis@6 with socket.io@3. Please note that both versions are compatible, so you can update each server one by one (no big bang is needed).

  • 然后,更新客户端

    ¥then, update the clients

此步骤实际上可能需要一些时间,因为某些客户端可能在缓存中仍然有 v2 客户端。

¥This step may actually take some time, as some clients may still have a v2 client in cache.

你可以通过以下方式检查连接的版本:

¥You can check the version of the connection with:

io.on("connection", (socket) => {
const version = socket.conn.protocol; // either 3 or 4
});

这与 HTTP 请求中的 EIO 查询参数的值匹配。

¥This matches the value of the EIO query parameter in the HTTP requests.

  • 最后,更新每个客户端后,将 allowEIO3 设置为 false(这是默认值)

    ¥and finally, once every client was updated, set allowEIO3 to false (which is the default value)

const io = require("socket.io")({
allowEIO3: false
});

allowEIO3 设置为 false 后,v2 客户端现在在连接时将收到 HTTP 400 错误 (Unsupported protocol version)。

¥With allowEIO3 set to false, v2 clients will now receive an HTTP 400 error (Unsupported protocol version) when connecting.

已知的迁移问题

¥Known migration issues

  • stream_1.pipeline is not a function
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 中引入的。

¥This error is probably due to your version of Node.js. The pipeline method was introduced in Node.js 10.0.0.

  • error TS2416: Property 'emit' in type 'Namespace' is not assignable to the same property in base type '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 (commit) 中已修复。

¥The signature of the emit() method was fixed in version 3.0.1 (commit).

  • 发送大负载(> 1MB)时客户端会断开连接

    ¥the client is disconnected when sending a big payload (> 1MB)

这可能是因为 maxHttpBufferSize 的默认值现在是 1MB。当服务器收到大于此大小的数据包时,将断开客户端的连接,以防止恶意客户端使服务器过载。

¥This is probably due to the fact that the default value of maxHttpBufferSize is now 1MB. When receiving a packet that is larger than this, the server disconnects the client, in order to prevent malicious clients from overloading the server.

你可以在创建服务器时调整该值:

¥You can adjust the value when creating the server:

const io = require("socket.io")(httpServer, {
maxHttpBufferSize: 1e8
});
  • Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at xxx/socket.io/?EIO=4&transport=polling&t=NMnp2WI. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).

从 Socket.IO v3 开始,你需要显式启用 跨域资源共享 (CORS)。该文档可以在 此处 中找到。

¥Since Socket.IO v3, you need to explicitly enable Cross-Origin Resource Sharing (CORS). The documentation can be found here.

  • Uncaught TypeError: packet.data is undefined

看来你正在使用 v3 客户端连接到 v2 服务器,这是不可能的。请参阅 以下部分

¥It seems that you are using a v3 client to connect to a v2 server, which is not possible. Please see the following section.

  • Object literal may only specify known properties, and 'extraHeaders' does not exist in type 'ConnectOpts'

由于代码库已被重写为 TypeScript(更多信息 此处),因此不再需要 @types/socket.io-client,并且实际上会与来自 socket.io-client 包的类型冲突。

¥Since the codebase has been rewritten to TypeScript (more information here), @types/socket.io-client is no longer needed and will actually conflict with the typings coming from the socket.io-client package.

  • 跨源上下文中缺少 cookie

    ¥missing cookie in a cross-origin context

如果前端不是由与后端相同的域提供服务,你现在需要显式启用 cookie:

¥You now need to explicitly enable cookies if the front is not served from the same domain as the backend:

服务器

¥Server

import { Server } from "socket.io";

const io = new Server({
cors: {
origin: ["https://front.domain.com"],
credentials: true
}
});

客户端

¥Client

import { io } from "socket.io-client";

const socket = io("https://backend.domain.com", {
withCredentials: true
});

参考:

¥Reference: