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

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/)。

查询参数

使用以下查询参数

名称描述
EIO4必需,协议的版本。
transportpolling必需,传输的名称。
sid<sid>在建立会话后必需,会话标识符。

如果缺少必需的查询参数,则服务器 MUST 以 HTTP 400 错误状态响应。

标头

发送二进制数据时,发送方(客户端或服务器)MUST 包含 Content-Type: application/octet-stream 标头。

如果没有显式的 Content-Type 标头,接收方 SHOULD 推断数据为纯文本。

参考: https://mdn.org.cn/en-US/docs/Web/HTTP/Headers/Content-Type

发送和接收数据

发送数据

要发送一些数据包,客户端 MUST 创建一个 HTTP POST 请求,并将数据包编码在请求正文中

CLIENT                                                 SERVER

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

如果会话 ID(来自 sid 查询参数)未知,则服务器 MUST 返回 HTTP 400 响应。

要指示成功,服务器 MUST 返回 HTTP 200 响应,并在响应正文中包含字符串 ok

为了确保数据包排序,客户端 MUST 不得有多个活动的 POST 请求。如果发生这种情况,服务器 MUST 返回 HTTP 400 错误状态并关闭会话。

接收数据

要接收一些数据包,客户端 MUST 创建一个 HTTP GET 请求

CLIENT                                                SERVER

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

如果会话 ID(来自 sid 查询参数)未知,则服务器 MUST 返回 HTTP 400 响应。

如果为给定会话缓冲的数据包不存在,则服务器 MAY 不立即响应。一旦有一些数据包要发送,服务器 SHOULD 对其进行编码(参见 数据包编码)并在 HTTP 请求的响应正文中发送它们。

为了确保数据包排序,客户端 MUST 不得有多个活动的 GET 请求。如果发生这种情况,服务器 MUST 返回 HTTP 400 错误状态并关闭会话。

WebSocket

WebSocket 传输由一个 WebSocket 连接 组成,该连接在服务器和客户端之间提供双向和低延迟通信通道。

使用以下查询参数

名称描述
EIO4必需,协议的版本。
transportwebsocket必需,传输的名称。
sid<sid>可选,取决于它是否是从 HTTP 长轮询升级而来。

如果缺少必需的查询参数,则服务器 MUST 关闭 WebSocket 连接。

每个数据包(读或写)都发送到其自己的 WebSocket 帧

客户端 MUST 不得为每个会话打开多个 WebSocket 连接。如果发生这种情况,服务器 MUST 关闭 WebSocket 连接。

协议

Engine.IO 数据包由以下部分组成

  • 数据包类型
  • 可选的数据包有效负载

以下是可用数据包类型的列表

类型ID用途
open0握手 期间使用。
close1用于指示传输可以关闭。
ping2心跳机制 中使用。
pong3心跳机制 中使用。
message4用于向另一方发送有效负载。
upgrade5升级过程 期间使用。
noop6升级过程 期间使用。

握手

要建立连接,客户端 MUST 向服务器发送 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 │
│ │

如果服务器接受连接,则它 MUST 以 open 数据包响应,该数据包包含以下 JSON 编码的有效负载

类型描述
sidstring会话 ID。
upgradesstring[]可用 传输升级 的列表。
pingIntervalnumberping 间隔,用于 心跳机制(以毫秒为单位)。
pingTimeoutnumberping 超时,用于 心跳机制(以毫秒为单位)。
maxPayloadnumber每个块的最大字节数,客户端使用它将数据包聚合到 有效负载 中。

示例

{
"sid": "lv_VI97HAXpY6yYWAAAC",
"upgrades": ["websocket"],
"pingInterval": 25000,
"pingTimeout": 20000,
"maxPayload": 1000000
}

客户端 MUST 在所有后续请求的查询参数中发送 sid 值。

心跳

完成 握手 后,将启动心跳机制以检查连接的活动状态

CLIENT                                                 SERVER

│ *** Handshake *** │
│ │
│ ◄───────────────────────────────────────────────── │
│ 2 │ (ping packet)
│ ─────────────────────────────────────────────────► │
│ 3 │ (pong packet)

在给定的间隔(握手期间发送的 pingInterval 值)内,服务器发送 ping 数据包,客户端有几秒钟(pingTimeout 值)时间发送 pong 数据包作为响应。

如果服务器没有收到 pong 数据包作为响应,则它 SHOULD 认为连接已关闭。

相反,如果客户端在 pingInterval + pingTimeout 内没有收到 ping 数据包,则它 SHOULD 认为连接已关闭。

升级

默认情况下,客户端 SHOULD 创建 HTTP 长轮询连接,然后升级到更好的传输(如果可用)。

要升级到 WebSocket,客户端 MUST

  • 暂停 HTTP 长轮询传输(不再发送 HTTP 请求),以确保不会丢失任何数据包
  • 使用相同的会话 ID 打开 WebSocket 连接
  • 发送一个 ping 数据包,并在有效负载中包含字符串 probe

服务器 MUST

  • 向任何挂起的 GET 请求发送 noop 数据包(如果适用),以干净地关闭 HTTP 长轮询传输
  • pong 数据包响应,并在有效负载中包含字符串 probe

最后,客户端 MUST 发送 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

二进制有效负载 MUST 使用 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

客户端 SHOULD 使用握手期间发送的 maxPayload 值来决定应该连接多少个数据包。

WebSocket

每个 Engine.IO 数据包都发送到其自己的 WebSocket 帧

格式

<packet type>[<data>]

示例

4hello

with:

4 => message packet type
hello => message payload (UTF-8 encoded)

二进制有效负载按原样发送,无需修改。

历史

从 v2 到 v3

  • 添加对二进制数据的支持

Socket.IO v0.9 及以下版本使用协议的 第 2 版

Socket.IO v1v2 使用协议的 第 3 版

从 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);
});
});