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

從 2.x 遷移到 3.0

此版本應修正 Socket.IO 程式庫的大部分不一致,並提供更直覺的行為給最終使用者。這是多年來社群回饋的成果。非常感謝所有參與的人員!

重點摘要: 由於有許多重大變更,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)。

這種行為雖然方便,但在安全性方面卻不是很好,因為這表示除非另行使用 origins 選項指定,否則所有網域都允許連線到您的 Socket.IO 伺服器。

因此,從 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
});

此外,我們現在會使用「main」命名空間,而非「預設」命名空間。

Namespace.connected 已重新命名為 Namespace.sockets,現在是 Map

connected 物件(用於儲存所有連線至指定 Namespace 的 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.IO v3 中,Socket 執行個體的 query 選項重新命名為 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資料封包-
重新連線嘗試重新連線嘗試重新連線嘗試 & 重新連線中
重新連線重新連線成功-
重新連線錯誤重新連線失敗-
重新連線失敗所有嘗試後重新連線失敗-

以下是 Socket 發出的事件更新清單

名稱說明先前 (如果不同)
連線與命名空間的連線成功-
connect_error連線失敗error
中斷連線中斷連線-

最後,以下是應用程式中無法使用的保留事件更新清單

  • 連線(在客戶端使用)
  • 連線錯誤(在客戶端使用)
  • 中斷連線(在兩端使用)
  • 中斷連線中(在伺服器端使用)
  • newListenerremoveListener(EventEmitter 保留事件
socket.emit("connect_error"); // will now throw an Error

Namespace.clients() 已重新命名為 Namespace.allSockets(),現在會傳回 Promise

此函式會傳回連線到此命名空間的 Socket 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 伺服器的 Socket 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 未連接時(預設情況下,封包會緩衝直到重新連線)。

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@2 搭配 socket.io-redis@5 使用,而將 socket.io@3 搭配 socket.io-redis@6 使用。請注意,這兩個版本都相容,因此您可以逐一更新每個伺服器 (不需要大規模變更)。

  • 然後,更新客戶端

這個步驟實際上可能需要一些時間,因為某些客戶端可能仍會在快取中保留 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
});

參考