Engine.IO 协议
本文档描述了 Engine.IO 协议的第四版。
¥This document describes the 4th version of the Engine.IO protocol.
该文档的来源可以找到 此处。
¥The source of this document can be found here.
表中的内容
¥Table of content
介绍
¥Introduction
Engine.IO 协议支持客户端和服务器之间的 full-duplex 和低开销通信。
¥The Engine.IO protocol enables full-duplex and low-overhead communication between a client and a server.
它基于 WebSocket 协议,如果无法建立 WebSocket 连接,则使用 HTTP 长轮询 作为后备。
¥It is based on the WebSocket protocol and uses HTTP long-polling as fallback if the WebSocket connection can't be established.
参考实现写在 TypeScript 中:
¥The reference implementation is written in TypeScript:
Socket.IO 协议 建立在这些基础之上,通过 Engine.IO 协议提供的通信通道带来了附加功能。
¥The Socket.IO protocol is built on top of these foundations, bringing additional features over the communication channel provided by the Engine.IO protocol.
传输
¥Transports
Engine.IO 客户端和 Engine.IO 服务器之间的连接可以通过以下方式建立:
¥The connection between an Engine.IO client and an Engine.IO server can be established with:
HTTP 长轮询
¥HTTP long-polling
HTTP 长轮询传输(也简称为 "polling")由连续的 HTTP 请求组成:
¥The HTTP long-polling transport (also simply referred as "polling") consists of successive HTTP requests:
长时间运行的
GET
请求,用于从服务器接收数据¥long-running
GET
requests, for receiving data from the server短期运行的
POST
请求,用于向服务器发送数据¥short-running
POST
requests, for sending data to the server
请求路径
¥Request path
HTTP 请求的路径默认为 /engine.io/
。
¥The path of the HTTP requests is /engine.io/
by default.
它可能会通过基于协议构建的库进行更新(例如,Socket.IO 协议使用 /socket.io/
)。
¥It might be updated by libraries built on top of the protocol (for example, the Socket.IO protocol uses /socket.io/
).
查询参数
¥Query parameters
使用以下查询参数:
¥The following query parameters are used:
名称 | 值 | 描述 |
---|---|---|
EIO | 4 | 强制,协议版本。 |
transport | polling | 必填,运输名称。 |
sid | <sid> | 会话建立后,会话标识符是必需的。 |
如果缺少强制查询参数,则服务器必须以 HTTP 400 错误状态进行响应。
¥If a mandatory query parameter is missing, then the server MUST respond with an HTTP 400 error status.
标头
¥Headers
发送二进制数据时,发送方(客户端或服务器)必须包含 Content-Type: application/octet-stream
标头。
¥When sending binary data, the sender (client or server) MUST include a Content-Type: application/octet-stream
header.
如果没有显式的 Content-Type
标头,接收方应该推断数据是明文。
¥Without an explicit Content-Type
header, the receiver SHOULD infer that the data is plaintext.
参考:https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers/Content-Type
¥Reference: https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers/Content-Type
发送和接收数据
¥Sending and receiving data
发送数据
¥Sending data
要发送一些数据包,客户端必须创建一个 HTTP POST
请求,并将数据包编码在请求正文中:
¥To send some packets, a client MUST create an HTTP POST
request with the packets encoded in the request body:
CLIENT SERVER
│ │
│ POST /engine.io/?EIO=4&transport=polling&sid=... │
│ ───────────────────────────────────────────────────► │
│ ◄──────────────────────────────────────────────────┘ │
│ HTTP 200 │
│ │
如果会话 ID(来自 sid
查询参数)未知,服务器必须返回 HTTP 400 响应。
¥The server MUST return an HTTP 400 response if the session ID (from the sid
query parameter) is not known.
要指示成功,服务器必须返回 HTTP 200 响应,并在响应正文中包含字符串 ok
。
¥To indicate success, the server MUST return an HTTP 200 response, with the string ok
in the response body.
为了确保数据包排序,客户端不得有多个活动 POST
请求。如果发生这种情况,服务器必须返回 HTTP 400 错误状态并关闭会话。
¥To ensure packet ordering, a client MUST NOT have more than one active POST
request. Should it happen, the server MUST return an HTTP 400 error status and close the session.
接收数据
¥Receiving data
要接收某些数据包,客户端必须创建 HTTP GET
请求:
¥To receive some packets, a client MUST create an HTTP GET
request:
CLIENT SERVER
│ GET /engine.io/?EIO=4&transport=polling&sid=... │
│ ──────────────────────────────────────────────────► │
│ . │
│ . │
│ . │
│ . │
│ ◄─────────────────────────────────────────────────┘ │
│ HTTP 200 │
如果会话 ID(来自 sid
查询参数)未知,服务器必须返回 HTTP 400 响应。
¥The server MUST return an HTTP 400 response if the session ID (from the sid
query parameter) is not known.
如果没有为给定会话缓冲数据包,服务器可能不会立即响应。一旦有一些数据包要发送,服务器应该对它们进行编码(参见 数据包编码)并在 HTTP 请求的响应正文中发送它们。
¥The server MAY not respond right away if there are no packets buffered for the given session. Once there are some packets to be sent, the server SHOULD encode them (see Packet encoding) and send them in the response body of the HTTP request.
为了确保数据包排序,客户端不得有多个活动 GET
请求。如果发生这种情况,服务器必须返回 HTTP 400 错误状态并关闭会话。
¥To ensure packet ordering, a client MUST NOT have more than one active GET
request. Should it happen, the server MUST return an HTTP 400 error status and close the session.
WebSocket
WebSocket 传输由 WebSocket 连接 组成,它在服务器和客户端之间提供双向且低延迟的通信通道。
¥The WebSocket transport consists of a WebSocket connection, which provides a bidirectional and low-latency communication channel between the server and the client.
使用以下查询参数:
¥The following query parameters are used:
名称 | 值 | 描述 |
---|---|---|
EIO | 4 | 强制,协议版本。 |
transport | websocket | 必填,运输名称。 |
sid | <sid> | 可选,取决于它是否是 HTTP 长轮询的升级。 |
如果缺少强制查询参数,则服务器必须关闭 WebSocket 连接。
¥If a mandatory query parameter is missing, then the server MUST close the WebSocket connection.
每个数据包(读或写)都会发送自己的 WebSocket 框架。
¥Each packet (read or write) is sent its own WebSocket frame.
客户端不得在每个会话中打开多个 WebSocket 连接。如果发生这种情况,服务器必须关闭 WebSocket 连接。
¥A client MUST NOT open more than one WebSocket connection per session. Should it happen, the server MUST close the WebSocket connection.
协议
¥Protocol
Engine.IO 数据包包含:
¥An Engine.IO packet consists of:
数据包类型
¥a packet type
可选的数据包有效负载
¥an optional packet payload
以下是可用数据包类型的列表:
¥Here is the list of available packet types:
类型 | ID | 用法 |
---|---|---|
打开 | 0 | handshake 期间使用。 |
关闭 | 1 | 用于指示可以关闭传输。 |
乒 | 2 | 用于 心跳机制。 |
pong | 3 | 用于 心跳机制。 |
message | 4 | 用于向另一端发送有效负载。 |
升级 | 5 | 升级过程 期间使用。 |
努普 | 6 | 升级过程 期间使用。 |
握手
¥Handshake
要建立连接,客户端必须向服务器发送 HTTP GET
请求:
¥To establish a connection, the client MUST send an HTTP GET
request to the server:
HTTP 长轮询优先(默认)
¥HTTP long-polling first (by default)
CLIENT SERVER
│ │
│ GET /engine.io/?EIO=4&transport=polling │
│ ───────────────────────────────────────────────────────► │
│ ◄──────────────────────────────────────────────────────┘ │
│ HTTP 200 │
│ │
仅 WebSocket 会话
¥WebSocket-only session
CLIENT SERVER
│ │
│ GET /engine.io/?EIO=4&transport=websocket │
│ ───────────────────────────────────────────────────────► │
│ ◄──────────────────────────────────────────────────────┘ │
│ HTTP 101 │
│ │
如果服务器接受连接,则它必须使用带有以下 JSON 编码负载的 open
数据包进行响应:
¥If the server accepts the connection, then it MUST respond with an open
packet with the following JSON-encoded payload:
键 | 类型 | 描述 |
---|---|---|
sid | string | 会话 ID。 |
upgrades | string[] | 可用 传输升级 的列表。 |
pingInterval | number | ping 间隔,在 心跳机制 中使用(以毫秒为单位)。 |
pingTimeout | number | ping 超时,在 心跳机制 中使用(以毫秒为单位)。 |
maxPayload | number | 每个块的最大字节数,客户端用于将数据包聚合到 payloads 中。 |
示例:
¥Example:
{
"sid": "lv_VI97HAXpY6yYWAAAC",
"upgrades": ["websocket"],
"pingInterval": 25000,
"pingTimeout": 20000,
"maxPayload": 1000000
}
客户端必须在所有后续请求的查询参数中发送 sid
值。
¥The client MUST send the sid
value in the query parameters of all subsequent requests.
心跳
¥Heartbeat
一旦 handshake 完成,就会启动心跳机制来检查连接的活跃度:
¥Once the handshake is completed, a heartbeat mechanism is started to check the liveness of the connection:
CLIENT SERVER
│ *** Handshake *** │
│ │
│ ◄───────────────────────────────────────────────── │
│ 2 │ (ping packet)
│ ─────────────────────────────────────────────────► │
│ 3 │ (pong packet)
在给定的时间间隔(握手中发送的 pingInterval
值),服务器发送 ping
数据包,客户端有几秒钟(pingTimeout
值)发回 pong
数据包。
¥At a given interval (the pingInterval
value sent in the handshake) the server sends a ping
packet and the client has a few seconds (the pingTimeout
value) to send a pong
packet back.
如果服务器没有收到返回的 pong
数据包,那么它应该认为连接已关闭。
¥If the server does not receive a pong
packet back, then it SHOULD consider that the connection is closed.
相反,如果客户端在 pingInterval + pingTimeout
内没有收到 ping
数据包,那么它应该认为连接已关闭。
¥Conversely, if the client does not receive a ping
packet within pingInterval + pingTimeout
, then it SHOULD consider that the connection is closed.
升级
¥Upgrade
默认情况下,客户端应该创建一个 HTTP 长轮询连接,然后升级到更好的传输(如果可用)。
¥By default, the client SHOULD create an HTTP long-polling connection, and then upgrade to better transports if available.
要升级到 WebSocket,客户端必须:
¥To upgrade to WebSocket, the client MUST:
暂停 HTTP 长轮询传输(不再发送 HTTP 请求),以确保没有数据包丢失
¥pause the HTTP long-polling transport (no more HTTP request gets sent), to ensure that no packet gets lost
使用相同的会话 ID 打开 WebSocket 连接
¥open a WebSocket connection with the same session ID
发送
ping
数据包,负载中包含字符串probe
¥send a
ping
packet with the stringprobe
in the payload
服务器必须:
¥The server MUST:
向任何待处理的
GET
请求(如果适用)发送noop
数据包,以彻底关闭 HTTP 长轮询传输¥send a
noop
packet to any pendingGET
request (if applicable) to cleanly close HTTP long-polling transport使用负载中包含字符串
probe
的pong
数据包进行响应¥respond with a
pong
packet with the stringprobe
in the payload
最后,客户端必须发送 upgrade
数据包来完成升级:
¥Finally, the client MUST send a upgrade
packet to complete the 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
一旦 handshake 完成,客户端和服务器就可以通过将其包含在 message
数据包中来交换数据。
¥Once the handshake is completed, the client and the server can exchange data by including it in a message
packet.
数据包编码
¥Packet encoding
Engine.IO 数据包的序列化取决于有效负载的类型(纯文本或二进制)和传输方式。
¥The serialization of an Engine.IO packet depends on the type of the payload (plaintext or binary) and on the transport.
HTTP 长轮询
¥HTTP long-polling
由于 HTTP 长轮询传输的性质,多个数据包可能会串联在一个有效负载中,以提高吞吐量。
¥Due to the nature of the HTTP long-polling transport, multiple packets might be concatenated in a single payload in order to increase throughput.
格式:
¥Format:
<packet type>[<data>]<separator><packet type>[<data>]<separator><packet type>[<data>][...]
示例:
¥Example:
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
¥The packets are separated by the record separator character: \x1e
二进制有效负载必须采用 base64 编码并以 b
字符为前缀:
¥Binary payloads MUST be base64-encoded and prefixed with a b
character:
示例:
¥Example:
4hello\x1ebAQIDBA==
with:
4 => message packet type
hello => message payload
\x1e => separator
b => binary prefix
AQIDBA== => buffer <01 02 03 04> encoded as base64
客户端应该使用 handshake 期间发送的 maxPayload
值来决定应连接多少个数据包。
¥The client SHOULD use the maxPayload
value sent during the handshake to decide how many packets should be concatenated.
WebSocket
每个 Engine.IO 数据包都在其自己的 WebSocket 框架 中发送。
¥Each Engine.IO packet is sent in its own WebSocket frame.
格式:
¥Format:
<packet type>[<data>]
示例:
¥Example:
4hello
with:
4 => message packet type
hello => message payload (UTF-8 encoded)
二进制有效负载按原样发送,无需修改。
¥Binary payloads are sent as is, without modification.
历史
¥History
从 v2 到 v3
¥From v2 to v3
添加对二进制数据的支持
¥add support for binary data
该协议的 第二版 用于 Socket.IO v0.9
及以下。
¥The 2nd version of the protocol is used in Socket.IO v0.9
and below.
协议的 第三版 用在 Socket.IO v1
和 v2
中。
¥The 3rd version of the protocol is used in Socket.IO v1
and v2
.
从 v3 到 v4
¥From v3 to v4
反向乒乓机制
¥reverse ping/pong mechanism
现在 ping 数据包由服务器发送,因为浏览器中设置的计时器不够可靠。我们怀疑很多超时问题是由于客户端的计时器延迟造成的。
¥The ping packets are now sent by the server, because the timers set in the browsers are not reliable enough. We suspect that a lot of timeout problems came from timers being delayed on the client-side.
使用二进制数据对有效负载进行编码时始终使用 base64
¥always use base64 when encoding a payload with binary data
此更改允许以相同的方式处理所有有效负载(带或不带二进制),而不必考虑客户端或当前传输是否支持二进制数据。
¥This change allows to treat all payloads (with or without binary) the same way, without having to take in account whether the client or the current transport supports binary data or not.
请注意,这仅适用于 HTTP 长轮询。二进制数据在 WebSocket 帧中发送,无需额外转换。
¥Please note that this only applies to HTTP long-polling. Binary data is sent in WebSocket frames with no additional transformation.
使用记录分隔符 (
\x1e
) 代替字符计数¥use a record separator (
\x1e
) instead of counting of characters
字符计数阻止(或至少变得更难)以其他语言实现协议,这些语言可能不使用 UTF-16 编码。
¥Counting characters prevented (or at least makes harder) to implement the protocol in other languages, which may not use the UTF-16 encoding.
例如,€
被编码为 2:4€
,但 Buffer.byteLength('€') === 3
却被编码为 2:4€
。
¥For example, €
was encoded to 2:4€
, though Buffer.byteLength('€') === 3
.
注意:这假设数据中未使用记录分隔符。
¥Note: this assumes the record separator is not used in the data.
第 4 个版本(当前)包含在 Socket.IO v3
及更高版本中。
¥The 4th version (current) is included in Socket.IO v3
and above.
测试套件
¥Test suite
test-suite/
目录中的测试套件可让你检查服务器实现的合规性。
¥The test suite in the test-suite/
directory lets you check the compliance of a server implementation.
用法:
¥Usage:
在 Node.js 中:
npm ci && npm test
¥in Node.js:
npm ci && npm test
在浏览器中:只需在浏览器中打开
index.html
文件¥in a browser: simply open the
index.html
file in your browser
作为参考,以下是 JavaScript 服务器通过所有测试的预期配置:
¥For reference, here is expected configuration for the JavaScript server to pass all tests:
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);
});
});