Socket.IO プロトコル
このドキュメントでは、Socket.IO プロトコルの第5版について説明します。
このドキュメントのソースはこちらにあります。
目次
はじめに
Socket.IO プロトコルは、クライアントとサーバー間の全二重かつ低オーバーヘッドの通信を可能にします。
これは、WebSocket および HTTP ロングポーリングによる低レベルの配管処理を行うEngine.IO プロトコルの上に構築されています。
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` パケットを送信する必要があります。
サーバーは次のいずれかで応答する必要があります。
- 接続が成功した場合、ペイロードにセッション ID を含む `CONNECT` パケット
- または接続が許可されていない場合は `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` パケットとして送信されることに注意してください (詳細についてはこちらを参照)。したがって、エンコードされた結果は、(HTTP ロングポーリングでのリクエスト/レスポンス本文、または WebSocket フレームで) 送信されるときに文字 `"4"` が先頭に付加されます。
フォーマット
<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 の両方のプロトコルを組み合わせたときに、ネットワーク上で送信される内容の例を次に示します。
- リクエスト n°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` クエリパラメータは、リクエストがブラウザによってキャッシュされないようにするために使用されます。
- リクエスト n°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
- リクエスト n°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"}
- リクエスト n°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
- リクエスト n°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
- リクエスト n°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リクエストのクエリパラメータにあるもの)とは異なるものになります。
コミット: 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/
ディレクトリのテストスイートでは、サーバー実装のコンプライアンスを確認できます。
使用法
- 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);
});