Skip to main content
Version: 4.x

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:

名称描述
EIO4强制,协议版本。
transportpolling必填,运输名称。
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:

名称描述
EIO4强制,协议版本。
transportwebsocket必填,运输名称。
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用法
打开0handshake 期间使用。
关闭1用于指示可以关闭传输。
2用于 心跳机制
pong3用于 心跳机制
message4用于向另一端发送有效负载。
升级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:

类型描述
sidstring会话 ID。
upgradesstring[]可用 传输升级 的列表。
pingIntervalnumberping 间隔,在 心跳机制 中使用(以毫秒为单位)。
pingTimeoutnumberping 超时,在 心跳机制 中使用(以毫秒为单位)。
maxPayloadnumber每个块的最大字节数,客户端用于将数据包聚合到 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 string probe in the payload

服务器必须:

¥The server MUST:

  • 向任何待处理的 GET 请求(如果适用)发送 noop 数据包,以彻底关闭 HTTP 长轮询传输

    ¥send a noop packet to any pending GET request (if applicable) to cleanly close HTTP long-polling transport

  • 使用负载中包含字符串 probepong 数据包进行响应

    ¥respond with a pong packet with the string probe 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 v1v2 中。

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