Socket.IO 4.8
概觀
此技能包含了 Socket.IO 4.8 中容易出錯的規則:使用短暫的 socket.id 作為使用者鍵值、監聽器重複註冊、確認回呼因 timeout() 而改變形狀、無法保護 WebSocket 的 CORS、負載平衡器後遺忘的黏性連線,以及 v2 或 v3 中不再運作的 API。每項說明都附有頁面 URL。參考檔案按領域儲存了詳細資訊。
涵蓋的版本
Socket.IO 4.8(伺服端 socket.io,客戶端 socket.io-client),文件 v4。JS 客戶端 3.x 與 4.x 可與伺服端 3.x 與 4.x 通訊。https://socket.io/docs/v4/client-installation/#version-compatibility
快速開始
- 將 Socket.IO 附加到您建立的 HTTP 伺服器,並對其呼叫
listen,切勿對 Express 的app呼叫:app.listen()會建立另一個 Socket.IO 未附加的 HTTP 伺服器。https://socket.io/docs/v4/server-initialization/#with-express
import express from "express";
import { createServer } from "node:http";
import { Server } from "socket.io";
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, { cors: { origin: ["https://example.com"] } });
io.on("connection", (socket) => {
socket.on("hello", (arg, callback) => callback("got it"));
});
httpServer.listen(3000);
https://socket.io/docs/v4/server-initialization/#with-express
- 客戶端:相同來源使用
io(),跨來源使用io("https://server-domain.com")(伺服端必須啟用 CORS)。使用import { io } from "socket.io-client"匯入。https://socket.io/docs/v4/client-initialization/#from-a-different-domain
import { io } from "socket.io-client";
const socket = io("https://server-domain.com", { auth: { token: "abc" } });
const answer = await socket.timeout(5000).emitWithAck("hello", "world");
https://socket.io/docs/v4/client-api/#socketemitwithackeventname-args
- 型別定義已隨
socket.io-client提供。請勿安裝@types/socket.io-client,它會與內建的型別定義衝突。https://socket.io/docs/v4/client-installation/#from-npm
規則與陷阱
連線與身份
- Socket.IO 並非 WebSocket 實作:純 WebSocket 客戶端無法連接 Socket.IO 伺服器,且 Socket.IO 客戶端也無法連接純 WebSocket 伺服器。https://socket.io/docs/v4/#what-socketio-is-not
- 除非啟用連線狀態恢復,否則
socket.id會在每次重新連線與頁面重新整理時改變,每個分頁的值都不同,且沒有訊息佇列。請使用您自己的 session ID(透過 cookie 或auth傳送)來識別使用者,並透過以使用者 ID 命名的房間來觸及使用者的每個分頁。https://socket.io/docs/v4/server-socket-instance/#socketid
io.on("connection", async (socket) => {
const userId = await computeUserIdFromHeaders(socket.handshake.headers);
socket.join(userId);
io.to(userId).emit("hi");
});
https://socket.io/docs/v4/rooms/#sample-use-cases
- 在連線中介軟體中使用
socket.handshake.auth進行驗證。請在每個程式碼路徑中都呼叫next(),否則連線將一直懸掛,直到逾時後被關閉。next(err)會以connect_error的形式到達客戶端,err.data則提供詳細資訊。https://socket.io/docs/v4/middlewares/
io.use((socket, next) => {
if (isValid(socket.handshake.auth.token)) return next();
const err = new Error("not authorized");
err.data = { content: "Please retry later" };
next(err);
});
https://socket.io/docs/v4/middlewares/#handling-middleware-error
- 被中介軟體拒絕的連線不會重試。在
connect_error中,socket.active === false表示連線已被伺服器拒絕,客戶端必須自行呼叫socket.connect()來重新連線。https://socket.io/docs/v4/client-socket-instance/#connect_error - 請在頂層註冊客戶端監聽器,切勿在
connect處理函式內部註冊:connect會在每次重新連線時再次觸發,並每次添加重複的監聽器。https://socket.io/docs/v4/client-socket-instance/#connect - 在伺服端,請在
connection處理函式中同步註冊socket.on(...),在任何await之前完成,否則較早到達的封包將無法被處理。https://socket.io/docs/v4/troubleshooting-connection-issues/#delayed-event-handler-registration - Express 中介軟體應使用
io.engine.use(...),而非io.use(...)。https://socket.io/docs/v4/middlewares/#compatibility-with-express-middleware
事件與確認
- 請直接傳遞物件。切勿對負載進行
JSON.stringify()。Date會以字串形式送達,Map和Set必須手動轉換。https://socket.io/docs/v4/emitting-events/#basic-emit - 確認回呼是最後一個參數。未使用
timeout()時,它接收(response);使用timeout(ms)時,它接收(err, response)。https://socket.io/docs/v4/emitting-events/#with-timeout - 建議在
try區塊中使用await socket.timeout(ms).emitWithAck(...):它會在逾時時拒絕。接收端以 1 個參數呼叫其回呼。https://socket.io/docs/v4/client-api/#socketemitwithackeventname-args - 帶有確認的廣播需要
timeout(),並為每個客戶端返回 1 個回應。https://socket.io/docs/v4/server-api/#serveremiteventname-args
const responses = await io.timeout(10000).emitWithAck("some-event");
https://socket.io/docs/v4/server-api/#serveremitwithackeventname-args
- 監聽器沒有內建的錯誤處理與驗證。請在每個處理函式中捕獲錯誤,使用 schema 函式庫驗證負載,並在使用回呼前檢查
typeof callback === "function"。https://socket.io/docs/v4/listening-to-events/#validation - 切勿使用
connect、connect_error、disconnect、disconnecting、newListener或removeListener作為應用程式事件:發送保留名稱(例如socket.emit("disconnecting"))會拋出錯誤。https://socket.io/docs/v4/emit-cheatsheet/
廣播與房間
socket.to(room)和socket.broadcast會排除發送者,而io.to(room)則不會排除。socket.to(socket.id).emit()不會傳送任何資料:請改用io.to(socket.id).emit()。https://socket.io/docs/v4/server-api/#sockettoroom- 鏈式呼叫的
to()是聯集,每個 socket 只會收到一次事件。https://socket.io/docs/v4/rooms/#joining-and-leaving - 每個廣播修飾詞都會返回一個新的不可變運算子:請將其儲存在變數中,而不是先呼叫
io.to()然後再呼叫io.emit()。https://socket.io/docs/v4/migrating-from-3-x-to-4-0/#ioto-is-now-immutable - 房間名稱不會被強制轉型:
socket.join(42)不會匹配io.to("42")。https://socket.io/docs/v4/migrating-from-2-x-to-3-0/#room-names-are-not-coerced-to-string-anymore - 房間會在斷線時自動離開。請在
disconnecting事件中讀取房間,不要在disconnect中讀取,因為此時socket.rooms是空的。https://socket.io/docs/v4/rooms/#disconnection - 使用
io.of("/").adapter讀取適配器:文件警告說io.adapter()在此情況下無法運作,因為它是設定器。https://socket.io/docs/v4/adapter/#api
傳遞
- 預設保證是「至多一次」,伺服器不儲存任何訊息:斷線的客戶端將永遠錯過廣播。請使用資料庫根據
auth中傳送的偏移量進行重播,或啟用連線狀態恢復。https://socket.io/docs/v4/delivery-guarantees#at-most-once - 客戶端在離線時會緩衝發送的事件,並在重新連線時刷新。請檢查
socket.connected或對於可丟棄的資料使用socket.volatile.emit(...)。https://socket.io/docs/v4/client-offline-behavior/#buffered-events - 客戶端到伺服器的「至少一次」:
io({ retries: 3, ackTimeout: 10000 })。要實現「恰好一次」,請在唯一約束下儲存客戶端產生的 ID。在成功時確認,並在遇到重複時也確認,但不要在遇到任何其他錯誤時確認,這樣客戶端會因為逾時而重試。https://socket.io/docs/v4/tutorial/step-8 - 連線狀態恢復是選擇加入的,需要一個有界線的
maxDisconnectionDuration,並且可能失敗:請根據socket.recovered進行分支處理。Redis(Pub/Sub)適配器不支援此功能,但 Redis Streams 適配器支援。https://socket.io/docs/v4/connection-state-recovery
const io = new Server(httpServer, {
connectionStateRecovery: { maxDisconnectionDuration: 2 * 60 * 1000, skipMiddlewares: false },
});
https://socket.io/docs/v4/connection-state-recovery#usage
CORS 與部署
- CORS 預設為關閉。選項是
cors.origin,絕不是origins。origin: "*"搭配credentials: true在瀏覽器中會失敗:請列出明確的來源。https://socket.io/docs/v4/server-options/#cors - CORS 僅涵蓋 HTTP long-polling 模式下的瀏覽器,不涵蓋 WebSocket。請使用
allowRequest來限制誰可以連線。https://socket.io/docs/v4/handling-cors/ - 代理伺服器必須轉發
Upgrade和Connection標頭,且其讀取逾時必須大於pingInterval + pingTimeout(預設為 45 秒),否則客戶端會看到transport close。https://socket.io/docs/v4/reverse-proxy/#nginx - 在使用多台伺服器且啟用 HTTP long-polling 的情況下,無論使用何種適配器,黏性連線仍然是必要的(替代方案是停用 long-polling 或進行客戶端負載平衡)。https://socket.io/docs/v4/using-multiple-nodes/ 和 https://socket.io/docs/v4/adapter/
- 若無黏性連線,客戶端會收到 HTTP 400 回應,內容為
{"code":1,"message":"Session ID unknown"}。https://socket.io/docs/v4/troubleshooting-connection-issues/#you-didnt-enable-sticky-sessions-in-a-multi-server-setup path(預設為/socket.io/)必須在兩端匹配。io("/my-path/")選擇的是命名空間,而非路徑。https://socket.io/docs/v4/troubleshooting-connection-issues/#the-request-path-does-not-match-on-both-sides- 使用
io.close()關閉,這也會關閉 HTTP 伺服器。https://socket.io/docs/v4/server-api/#serverclosecallback
參考檔案
| 檔案 | 閱讀目的 |
|---|---|
| references/server-setup.md | 安裝、與 Express、Koa、Fastify、Hono、HTTPS、HTTP/2、Bun、uWebSockets.js 的初始設定、伺服器選項與預設值、打包、處理函式佈局 |
| references/middleware-and-auth.md | 連線、socket 與引擎中介軟體、憑證、使用者身份、標頭鉤子、allowRequest、客戶端憑證 |
| references/cors-and-proxies.md | CORS 選項與錯誤、cookies、nginx、Apache、Caddy、http-proxy、雲端負載平衡器、與 ws 共用連接埠 |
| references/server-api.md | Server 與 Socket 實例、io 上的命名空間、工具方法、serverSideEmit、斷線原因、引擎事件與錯誤碼、關閉 |
| references/client.md | 套件、初始化、Socket 事件、connect_error 與 disconnect、Manager 與多路複用、所有客戶端選項、打包工具 |
| references/events-rooms-namespaces.md | 序列化、確認、捕獲所有事件的監聽器、驗證、錯誤處理、廣播、房間、動態命名空間 |
| references/delivery-and-recovery.md | 傳遞保證、重試、恰好一次、偏移量重播、連線狀態恢復 |
| references/multiple-nodes.md | 黏性連線、多節點的代理伺服器設定、替代方案、Node.js 叢集、PM2 |
| references/adapters.md | Redis、分片 Redis、Redis Streams、Postgres、MongoDB、叢集、AWS SQS、Azure、GCP 適配器、功能矩陣、發射器 |
| references/performance-and-operations.md | 原生附加元件、wsEngine、記憶體、自訂解析器、作業系統限制、負載測試、Admin UI、除錯日誌 |
| references/typescript.md | 伺服端、客戶端與命名空間上的事件介面、SocketData、型別化測試 |
| references/testing-and-troubleshooting.md | 伺服器整合測試、使用虛擬 socket 的客戶端測試、連線失敗、斷線、卡住的 long-polling |
| references/migrations-and-protocol.md | v2 到 v3 與 v3 到 v4 的變更、使用 allowEIO3 的滾動升級、Engine.IO 與 Socket.IO 有線協定 |
舊有模式
| 舊用法 | 替代方案 | 來源 |
|---|---|---|
origins 或 handlePreflightRequest 選項 | cors: { origin: [...] } | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/#cors-handling |
io.set("authorization", fn) | io.use((socket, next) => ...) | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/#ioset-is-removed |
Socket query 選項中的 Token | auth 選項,讀取為 socket.handshake.auth | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
客戶端拒絕連線時的 error 事件 | connect_error,包含 err.message 與 err.data | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
Socket 上的 socket.on("reconnect_attempt") | Manager 上的 socket.io.on("reconnect_attempt") | https://socket.io/docs/v4/client-socket-instance/#events |
客戶端用於延遲的 pong 事件 | 在間隔中使用 socket.volatile.emit("ping", cb) | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/#no-more-pong-event-for-retrieving-latency |
Namespace.connected 物件 | Namespace.sockets,一個 Map | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
Object.keys(socket.rooms) | socket.rooms 是一個 Set | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
socket.join(room, cb) | socket.join(room),使用非同步適配器搭配 await | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
Namespace.clients(cb)(v2),然後 allSockets()(v3,將在下一個主要版本中移除) | fetchSockets() | https://socket.io/docs/v4/server-api/#namespaceallsockets |
socket.binary(false) | 自訂 parser | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
socket.emit("a").emit("b") | 分開呼叫,emit() 返回 true | https://socket.io/docs/v4/migrating-from-2-x-to-3-0/ |
socket.io 或 socket.io-client 的預設匯入 | 命名匯入 import { Server } from "socket.io" 或 import { io } from "socket.io-client" | https://socket.io/docs/v4/migrating-from-3-x-to-4-0/#syntax-changes |