從 2.x 遷移到 3.0
此版本應修正 Socket.IO 程式庫的大部分不一致,並提供更直覺的行為給最終使用者。這是多年來社群回饋的成果。非常感謝所有參與的人員!
重點摘要: 由於有許多重大變更,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)。
這種行為雖然方便,但在安全性方面卻不是很好,因為這表示除非另行使用 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
}
});
預設不再使用 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
});
此外,我們現在會使用「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 | 中斷連線 | - |
ping | ping 封包 | - |
packet | 資料封包 | - |
重新連線嘗試 | 重新連線嘗試 | 重新連線嘗試 & 重新連線中 |
重新連線 | 重新連線成功 | - |
重新連線錯誤 | 重新連線失敗 | - |
重新連線失敗 | 所有嘗試後重新連線失敗 | - |
以下是 Socket 發出的事件更新清單
名稱 | 說明 | 先前 (如果不同) |
---|---|---|
連線 | 與命名空間的連線成功 | - |
connect_error | 連線失敗 | error |
中斷連線 | 中斷連線 | - |
最後,以下是應用程式中無法使用的保留事件更新清單
連線
(在客戶端使用)連線錯誤
(在客戶端使用)中斷連線
(在兩端使用)中斷連線中
(在伺服器端使用)newListener
和removeListener
(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.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 未連接時(預設情況下,封包會緩衝直到重新連線)。
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.dev.org.tw/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@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
});
參考