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

Engine.IO 協定

此文件說明 Engine.IO 協定的第 4 版。

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

目錄

簡介

Engine.IO 協定能讓 全雙工 以及低負載的通訊在客戶端與伺服器之間進行。

它是基於 WebSocket 協定 並使用 HTTP 長輪詢 作為備用,如果 WebSocket 連線無法建立。

參考實作是用 TypeScript 編寫的

這些基礎上建構了 Socket.IO 協定,透過 Engine.IO 協定提供的通訊管道帶來額外的功能。

傳輸

Engine.IO 客戶端與 Engine.IO 伺服器之間的連線可以用

HTTP 長輪詢

HTTP 長輪詢傳輸(也簡稱為「輪詢」)包含連續的 HTTP 要求

  • 長執行時間的 GET 要求,用於從伺服器接收資料
  • 短執行時間的 POST 要求,用於將資料傳送至伺服器

要求路徑

HTTP 要求的路徑預設為 /engine.io/

它可能會由建構在協定上的函式庫更新(例如,Socket.IO 協定使用 /socket.io/)。

查詢參數

使用以下查詢參數

名稱說明
EIO4強制,協定的版本。
transportpolling強制,傳輸的名稱。
sid<sid>一旦建立連線就強制,連線識別碼。

如果缺少強制查詢參數,伺服器必須回應 HTTP 400 錯誤狀態。

標頭

傳送二進制資料時,傳送者(客戶端或伺服器)必須包含 Content-Type: application/octet-stream 標頭。

沒有明確的 Content-Type 標頭,接收者應該推斷資料為純文字。

參考:https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type

傳送和接收資料

傳送資料

要傳送一些封包,客戶端必須建立 HTTP POST 要求,並將封包編碼在要求主體中

CLIENT                                                 SERVER

│ │
│ POST /engine.io/?EIO=4&transport=polling&sid=... │
│ ───────────────────────────────────────────────────► │
│ ◄──────────────────────────────────────────────────┘ │
│ HTTP 200 │
│ │

如果不知道會話 ID(來自 sid 查詢參數),伺服器必須傳回 HTTP 400 回應。

為了表示成功,伺服器必須傳回 HTTP 200 回應,並在回應主體中包含字串 ok

為了確保封包順序,客戶端不得有多個 active POST 要求。如果發生這種情況,伺服器必須傳回 HTTP 400 錯誤狀態,並關閉會話。

接收資料

要接收一些封包,客戶端必須建立 HTTP GET 要求

CLIENT                                                SERVER

│ GET /engine.io/?EIO=4&transport=polling&sid=... │
│ ──────────────────────────────────────────────────► │
│ . │
│ . │
│ . │
│ . │
│ ◄─────────────────────────────────────────────────┘ │
│ HTTP 200 │

如果不知道會話 ID(來自 sid 查詢參數),伺服器必須傳回 HTTP 400 回應。

如果沒有為特定會話緩衝任何封包,伺服器可能不會立即回應。一旦有一些封包要傳送,伺服器應該對其進行編碼(請參閱 封包編碼),並在 HTTP 要求的回應主體中傳送它們。

為了確保封包順序,客戶端不得有多個 active GET 要求。如果發生這種情況,伺服器必須傳回 HTTP 400 錯誤狀態,並關閉會話。

WebSocket

WebSocket 傳輸包含 WebSocket 連線,它提供伺服器和客戶端之間的雙向低延遲通訊管道。

使用以下查詢參數

名稱說明
EIO4強制,協定的版本。
transportwebsocket強制,傳輸的名稱。
sid<sid>選擇性,視是否從 HTTP 長輪詢升級而定。

如果缺少強制查詢參數,伺服器必須關閉 WebSocket 連線。

每個封包(讀取或寫入)會傳送其自己的 WebSocket 框架

用戶端不得為每個工作階段開啟超過一個 WebSocket 連線。如果發生,伺服器必須關閉 WebSocket 連線。

通訊協定

Engine.IO 封包包含

  • 封包類型
  • 選用封包酬載

以下是可用封包類型的清單

類型ID用法
開啟0交握 期間使用。
關閉1用於指出傳輸可以關閉。
ping2心跳機制 中使用。
pong3心跳機制 中使用。
訊息4用於將酬載傳送至另一方。
升級5升級程序 中使用。
noop6升級程序 中使用。

交握

為建立連線,用戶端必須傳送 HTTP GET 要求至伺服器

  • HTTP 長輪詢優先(預設)
CLIENT                                                    SERVER

│ │
│ GET /engine.io/?EIO=4&transport=polling │
│ ───────────────────────────────────────────────────────► │
│ ◄──────────────────────────────────────────────────────┘ │
│ HTTP 200 │
│ │
  • 僅 WebSocket 工作階段
CLIENT                                                    SERVER

│ │
│ GET /engine.io/?EIO=4&transport=websocket │
│ ───────────────────────────────────────────────────────► │
│ ◄──────────────────────────────────────────────────────┘ │
│ HTTP 101 │
│ │

如果伺服器接受連線,則必須以下列 JSON 編碼酬載回應 open 封包

金鑰類型說明
sid字串工作階段 ID。
升級字串陣列可用 傳輸升級 清單。
ping 間隔數字ping 間隔,用於 心跳機制(以毫秒為單位)。
ping 超時數字ping 超時,用於 心跳機制(以毫秒為單位)。
最大酬載數字每個區塊的位元組最大數量,由用戶端用於將封包彙整至 酬載

範例

{
"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 長輪詢連線,然後在有可用情況下升級至更好的傳輸方式。

若要升級至 WebSocket,用戶端必須

  • 暫停 HTTP 長輪詢傳輸(不再傳送 HTTP 要求),以確保沒有封包遺失
  • 使用相同會話 ID 開啟 WebSocket 連線
  • 傳送載荷中包含字串 probeping 封包

伺服器必須

  • 傳送 noop 封包至任何待處理的 GET 要求(如果適用),以乾淨地關閉 HTTP 長輪詢傳輸
  • 回應載荷中包含字串 probepong 封包

最後,用戶端必須傳送 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 v1v2

從 v3 到 v4

  • 反向 ping/pong 機制

ping 封包現在由伺服器傳送,因為瀏覽器中設定的計時器不夠可靠。我們懷疑許多逾時問題來自於計時器在客戶端延遲。

  • 編碼含有二進位資料的 payload 時,總是使用 base64

此變更允許以相同方式處理所有 payload(無論是否有二進位資料),而無需考慮客戶端或目前的傳輸是否支援二進位資料。

請注意,這僅適用於 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);
});
});