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

疑難排解連線問題

提示

透過 管理員介面 可以進一步了解 Socket.IO 部署的狀態。

常見/已知問題

其他常見問題

問題:Socket 無法連線

疑難排解步驟

在用戶端,connect_error 事件提供其他資訊

socket.on("connect_error", (err) => {
// the reason of the error, for example "xhr poll error"
console.log(err.message);

// some additional description, for example the status code of the initial HTTP response
console.log(err.description);

// some additional context, for example the XMLHttpRequest object
console.log(err.context);
});

在伺服器端,connection_error 事件也可能提供一些其他見解

io.engine.on("connection_error", (err) => {
console.log(err.req); // the request object
console.log(err.code); // the error code, for example 1
console.log(err.message); // the error message, for example "Session ID unknown"
console.log(err.context); // some additional error context
});

以下是可能的錯誤代碼清單

代碼訊息可能的解釋
0"傳輸不明"在正常情況下不應發生此情況。
1"Session ID 不明"通常,這表示未啟用黏著式會話(請參閱 下方)。
2"錯誤的握手方法"在正常情況下不應發生此情況。
3"錯誤的請求"通常,這表示伺服器前面的代理程式未正確轉送 WebSocket 標頭(請參閱 此處)。
4"禁止"連線被 allowRequest() 方法拒絕。
5"不支援的協定版本"用戶端版本與伺服器不相容(請參閱 此處)。

可能的解釋

您嘗試連線到純 WebSocket 伺服器

"Socket.IO 是什麼" 部分所述,Socket.IO 用戶端並非 WebSocket 實作,因此無法與 WebSocket 伺服器建立連線,即使使用 transports: ["websocket"] 也是如此

const socket = io("ws://echo.websocket.org", {
transports: ["websocket"]
});

伺服器無法連線

請確認 Socket.IO 伺服器實際上可以在指定的 URL 連線。你可以使用以下指令測試

curl "<the server URL>/socket.io/?EIO=4&transport=polling"

它應該會傳回類似這樣的內容

0{"sid":"Lbo5JLzTotvW3g2LAAAA","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":20000}

如果不是這樣,請檢查 Socket.IO 伺服器是否正在執行,以及是否有任何阻礙連線的因素。

註解

實作協定 v3 的 v1/v2 伺服器(因此有 EIO=3)會傳回類似這樣的內容

96:0{"sid":"ptzi_578ycUci8WLB9G1","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000}2:40

用戶端與伺服器版本不相容

維持向後相容性是我們的首要任務,但在某些特定情況下,我們必須在協定層級實作一些重大變更

  • 從 v1.x 到 v2.0.0(於 2017 年 5 月發布),以改善與非 JavaScript 用戶端的相容性(請參閱 此處
  • 從 v2.x 到 v3.0.0(於 2020 年 11 月發布),以一次修正協定中一些長久存在的問題(請參閱 此處
資訊

v4.0.0 包含 JavaScript 伺服器 API 中的一些重大變更。Socket.IO 協定本身並未更新,因此 v3 用戶端將能夠連線到 v4 伺服器,反之亦然(請參閱 此處)。

例如,使用 v1/v2 用戶端連線到 v3/v4 伺服器將產生以下回應

< HTTP/1.1 400 Bad Request
< Content-Type: application/json

{"code":5,"message":"Unsupported protocol version"}

以下是 JS 用戶端 的相容性表格

JS 用戶端版本Socket.IO 伺服器版本
1.x2.x3.x4.x
1.x
2.x11
3.x
4.x

[1] 是,使用 allowEIO3: true

以下是 Java 客户端 的相容性表格

Java 客户端版本Socket.IO 伺服器版本
2.x3.x4.x
1.x11
2.x

[1] 是,使用 allowEIO3: true

以下是 Swift 客户端 的相容性表格

Swift 客户端版本Socket.IO 伺服器版本
2.x3.x4.x
v15.x12
v16.x3

[1] 是,使用 allowEIO3: true(伺服器)和 .connectParams(["EIO": "3"])(客户端)

SocketManager(socketURL: URL(string:"http://localhost:8087/")!, config: [.connectParams(["EIO": "3"])])

[2] 是,allowEIO3: true(伺服器)

[3] 是,使用 .version(.two)(客户端)

SocketManager(socketURL: URL(string:"http://localhost:8087/")!, config: [.version(.two)])

伺服器未傳送必要的 CORS 標頭

如果您在主控台看到以下錯誤訊息

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at ...

這可能表示

請參閱此處的說明文件 here

您未啟用黏著式連線(在多伺服器設定中)

當擴充到多個 Socket.IO 伺服器時,您需要確保特定 Socket.IO 連線的所有要求都到達同一個 Socket.IO 伺服器。說明可在此處找到 here

未執行此操作將導致 HTTP 400 回應,其中包含代碼:{"code":1,"message":"Session ID unknown"}

請參閱此處的說明文件 here

要求路徑在兩側不匹配

預設情況下,客戶端會傳送(而伺服器會接收)HTTP 要求,其中包含 "/socket.io/" 要求路徑。

這可以使用 path 選項來控制

伺服器

import { Server } from "socket.io";

const io = new Server({
path: "/my-custom-path/"
});

io.listen(3000);

用戶端

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

const socket = io(SERVER_URL, {
path: "/my-custom-path/"
});

在這種情況下,HTTP 要求看起來會像 <SERVER_URL>/my-custom-path/?EIO=4&transport=polling[&...]

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

const socket = io("/my-custom-path/");

表示客戶端將嘗試連接到命名為 "/my-custom-path/" 的命名空間,但要求路徑仍將是 "/socket.io/"。

問題:Socket 斷線

疑難排解步驟

首先,請注意,即使在穩定的網路連線中,斷線是很常見且預期的

  • 使用者與 Socket.IO 伺服器之間的任何事物都可能遇到暫時故障或重新啟動
  • 伺服器本身可能會在自動擴充政策中被終止
  • 在行動瀏覽器的案例中,使用者可能會失去連線或從 WiFi 切換到 4G
  • 瀏覽器本身可能會凍結不活動的分頁

話雖如此,除非特別告知否則,Socket.IO 伺服器將始終嘗試重新連線。

disconnect 事件提供額外資訊

socket.on("disconnect", (reason, details) => {
// the reason of the disconnection, for example "transport error"
console.log(reason);

// the low-level reason of the disconnection, for example "xhr post error"
console.log(details.message);

// some additional description, for example the status code of the HTTP response
console.log(details.description);

// some additional context, for example the XMLHttpRequest object
console.log(details.context);
});

可能的理由列於此處

可能的解釋

伺服器和客戶端之間的某個事物關閉連線

如果斷線發生在規律的間隔中,這可能表示伺服器和客戶端之間的某個事物未正確設定,並關閉連線

  • nginx

nginx 的 proxy_read_timeout 值(預設為 60 秒)必須大於 Socket.IO 的 pingInterval + pingTimeout(預設為 45 秒),否則如果在給定的延遲後未傳送資料,它將強制關閉連線,而客戶端將收到「傳輸關閉」錯誤。

  • Apache HTTP 伺服器

httpd 的 ProxyTimeout 值(預設為 60 秒)必須大於 Socket.IO 的 pingInterval + pingTimeout(預設為 45 秒),否則如果在給定的延遲後未傳送資料,它將強制關閉連線,而客戶端將收到「傳輸關閉」錯誤。

瀏覽器標籤已最小化,心跳已失敗

當瀏覽器標籤不在焦點時,某些瀏覽器(例如 Chrome)會限制 JavaScript 計時器,這可能會導致 ping 超時而中斷連線在 Socket.IO v2 中,因為心跳機制依賴於客戶端上的 setTimeout 函式。

作為解決方法,您可以在伺服器端增加 pingTimeout

const io = new Server({
pingTimeout: 60000
});

請注意,升級到 Socket.IO v4(至少 socket.io-client@4.1.3,由於 )應可防止此類問題,因為心跳機制已反轉(伺服器現在會傳送 PING 封包)。

客戶端與伺服器版本不相容

由於在 v2 和 v3/v4 中透過 WebSocket 傳輸傳送的封包格式相似,您可能可以使用不相容的客戶端進行連線(請參閱上方),但連線最終會在經過一段延遲後關閉。

因此,如果您在 30 秒後遇到定期中斷(這是 Socket.IO v2 中 pingTimeoutpingInterval 值的總和),這肯定是由於版本不相容。

您正在嘗試傳送龐大酬載

如果您在傳送龐大酬載時中斷連線,這可能表示您已達到 maxHttpBufferSize 值,其預設值為 1 MB。請根據您的需求調整它

const io = require("socket.io")(httpServer, {
maxHttpBufferSize: 1e8
});

pingTimeout 選項值花費更多時間上傳的龐大酬載也會觸發中斷(因為 心跳機制 在上傳期間會失敗)。請根據您的需求調整它

const io = require("socket.io")(httpServer, {
pingTimeout: 60000
});

問題:Socket 停留在 HTTP 長輪詢

疑難排解步驟

在大多數情況下,您應該看到類似的內容

Network monitor upon success

  1. Engine.IO 握手(包含會在後續要求中使用的會話 ID,在此為 zBjrh...AAAK
  2. Socket.IO 握手要求(包含 auth 選項的值)
  3. Socket.IO 握手回應(包含 Socket#id
  4. WebSocket 連線
  5. 第一個 HTTP 長輪詢要求,在建立 WebSocket 連線後會關閉

如果您沒有看到第 4 個要求的 HTTP 101 切換協定 回應,表示伺服器和您的瀏覽器之間有某項內容阻擋了 WebSocket 連線。

請注意,這不一定是阻擋,因為連線仍然透過 HTTP 長輪詢建立,但效率較差。

您可以透過以下方式取得目前傳輸的名稱

客戶端

socket.on("connect", () => {
const transport = socket.io.engine.transport.name; // in most cases, "polling"

socket.io.engine.on("upgrade", () => {
const upgradedTransport = socket.io.engine.transport.name; // in most cases, "websocket"
});
});

伺服器端

io.on("connection", (socket) => {
const transport = socket.conn.transport.name; // in most cases, "polling"

socket.conn.on("upgrade", () => {
const upgradedTransport = socket.conn.transport.name; // in most cases, "websocket"
});
});

可能的說明

伺服器前的代理程式不接受 WebSocket 連線

如果 nginx 或 Apache HTTPD 等代理程式未正確設定為接受 WebSocket 連線,您可能會收到 TRANSPORT_MISMATCH 錯誤

io.engine.on("connection_error", (err) => {
console.log(err.code); // 3
console.log(err.message); // "Bad request"
console.log(err.context); // { name: 'TRANSPORT_MISMATCH', transport: 'websocket', previousTransport: 'polling' }
});

表示 Socket.IO 伺服器未收到必要的 Connection: upgrade 標頭(您可以檢查 err.req.headers 物件)。

請參閱此處的說明文件。

express-status-monitor 執行自己的 socket.io 執行個體

請參閱此處的解決方案。

其他常見問題

重複事件註冊

在客戶端,每次 socket 重新連線時都會發出 connect 事件,因此必須在 connect 事件偵聽器之外註冊事件偵聽器

不佳 ⚠️

socket.on("connect", () => {
socket.on("foo", () => {
// ...
});
});

良好 👍

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

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

如果不是這樣,您的事件監聽器可能會被呼叫多次。

延遲事件處理器註冊

不佳 ⚠️

io.on("connection", async (socket) => {
await longRunningOperation();

// WARNING! Some packets might be received by the server but without handler
socket.on("hello", () => {
// ...
});
});

良好 👍

io.on("connection", async (socket) => {
socket.on("hello", () => {
// ...
});

await longRunningOperation();
});

socket.id 屬性的用法

請注意,除非已啟用連線狀態復原,否則 id 屬性為臨時性 ID,不應在您的應用程式中使用(或僅用於除錯目的),因為

  • 此 ID 會在每次重新連線後重新產生(例如當 WebSocket 連線中斷,或當使用者重新整理頁面時)
  • 兩個不同的瀏覽器分頁將會有兩個不同的 ID
  • 伺服器上沒有為特定 ID 儲存訊息佇列(亦即如果用戶端斷線,伺服器傳送至該 ID 的訊息將會遺失)

請改用一般會話 ID(透過 Cookie 傳送,或儲存在 localStorage 中並在auth payload 中傳送)。

另請參閱

在無伺服器平台上部署

由於大多數無伺服器平台(例如 Vercel)會根據請求處理器的持續時間計費,因此不建議與 Socket.IO(或純粹的 WebSocket)維持長時間連線。

參考資料