Socket.IO 協定
本文件說明 Socket.IO 協定的第 5 版。
此文件的來源可在此處找到 這裡。
目錄
簡介
Socket.IO 協定啟用 全雙工,並在客戶端和伺服器之間進行低負載通訊。
它建立在 Engine.IO 協定 之上,它處理 WebSocket 和 HTTP 長輪詢的低階管道。
Socket.IO 協定新增下列功能
- 多工(在 Socket.IO 術語中稱為 「命名空間」)
使用 JavaScript API 的範例
伺服器
// declare the namespace
const namespace = io.of("/admin");
// handle the connection to the namespace
namespace.on("connection", (socket) => {
// ...
});
客戶端
// reach the main namespace
const socket1 = io();
// reach the "/admin" namespace (with the same underlying WebSocket connection)
const socket2 = io("/admin");
// handle the connection to the namespace
socket2.on("connect", () => {
// ...
});
- 封包確認
使用 JavaScript API 的範例
// on one side
socket.emit("hello", "foo", (arg) => {
console.log("received", arg);
});
// on the other side
socket.on("hello", (arg, ack) => {
ack("bar");
});
參考實作是用 TypeScript 撰寫的
交換協定
Socket.IO 封包包含下列欄位
- 封包類型(整數)
- 命名空間(字串)
- 選擇性地,酬載(物件 | 陣列)
- 選擇性地,確認 ID(整數)
以下是可用封包類型的清單
類型 | ID | 用法 |
---|---|---|
CONNECT | 0 | 在 連線到命名空間 時使用。 |
DISCONNECT | 1 | 在 中斷與命名空間的連線 時使用。 |
EVENT | 2 | 用於 傳送資料 到另一端。 |
ACK | 3 | 用於 確認 事件。 |
CONNECT_ERROR | 4 | 在 連線到命名空間 時使用。 |
BINARY_EVENT | 5 | 用於 傳送二進位資料 到另一端。 |
BINARY_ACK | 6 | 用於 確認 事件(回應包含二進位資料)。 |
連線到命名空間
在 Socket.IO 會話開始時,用戶端必須傳送 CONNECT
封包
伺服器必須以下列方式回應
- 如果連線成功,則傳送一個
CONNECT
封包,酬載中包含會話 ID - 如果連線不允許,則傳送一個
CONNECT_ERROR
封包
CLIENT SERVER
│ ───────────────────────────────────────────────────────► │
│ { type: CONNECT, namespace: "/" } │
│ ◄─────────────────────────────────────────────────────── │
│ { type: CONNECT, namespace: "/", data: { sid: "..." } } │
如果伺服器沒有先收到 CONNECT
封包,則必須立即關閉連線。
用戶端可以同時連線到多個命名空間,使用同一個底層 WebSocket 連線。
範例
- 使用主命名空間(名稱為
"/"
)
Client > { type: CONNECT, namespace: "/" }
Server > { type: CONNECT, namespace: "/", data: { sid: "wZX3oN0bSVIhsaknAAAI" } }
- 使用自訂命名空間
Client > { type: CONNECT, namespace: "/admin" }
Server > { type: CONNECT, namespace: "/admin", data: { sid: "oSO0OpakMV_3jnilAAAA" } }
- 附帶其他負載
Client > { type: CONNECT, namespace: "/admin", data: { "token": "123" } }
Server > { type: CONNECT, namespace: "/admin", data: { sid: "iLnRaVGHY4B75TeVAAAB" } }
- 在連線被拒絕的情況下
Client > { type: CONNECT, namespace: "/" }
Server > { type: CONNECT_ERROR, namespace: "/", data: { message: "Not authorized" } }
傳送和接收資料
一旦與名稱空間的連線建立,用戶端和伺服器便可以開始交換資料
CLIENT SERVER
│ ───────────────────────────────────────────────────────► │
│ { type: EVENT, namespace: "/", data: ["foo"] } │
│ │
│ ◄─────────────────────────────────────────────────────── │
│ { type: EVENT, namespace: "/", data: ["bar"] } │
負載是強制性的,且必須是非空的陣列。如果不是這樣,則接收器必須關閉連線。
範例
- 與主要名稱空間
Client > { type: EVENT, namespace: "/", data: ["foo"] }
- 使用自訂命名空間
Server > { type: EVENT, namespace: "/admin", data: ["bar"] }
- 與二進位資料
Client > { type: BINARY_EVENT, namespace: "/", data: ["baz", <Buffer <01 02 03 04>> ] }
確認
傳送者可以包含事件 ID,以便向接收者要求確認
CLIENT SERVER
│ ───────────────────────────────────────────────────────► │
│ { type: EVENT, namespace: "/", data: ["foo"], id: 12 } │
│ ◄─────────────────────────────────────────────────────── │
│ { type: ACK, namespace: "/", data: ["bar"], id: 12 } │
接收者必須使用具有相同事件 ID 的ACK
封包回應。
負載是強制性的,且必須是陣列(可能為空)。
範例
- 與主要名稱空間
Client > { type: EVENT, namespace: "/", data: ["foo"], id: 12 }
Server > { type: ACK, namespace: "/", data: [], id: 12 }
- 使用自訂命名空間
Server > { type: EVENT, namespace: "/admin", data: ["foo"], id: 13 }
Client > { type: ACK, namespace: "/admin", data: ["bar"], id: 13 }
- 與二進位資料
Client > { type: BINARY_EVENT, namespace: "/", data: ["foo", <buffer <01 02 03 04> ], id: 14 }
Server > { type: ACK, namespace: "/", data: ["bar"], id: 14 }
or
Server > { type: EVENT, namespace: "/", data: ["foo" ], id: 15 }
Client > { type: BINARY_ACK, namespace: "/", data: ["bar", <buffer <01 02 03 04>], id: 15 }
從名稱空間中斷開連線
在任何時候,一方都可以透過傳送DISCONNECT
封包來結束與名稱空間的連線
CLIENT SERVER
│ ───────────────────────────────────────────────────────► │
│ { type: DISCONNECT, namespace: "/" } │
預期另一方不會回應。如果用戶端已連線至另一個名稱空間,則低階層連線可能會保持連線狀態。
封包編碼
此區段詳細說明預設剖析器使用的編碼,該剖析器包含在 Socket.IO 伺服器和用戶端中,其原始碼可在此處找到這裡。
JavaScript 伺服器和用戶端實作也支援自訂剖析器,這些剖析器具有不同的權衡,並可能對某些類型的應用程式有益。請參閱socket.io-json-parser或socket.io-msgpack-parser作為範例。
請另外注意,每個 Socket.IO 封包會以 Engine.IO message
封包傳送(更多資訊在此處),因此編碼後的結果會在透過網路傳送時加上字元"4"
作為前綴(在使用 HTTP 長輪詢的請求/回應主體中,或在 WebSocket 框架中)。
格式
<packet type>[<# of binary attachments>-][<namespace>,][<acknowledgment id>][JSON-stringified payload without binary]
+ binary attachments extracted
注意:只有當命名空間與主命名空間(/
)不同時,才會包含命名空間
範例
連線至命名空間
- 與主要名稱空間
封包
{ type: CONNECT, namespace: "/" }
編碼
0
- 使用自訂命名空間
封包
{ type: CONNECT, namespace: "/admin", data: { sid: "oSO0OpakMV_3jnilAAAA" } }
編碼
0/admin,{"sid":"oSO0OpakMV_3jnilAAAA"}
- 在連線被拒絕的情況下
封包
{ type: CONNECT_ERROR, namespace: "/", data: { message: "Not authorized" } }
編碼
4{"message":"Not authorized"}
傳送和接收資料
- 與主要名稱空間
封包
{ type: EVENT, namespace: "/", data: ["foo"] }
編碼
2["foo"]
- 使用自訂命名空間
封包
{ type: EVENT, namespace: "/admin", data: ["bar"] }
編碼
2/admin,["bar"]
- 與二進位資料
封包
{ type: BINARY_EVENT, namespace: "/", data: ["baz", <Buffer <01 02 03 04>> ] }
編碼
51-["baz",{"_placeholder":true,"num":0}]
+ <Buffer <01 02 03 04>>
- 含多個附件
封包
{ type: BINARY_EVENT, namespace: "/admin", data: ["baz", <Buffer <01 02>>, <Buffer <03 04>> ] }
編碼
52-/admin,["baz",{"_placeholder":true,"num":0},{"_placeholder":true,"num":1}]
+ <Buffer <01 02>>
+ <Buffer <03 04>>
請記住,每個 Socket.IO 封包都包覆在 Engine.IO message
封包中,因此透過網路傳送時,會加上字元 "4"
作為前置字元。
範例:{ type: EVENT, namespace: "/", data: ["foo"] }
會傳送為 42["foo"]
確認
- 與主要名稱空間
封包
{ type: EVENT, namespace: "/", data: ["foo"], id: 12 }
編碼
212["foo"]
- 使用自訂命名空間
封包
{ type: ACK, namespace: "/admin", data: ["bar"], id: 13 }
編碼
3/admin,13["bar"]`
- 與二進位資料
封包
{ type: BINARY_ACK, namespace: "/", data: ["bar", <Buffer <01 02 03 04>>], id: 15 }
編碼
61-15["bar",{"_placeholder":true,"num":0}]
+ <Buffer <01 02 03 04>>
中斷與命名空間的連線
- 與主要名稱空間
封包
{ type: DISCONNECT, namespace: "/" }
編碼
1
- 使用自訂命名空間
{ type: DISCONNECT, namespace: "/admin" }
編碼
1/admin,
範例會話
以下是結合 Engine.IO 和 Socket.IO 協定時,透過網路傳送的範例。
- 要求編號 1(開啟封包)
GET /socket.io/?EIO=4&transport=polling&t=N8hyd6w
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=UTF-8
0{"sid":"lv_VI97HAXpY6yYWAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000,"maxPayload":1000000}
詳細資料
0 => Engine.IO "open" packet type
{"sid":... => the Engine.IO handshake data
注意:t
查詢參數用於確保瀏覽器不會快取要求。
- 要求編號 2(命名空間連線要求)
POST /socket.io/?EIO=4&transport=polling&t=N8hyd7H&sid=lv_VI97HAXpY6yYWAAAC
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=UTF-8
40
詳細資料
4 => Engine.IO "message" packet type
0 => Socket.IO "CONNECT" packet type
- 要求編號 3(命名空間連線核准)
GET /socket.io/?EIO=4&transport=polling&t=N8hyd7H&sid=lv_VI97HAXpY6yYWAAAC
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=UTF-8
40{"sid":"wZX3oN0bSVIhsaknAAAI"}
- 要求編號 4
在伺服器上執行 socket.emit('hey', 'Jude')
GET /socket.io/?EIO=4&transport=polling&t=N8hyd7H&sid=lv_VI97HAXpY6yYWAAAC
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=UTF-8
42["hey","Jude"]
詳細資料
4 => Engine.IO "message" packet type
2 => Socket.IO "EVENT" packet type
[...] => content
- 要求編號 5(傳送訊息)
在用戶端上執行 socket.emit('hello'); socket.emit('world');
POST /socket.io/?EIO=4&transport=polling&t=N8hzxke&sid=lv_VI97HAXpY6yYWAAAC
> Content-Type: text/plain; charset=UTF-8
42["hello"]\x1e42["world"]
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=UTF-8
ok
詳細資料
4 => Engine.IO "message" packet type
2 => Socket.IO "EVENT" packet type
["hello"] => the 1st content
\x1e => separator
4 => Engine.IO "message" packet type
2 => Socket.IO "EVENT" packet type
["world"] => the 2nd content
- 要求編號 6(WebSocket 升級)
GET /socket.io/?EIO=4&transport=websocket&sid=lv_VI97HAXpY6yYWAAAC
< HTTP/1.1 101 Switching Protocols
WebSocket 框架
< 2probe => Engine.IO probe request
> 3probe => Engine.IO probe response
> 5 => Engine.IO "upgrade" packet type
> 42["hello"]
> 42["world"]
> 40/admin, => request access to the admin namespace (Socket.IO "CONNECT" packet)
< 40/admin,{"sid":"-G5j-67EZFp-q59rADQM"} => grant access to the admin namespace
> 42/admin,1["tellme"] => Socket.IO "EVENT" packet with acknowledgement
< 461-/admin,1[{"_placeholder":true,"num":0}] => Socket.IO "BINARY_ACK" packet with a placeholder
< <binary> => the binary attachment (sent in the following frame)
... after a while without message
> 2 => Engine.IO "ping" packet type
< 3 => Engine.IO "pong" packet type
> 1 => Engine.IO "close" packet type
歷史
v5 和 v4 之間的差異
Socket.IO 協定的第 5 次修訂(目前版本)用於 Socket.IO v3 及以上版本(v3.0.0
於 2020 年 11 月發布)。
它建立在 Engine.IO 協定的第 4 次修訂 之上(因此有 EIO=4
查詢參數)。
變更清單
- 移除與預設名稱空間的隱含連線
在先前版本中,即使客戶端要求存取其他名稱空間,它仍會始終連線到預設名稱空間。
現在不再是這樣,客戶端必須在任何情況下都傳送 CONNECT
封包。
提交記錄:09b6f23(伺服器)和 249e0be(客戶端)
- 將
ERROR
重新命名為CONNECT_ERROR
意義和代碼號碼 (4) 未變更:當拒絕連線到名稱空間時,伺服器仍會使用此封包類型。但我們認為這個名稱更能自我描述。
提交記錄:d16c035(伺服器)和 13e1db7c(客戶端)。
CONNECT
封包現在可以包含酬載
客戶端可以傳送酬載以進行驗證/授權。範例
{
"type": 0,
"nsp": "/admin",
"data": {
"token": "123"
}
}
如果成功,伺服器會回應包含 Socket ID 的酬載。範例
{
"type": 0,
"nsp": "/admin",
"data": {
"sid": "CjdVH4TQvovi1VvgAC5Z"
}
}
此變更表示 Socket.IO 連線的 ID 現在會與底層 Engine.IO 連線的 ID 不同(HTTP 要求的查詢參數中找到的 ID)。
提交記錄:2875d2c(伺服器)和 bbe94ad(客戶端)
- 酬載
CONNECT_ERROR
封包現在是一個物件,而不是純文字字串
提交記錄:54bf4a4(伺服器)和 0939395(客戶端)
v4 和 v3 之間的差異
Socket.IO 協定的第 4 次修訂用於 Socket.IO v1(v1.0.3
於 2014 年 6 月發布)和 v2(v2.0.0
於 2017 年 5 月發布)。
修訂的詳細資訊可以在這裡找到:https://github.com/socketio/socket.io-protocol/tree/v4
它建立在 Engine.IO 協定的第 3 次修訂 之上(因此有 EIO=3
查詢參數)。
變更清單
- 新增
BINARY_ACK
封包類型
先前,ACK
封包總是視為可能包含二進位物件,並對此類物件進行遞迴搜尋,這可能會損害效能。
參考:https://github.com/socketio/socket.io-parser/commit/ca4f42a922ba7078e840b1bc09fe3ad618acc065
v3 與 v2 之差異
Socket.IO 協定的第 3 次修訂用於早期 Socket.IO v1 版本 (socket.io@1.0.0...1.0.2
)(於 2014 年 5 月發布)。
修訂的詳細資訊可在此處找到:https://github.com/socketio/socket.io-protocol/tree/v3
變更清單
- 移除使用 msgpack 編碼包含二進位物件的封包(另請參閱 299849b)
v2 與 v1 之差異
變更清單
- 新增
BINARY_EVENT
封包類型
這是為了增加對二進位物件的支援,在 Socket.IO 1.0 的開發過程中加入的。BINARY_EVENT
封包以 msgpack 編碼。
初始修訂
第一次修訂是 Engine.IO 協定(使用 WebSocket/HTTP 長輪詢、心跳的低階管道)與 Socket.IO 協定分開後的結果。它從未包含在 Socket.IO 版本中,但為後續的版本鋪路。
測試套件
test-suite/
目錄中的測試套件 https://github.com/socketio/socket.io-protocol/tree/main/test-suite 讓您可以檢查伺服器實作的相容性。
用法
- 在 Node.js 中:
npm ci && npm test
- 在瀏覽器中:只要在瀏覽器中開啟
index.html
檔案
供您參考,以下是 JavaScript 伺服器通過所有測試的預期設定
import { Server } from "socket.io";
const io = new Server(3000, {
pingInterval: 300,
pingTimeout: 200,
maxPayload: 1000000,
cors: {
origin: "*"
}
});
io.on("connection", (socket) => {
socket.emit("auth", socket.handshake.auth);
socket.on("message", (...args) => {
socket.emit.apply(socket, ["message-back", ...args]);
});
socket.on("message-with-ack", (...args) => {
const ack = args.pop();
ack(...args);
})
});
io.of("/custom").on("connection", (socket) => {
socket.emit("auth", socket.handshake.auth);
});