Engine.IO プロトコル
このドキュメントは、Engine.IO プロトコルのバージョン 4 について説明します。
このドキュメントのソースは、こちらを参照してください。
目次
はじめに
Engine.IO プロトコルにより、クライアントとサーバー間の全二重でオーバーヘッドの少ない通信が可能になります。
WebSocket プロトコルをベースにしており、WebSocket 接続を確立できない場合はフォールバックとして HTTP ロングポーリングを使用します。
リファレンス実装は TypeScript で記述されています。
Socket.IO プロトコルは、これらの基盤の上に構築されており、Engine.IO プロトコルにより提供される通信チャネルの上に追加の機能をもたらします。
トランスポート
Engine.IO クライアントと Engine.IO サーバー間の接続は、以下の方法で確立できます。
HTTP ロングポーリング
HTTP ロングポーリング(単に「ポーリング」と称する場合もあります)のトランスポートは、連続する HTTP リクエストで構成されます。
- サーバーからデータを受信するための長期間実行される GET リクエスト
- 送信する短時間のPOSTリクエスト、データをサーバーに送信するために
リクエストパス
HTTPリクエストのパスはデフォルトで/engine.io/。
プロトコル上に構築されたライブラリによって更新される可能性があります(たとえば、Socket.IOプロトコルは/socket.io/を使用します)。
クエリパラメータ
次のクエリパラメータが使用されます
| 名前 | 値 | 説明 | 
|---|---|---|
| EIO | 4 | 必須、プロトコルのバージョン。 | 
| transport | polling | 必須、トランスポートの名前。 | 
| sid | <sid> | セッションが確立されると必須、セッションID。 | 
必須のクエリパラメータがない場合は、サーバーはHTTP 400エラーステータスで応答する必要があります。
ヘッダー
バイナリデータを送信する場合、送信者(クライアントまたはサーバー)はContent-Type: application/octet-streamヘッダーを含める必要があります。
明示的なContent-Typeヘッダーがない場合、受信者はデータがプレーンテキストであると推測する必要があります。
リファレンス:https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
データの送受信
データを送信する
一部のパケットを送信するには、クライアントはリクエストボディにパケットがエンコードされたHTTPPOSTリクエストを作成する必要があります
CLIENT                                                 SERVER
  │                                                      │
  │   POST /engine.io/?EIO=4&transport=polling&sid=...   │
  │ ───────────────────────────────────────────────────► │
  │ ◄──────────────────────────────────────────────────┘ │
  │                        HTTP 200                      │
  │                                                      │
セッションID(sidクエリパラメータから)が不明な場合、サーバーはHTTP 400応答を返す必要があります。
正常性を示すため、サーバーはHTTP 200応答を返し、応答ボディにok文字列を含める必要があります。
パケットの順序付けを確保するため、クライアントはアクティブなPOSTリクエストを1つ以上持つことはできません。発生した場合、サーバーはHTTP 400エラーステータスを返し、セッションを閉じます。
データを受信する
一部のパケットを受信するには、クライアントはHTTPGETリクエストを作成する必要があります
CLIENT                                                SERVER
  │   GET /engine.io/?EIO=4&transport=polling&sid=...   │
  │ ──────────────────────────────────────────────────► │
  │                                                   . │
  │                                                   . │
  │                                                   . │
  │                                                   . │
  │ ◄─────────────────────────────────────────────────┘ │
  │                       HTTP 200                      │
セッションID(sidクエリパラメータから)が不明な場合、サーバーはHTTP 400応答を返す必要があります。
特定のセッションにバッファリングされたパケットがない場合、サーバーはすぐに応答しない場合があります。送信するパケットがある場合、サーバーはそれらをエンコード(パケットエンコーディングを参照)し、HTTPリクエストの応答ボディで送信する必要があります。
パケットの順序付けを確保するため、クライアントはアクティブなGETリクエストを1つ以上持つことはできません。発生した場合、サーバーはHTTP 400エラーステータスを返し、セッションを閉じます。
WebSocket
WebSocketトランスポートは、WebSocket接続で構成され、サーバーとクライアント間の双方向で低遅延の通信チャネルを提供します。
次のクエリパラメータが使用されます
| 名前 | 値 | 説明 | 
|---|---|---|
| EIO | 4 | 必須、プロトコルのバージョン。 | 
| transport | websocket | 必須、トランスポートの名前。 | 
| sid | <sid> | HTTPロングポーリングからのアップグレードかどうかによってオプション。 | 
必須のクエリパラメータがない場合は、サーバーはWebSocket接続を閉じなければなりません。
各パケット(読み取りまたは書き込み)は独自に送信されますWebSocketフレーム。
クライアントはセッションごとに1つ以上のWebSocket接続を開くことはできません。発生した場合、サーバーはWebSocket接続を閉じる必要があります。
プロトコル
Engine.IOパケットは次の内容で構成されます
- パケットタイプ
- オプションのパケットペイロード
使用可能なパケットタイプを次に示します
| タイプ | ID | 使用方法 | 
|---|---|---|
| オープン | 0 | ハンドシェイクで使用される。 | 
| 閉じる | 1 | トランスポートを閉じることができることを示すために使用される。 | 
| ピング | 2 | ハートビートメカニズムで使用される。 | 
| ポング | 3 | ハートビートメカニズムで使用される。 | 
| メッセージ | 4 | もう一方にペイロードを送信するために使用される。 | 
| アップグレード | 5 | アップグレードプロセスで使用される。 | 
| ヌープ | 6 | アップグレードプロセスで使用される。 | 
ハンドシェイク
接続を確立するには、クライアントはサーバーにHTTP GETリクエストを送信する必要があります。
- 最初にHTTPロングポーリング(デフォルト)
CLIENT                                                    SERVER
  │                                                          │
  │        GET /engine.io/?EIO=4&transport=polling           │
  │ ───────────────────────────────────────────────────────► │
  │ ◄──────────────────────────────────────────────────────┘ │
  │                        HTTP 200                          │
  │                                                          │
- Webソケットのみのセッション
CLIENT                                                    SERVER
  │                                                          │
  │        GET /engine.io/?EIO=4&transport=websocket         │
  │ ───────────────────────────────────────────────────────► │
  │ ◄──────────────────────────────────────────────────────┘ │
  │                        HTTP 101                          │
  │                                                          │
サーバーが接続を受け付けると、次のJSONエンコードペイロードを含むopenパケットで必ず応答する必要があります。
| キー | タイプ | 説明 | 
|---|---|---|
| sid | 文字列 | セッションID。 | 
| アップグレード | 文字列配列 | 使用可能なトランスポートアップグレードのリスト。 | 
| pingInterval | 数値 | pingの間隔は、ハートビートメカニズムで使用されます(ミリ秒単位)。 | 
| pingTimeout | 数値 | pingのタイムアウトは、ハートビートメカニズムで使用されます(ミリ秒単位)。 | 
| maxPayload | 数値 | チャンクあたりの最大バイト数。クライアントがパケットをペイロードに集約するために使用します。 | 
例
{
  "sid": "lv_VI97HAXpY6yYWAAAC",
  "upgrades": ["websocket"],
  "pingInterval": 25000,
  "pingTimeout": 20000,
  "maxPayload": 1000000
}
クライアントは、後続のすべてのリクエストのクエリパラメータにsidの値を送信する必要があります。
ハートビート
ハンドシェイクが完了すると、接続のライブネスを確認するためのハートビートメカニズムが開始されます。
CLIENT                                                 SERVER
  │                   *** Handshake ***                  │
  │                                                      │
  │  ◄─────────────────────────────────────────────────  │
  │                           2                          │  (ping packet)
  │  ─────────────────────────────────────────────────►  │
  │                           3                          │  (pong packet)
一定の間隔(ハンドシェイクで送信されたpingIntervalの値)で、サーバーはpingパケットを送信し、クライアントには数秒(pingTimeoutの値)pongパケットを送信する猶予があります。
サーバーがpongパケットを受信しない場合、接続が閉じられたものとみなす必要があります。
逆に、クライアントがpingInterval + pingTimeout以内にpingパケットを受信しない場合、接続が閉じられたものとみなす必要があります。
アップグレード
デフォルトでは、クライアントはHTTPロングポーリング接続を作成し、利用可能な場合はより優れたトランスポートにアップグレードする必要があります。
Webソケットにアップグレードするには、クライアントは次のことを行う必要があります。
- HTTPロングポーリングトランスポートを一時停止します(これ以上HTTPリクエストが送信されないので、パケットが失われないようにします)。
- 同じセッションIDでWebソケット接続を開きます。
- ペイロードに文字列probeを含むpingパケットを送信します。
サーバーは次の操作を実行する必要があります。
- 保留中のGETリクエスト(該当する場合)にnoopパケットを送信して、HTTPロングポーリングトランスポートをクリーンに閉じます。
- ペイロードに文字列probeを含むpongパケットで応答します。
最後に、クライアントはアップグレードを完了するためにupgradeパケットを送信する必要があります。
CLIENT                                                 SERVER
  │                                                      │
  │   GET /engine.io/?EIO=4&transport=websocket&sid=...  │
  │ ───────────────────────────────────────────────────► │
  │  ◄─────────────────────────────────────────────────┘ │
  │            HTTP 101 (WebSocket handshake)            │
  │                                                      │
  │            -----  WebSocket frames -----             │
  │  ─────────────────────────────────────────────────►  │
  │                         2probe                       │ (ping packet)
  │  ◄─────────────────────────────────────────────────  │
  │                         3probe                       │ (pong packet)
  │  ─────────────────────────────────────────────────►  │
  │                         5                            │ (upgrade packet)
  │                                                      │
メッセージ
ハンドシェイクが完了すると、クライアントとサーバーはmessageパケットに含めることでデータを交換できます。
パケットエンコード
Engine.IOパケットのシリアル化は、ペイロードの種類(プレーンテキストまたはバイナリ)とトランスポートによって異なります。
HTTPロングポーリング
HTTPロングポーリングトランスポートの性質により、スループットを向上させるために複数のパケットを単一のペイロードに連結することができます。
フォーマット
<packet type>[<data>]<separator><packet type>[<data>]<separator><packet type>[<data>][...]
例
4hello\x1e2\x1e4world
with:
4      => message packet type
hello  => message payload
\x1e   => separator
2      => ping packet type
\x1e   => separator
4      => message packet type
world  => message payload
パケットは、レコードセパレータ文字\x1eで区切られます。
バイナリペイロードはBase64エンコードされ、b文字を接頭辞として使用する必要があります。
例
4hello\x1ebAQIDBA==
with:
4         => message packet type
hello     => message payload
\x1e      => separator
b         => binary prefix
AQIDBA==  => buffer <01 02 03 04> encoded as base64
クライアントは、ハンドシェイクの際に送信されたmaxPayload値を使用して、連結する必要があるパケットの数を決定する必要があります。
WebSocket
Engine.IOの各パケットは、独自のWebSocketフレームで送信されます。
フォーマット
<packet type>[<data>]
例
4hello
with:
4      => message packet type
hello  => message payload (UTF-8 encoded)
バイナリペイロードは変更せずにそのまま送信されます。
履歴
v2からv3へ
- バイナリデータのサポートを追加
プロトコルの第2バージョンは、Socket.IO v0.9以下で使用されています。
プロトコルの第3バージョンは、Socket.IO v1およびv2で使用されています。
v3からv4へ
- ping/pongメカニズムの逆転
pingパケットは、ブラウザーに設定されたタイマーが十分に信頼できないため、現在はサーバーから送信されます。タイムアウト発生の問題の多くは、クライアント側でタイマーが遅延することによって発生すると考えられています。
- バイナリデータを含むペイロードをエンコードする際には、常にbase64を使用
この変更により、すべてのペイロード(バイナリあり/なし)を同じ方法で処理できるようになり、クライアントまたは現在のトランスポートがバイナリデータをサポートしているかどうかを考慮する必要がなくなります。
これはHTTPロングポーリングにのみ適用されることに注意してください。バイナリデータは、追加の変換なしでWebSocketフレームで送信されます。
- 文字カウントの代わりにレコードセパレーター(\x1e)を使用
文字をカウントすることは、他の言語(UTF-16エンコーディングを使用しない場合があります)でプロトコルを実装することを防ぎます(少なくともより難しくなります)。
たとえば、€は2:4€としてエンコードされましたが、Buffer.byteLength('€') === 3です。
注: これは、データでレコードセパレーターが使用されていないことを前提としています。
第4バージョン(現在)は、Socket.IO v3以降に含まれています。
テストスイート
test-suite/ディレクトリのテストスイートを使用すると、サーバー実装の準拠性を確認できます。
使用方法
- Node.jsの場合: npm ci && npm test
- ブラウザーの場合: ブラウザーでindex.htmlファイルを開くだけです
参考までに、JavaScriptサーバーがすべてのテストに合格するための予想構成を以下に示します
import { listen } from "engine.io";
const server = listen(3000, {
  pingInterval: 300,
  pingTimeout: 200,
  maxPayload: 1e6,
  cors: {
    origin: "*"
  }
});
server.on("connection", socket => {
  socket.on("data", (...args) => {
    socket.send(...args);
  });
});