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

Socket.IO 協定

本文件說明 Socket.IO 協定的第 5 版。

此文件的來源可在此處找到 這裡

目錄

簡介

Socket.IO 協定啟用 全雙工,並在客戶端和伺服器之間進行低負載通訊。

它建立在 Engine.IO 協定 之上,它處理 WebSocket 和 HTTP 長輪詢的低階管道。

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用法
CONNECT0連線到命名空間 時使用。
DISCONNECT1中斷與命名空間的連線 時使用。
EVENT2用於 傳送資料 到另一端。
ACK3用於 確認 事件。
CONNECT_ERROR4連線到命名空間 時使用。
BINARY_EVENT5用於 傳送二進位資料 到另一端。
BINARY_ACK6用於 確認 事件(回應包含二進位資料)。

連線到命名空間

在 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-parsersocket.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);
});