Skip to main content
Version: 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>一旦会话建立,必须,表示会话标识符。

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

头信息

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

如果没有明确的 Content-Type 头,接收方应推断数据是纯文本。

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

发送和接收数据

发送数据

要发送一些数据包,客户端必须创建一个 HTTP POST 请求,并在请求体中编码数据包:

客户端                                                 服务器

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

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

为了表示成功,服务器必须返回 HTTP 200 响应,并在响应体中包含字符串 ok

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

接收数据

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

客户端                                                服务器

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

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

如果没有缓冲的包要发送,服务器可能不会立即响应。一旦有一些包要发送,服务器应对其进行编码(参见数据包编码)并在 HTTP 请求的响应体中发送它们。

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

WebSocket

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

使用以下查询参数:

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

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

每个数据包(读或写)都在自己的WebSocket 帧中发送。

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

协议

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

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

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

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

握手

要建立连接,客户端必须向服务器发送 HTTP GET 请求:

  • 首先是 HTTP 长轮询(默认)
客户端                                                    服务器

│ │
│ GET /engine.io/?EIO=4&transport=polling │
│ ───────────────────────────────────────────────────────► │
│ ◄──────────────────────────────────────────────────────┘ │
│ HTTP 200 │
│ │
  • 仅 WebSocket 会话
客户端                                                    服务器

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

如果服务器接受连接,则必须响应一个带有以下 JSON 编码负载的 open 数据包:

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

示例:

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

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

心跳

一旦握手完成,启动一个心跳机制来检查连接的活跃性:

客户端                                                 服务器

│ *** 握手 *** │
│ │
│ ◄───────────────────────────────────────────────── │
│ 2 │ (ping 数据包)
│ ─────────────────────────────────────────────────► │
│ 3 │ (pong 数据包)

在给定的间隔(握手中发送的 pingInterval 值)内,服务器发送一个 ping 数据包,客户端有几秒钟(pingTimeout 值)来发送一个 pong 数据包。

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

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

升级

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

要升级到 WebSocket,客户端必须:

  • 暂停 HTTP 长轮询传输(不再发送 HTTP 请求),以确保没有数据包丢失
  • 使用相同的会话 ID 打开一个 WebSocket 连接
  • 发送一个带有字符串 probeping 数据包

服务器必须:

  • 向任何挂起的 GET 请求发送一个 noop 数据包(如果适用),以干净地关闭 HTTP 长轮询传输
  • 响应一个带有字符串 probepong 数据包

最后,客户端必须发送一个 upgrade 数据包以完成升级:

客户端                                                 服务器

│ │
│ GET /engine.io/?EIO=4&transport=websocket&sid=... │
│ ───────────────────────────────────────────────────► │
│ ◄─────────────────────────────────────────────────┘ │
│ HTTP 101 (WebSocket 握手) │
│ │
│ ----- WebSocket 帧 ----- │
│ ─────────────────────────────────────────────────► │
│ 2probe │ (ping 数据包)
│ ◄───────────────────────────────────────────────── │
│ 3probe │ (pong 数据包)
│ ─────────────────────────────────────────────────► │
│ 5 │ (upgrade 数据包)
│ │

消息

一旦握手完成,客户端和服务器可以通过在 message 数据包中包含数据来交换数据。

数据包编码

Engine.IO 数据包的序列化取决于负载的类型(纯文本或二进制)和传输方式。

HTTP 长轮询

由于 HTTP 长轮询传输的性质,多个数据包可能会被连接到一个负载中以增加吞吐量。

格式:

<packet type>[<data>]<separator><packet type>[<data>]<separator><packet type>[<data>][...]

示例:

4hello\x1e2\x1e4world

其中:

4 => 消息数据包类型
hello => 消息负载
\x1e => 分隔符
2 => ping 数据包类型
\x1e => 分隔符
4 => 消息数据包类型
world => 消息负载

数据包由记录分隔符字符分隔:\x1e

二进制负载必须进行 base64 编码并以 b 字符为前缀:

示例:

4hello\x1ebAQIDBA==

其中:

4 => 消息数据包类型
hello => 消息负载
\x1e => 分隔符
b => 二进制前缀
AQIDBA== => 缓冲区 <01 02 03 04> 编码为 base64

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

WebSocket

每个 Engine.IO 数据包都在自己的WebSocket 帧中发送。

格式:

<packet type>[<data>]

示例:

4hello

其中:

4 => 消息数据包类型
hello => 消息负载(UTF-8 编码)

二进制负载按原样发送,不做修改。

历史

从 v2 到 v3

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

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

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

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