Socket.IO 协议 v3 详解:包类型、编码格式与交换流程(含 socket.io-parser 源码印证)
本文基于 Socket.IO 仓库中 Socket.IO 协议 v3 规范 撰写。读完本文,你将掌握 Socket.IO 协议第 3 版(revision 3,对应 socket.io@1.0.0...1.0.2 早期版本)的完整包格式、六种包类型的语义、线上编码(wire format)规则、连接/确认/断开的交换协议,并能对照仓库中的 socket.io-parser 参考实现理解每一段编码是如何被编解码的。
协议定位:Socket.IO 协议建立在 Engine.IO 协议之上
Socket.IO 协议文档开篇即明确了协议的分层设计:底层由 Engine.IO 协议(本文对应的 Engine.IO 第 3 版)负责 WebSocket 与 HTTP 长轮询(long-polling)的低层管道、心跳与升级;Socket.IO 协议则在其上再封装一层,提供两个核心能力:
- 多路复用(Multiplexing):即 Socket.IO 中的「命名空间(Namespace)」概念——同一条底层连接上可以并发接入多个逻辑连接;
- 包的确认机制(Acknowledgement):发送方可以为一个事件请求接收方回调确认。
原文档给出的 JavaScript API 示例:
// server-side
const nsp = io.of("/admin");
nsp.on("connect", socket => {});
// client-side
const socket1 = io(); // default namespace
const socket2 = io("/admin");
socket2.on("connect", () => {});
// on one side
socket.emit("hello", 1, () => { console.log("received"); });
// on the other side
socket.on("hello", (a, cb) => { cb(); });
这些高层 API 落到线上,就是本文后面详述的 CONNECT、EVENT、ACK 等包。当前仓库的参考实现即 socket.io-parser(编解码)、socket.io-client 与 socket.io 服务端。
包格式(Packet Format)
一个 Socket.IO v3 协议包包含以下字段:
type:类型,整数,取值见下方包类型表;nsp:命名空间,字符串;data(可选):payload,字符串或数组;id(可选):确认 ID(acknowledgment id),整数。
包类型一览(v3 版本共 6 种):
| 类型 | ID | 用途 |
|---|---|---|
| CONNECT | 0 | 客户端请求接入某命名空间;服务器接受连接时也发送 |
| DISCONNECT | 1 | 某一方断开与某命名空间的连接 |
| EVENT | 2 | 传输不含二进制的普通数据 |
| ACK | 3 | 响应带确认 ID 的 EVENT 或 BINARY_EVENT |
| ERROR | 4 | 服务器拒绝某命名空间的连接请求 |
| BINARY_EVENT | 5 | 传输含二进制的普通数据 |
注意与协议 v4/v5 的区别:v4 新增了
BINARY_ACK(类型 6);v5 将ERROR更名为CONNECT_ERROR,且CONNECT包可以携带 payload。完整的版本演进对照可参考同目录下的 v4 规范与 v5 规范。
0 - CONNECT
发送方有两种:
- 客户端请求接入某命名空间时发送;
- 服务器接受该命名空间的连接时回复。
它不携带 payload,也不携带确认 ID。示例:
{
"type": 0,
"nsp": "/admin"
}
客户端还可以在命名空间字段中附带额外信息(典型用途是认证),例如:
{
"type": 0,
"nsp": "/admin?token=1234&uid=abcd"
}
即把认证参数塞在 nsp 字段的查询串里——这正是 v3 阶段的认证方式;到了 v5 协议,这类 payload 被正式移入包的 data 字段。
1 - DISCONNECT
当某一方希望断开与某命名空间的连接时使用,无 payload、无确认 ID:
{
"type": 1,
"nsp": "/admin"
}
2 - EVENT
用于传输不含二进制的数据,携带 payload,确认 ID 可选:
{
"type": 2,
"nsp": "/",
"data": ["hello", 1]
}
带确认 ID 时:
{
"type": 2,
"nsp": "/admin",
"data": ["project:delete", 123],
"id": 456
}
3 - ACK
当某一方收到了带确认 ID 的 EVENT 或 BINARY_EVENT 后,用它回应。ACK 包包含从上一包收到的确认 ID,payload 可选且不含二进制:
{
"type": 3,
"nsp": "/admin",
"data": [],
"id": 456
}
4 - ERROR
当服务器拒绝某命名空间的连接时发送,payload 可指示拒绝原因:
{
"type": 4,
"nsp": "/admin",
"data": "Not authorized"
}
5 - BINARY_EVENT
用于传输包含二进制的数据,携带 payload,确认 ID 可选:
{
"type": 5,
"nsp": "/",
"data": ["hello", <Buffer 01 02 03>]
}
带确认 ID 时:
{
"type": 5,
"nsp": "/admin",
"data": ["project:delete", <Buffer 01 02 03>],
"id": 456
}
包编码(Packet Encoding)
这一节描述的是随 Socket.IO 服务端与客户端内置的默认解析器的编码规则(参考实现即本仓库的 packages/socket.io-parser)。文档还指出 JS 实现支持自定义解析器(如 socket.io-json-parser、socket.io-msgpack-parser),供不同权衡的场景选择。
编码格式
<packet type>[<# of binary attachments>-][<namespace>,][<acknowledgment id>][JSON-stringified payload without binary]
+ binary attachments extracted
即线上字符串由以下部分按序拼接:包类型数字 →(若含二进制附件)附件数量加 - →(若非默认命名空间)命名空间加 , → 确认 ID → 去除二进制后的 JSON payload;二进制附件则从字符串中剥离,作为独立数据块跟在后面传输。
注意:命名空间只有在不同于默认命名空间 / 时才会被写入编码字符串。
对照当前仓库源码 encodeAsString 可以逐段印证该格式:
// first is type
let str = "" + obj.type;
// attachments if we have them
if (obj.type === PacketType.BINARY_EVENT || obj.type === PacketType.BINARY_ACK) {
str += obj.attachments + "-";
}
// if we have a namespace other than `/`
// we append it followed by a comma `,`
if (obj.nsp && "/" !== obj.nsp) {
str += obj.nsp + ",";
}
// immediately followed by the id
if (null != obj.id) {
str += obj.id;
}
// json data
if (null != obj.data) {
str += JSON.stringify(obj.data, this.replacer);
}
反向的解析逻辑在 decodeString:先取首字符为包类型,再依次识别附件数(- 分隔)、命名空间(以 / 开头、逗号结束)、确认 ID(纯数字串)和 JSON payload。
另一个重要的线上细节:每个 Socket.IO 包都会被封装进一个 Engine.IO message 包发送,因此编码结果在网络上会被加上前缀 4(出现在 HTTP 长轮询的 request/response body 中,或 WebSocket 帧里)。
编码示例(完整继承自 v3 规范)
| 包 | 编码结果 |
|---|---|
CONNECT,默认命名空间 { "type": 0, "nsp": "/" } |
0 |
CONNECT,/admin 命名空间 |
0/admin |
DISCONNECT,/admin 命名空间 |
1/admin |
EVENT { "type": 2, "nsp": "/", "data": ["hello", 1] } |
2["hello",1] |
EVENT 带确认 ID { "type": 2, "nsp": "/admin", "data": ["project:delete", 123], "id": 456 } |
2/admin,456["project:delete",123] |
ACK { "type": 3, "nsp": "/admin", "data": [], "id": 456 } |
3/admin,456[] |
ERROR { "type": 4, "nsp": "/admin", "data": "Not authorized" } |
4/admin,"Not authorized" |
BINARY_EVENT { "type": 5, "nsp": "/", "data": ["hello", <Buffer 01 02 03>] } |
51-["hello",{"_placeholder":true,"num":0}] + <Buffer 01 02 03> |
BINARY_EVENT 带确认 ID(/admin,id: 456) |
51-/admin,456["project:delete",{"_placeholder":true,"num":0}] + <Buffer 01 02 03> |
其中 51- 的含义是「包类型 5 + 1 个二进制附件」;payload 中的 {"_placeholder":true,"num":0} 是第 0 个二进制附件的占位符。
占位符机制在源码中的实现值得展开:
- 编码侧:deconstructPacket 递归遍历 payload,将每个
Buffer/ArrayBuffer/Blob等二进制对象替换为{ _placeholder: true, num: <序号> },同时把真实二进制收集进buffers数组,并把附件数量写入pack.attachments。若对象实现了toJSON,会先序列化再递归。 - 解码侧:reconstructPacket 依据占位符的
num字段把buffers中的二进制数据按序还原回 payload 对应位置;若num越界则抛出illegal attachments错误。而 BinaryReconstructor 负责跨帧收集二进制数据——只有当收到的缓冲区数量等于包中声明的attachments数时,才输出最终还原后的包。
也就是说,一个含二进制的 BINARY_EVENT 在线上实际是「一段字符串 + 若干二进制块」的组合,解码端需要状态机式的重组,这也是 attachments 计数和占位符序号必须严格一致的原因。
与 Engine.IO 编码的关系
由于 Socket.IO 包外层套着 Engine.IO 包,HTTP 长轮询场景下还会再经过 Engine.IO 的 payload 编码。以 v3 时期的 Engine.IO 为例(参见 Engine.IO v3 规范),不支持 XHR2 时字符串 payload 格式为 <length1>:<packet1>[<length2>:<packet2>[...]],其中 length 是字符数而非字节数。因此服务端收到 2["hello",1] 这样的 Socket.IO EVENT 编码后,完整线上形态大致是 4:42["hello",1](外层 4 表示 Engine.IO message 包,前缀 42 中的 4 即该前缀)。仓库中 docs/socket.io-protocol/v3.md 的交换示例即按此展开。
交换协议(Exchange Protocol)
连接默认命名空间
只要底层连接建立,服务器总是先向客户端发送一个默认命名空间(/)的 CONNECT 包:
Server > { type: CONNECT, nsp: "/" }
也就是说,即使客户端请求的是非默认命名空间,它也会先收到默认命名空间的 CONNECT 包。客户端无需响应。这是 v3/v4 时代的显著特征——默认命名空间的连接是隐式建立的;到了 v5 协议,这一隐式行为被移除,客户端必须显式发送 CONNECT(参见 v5 规范的 History 章节)。
连接非默认命名空间
Client > { type: CONNECT, nsp: "/admin" }
Server > { type: CONNECT, nsp: "/admin" } (if the connection is successful)
or
Server > { type: ERROR, nsp: "/admin", data: "Not authorized" }
断开非默认命名空间
Client > { type: DISCONNECT, nsp: "/admin" }
服务器向客户端发起同理。对端不需要响应。
确认(Acknowledgement)
Client > { type: EVENT, nsp: "/admin", data: ["hello"], id: 456 }
Server > { type: ACK, nsp: "/admin", data: [], id: 456 }
双向同理:服务器发的 EVENT 带 ID,客户端则以 ACK 回应。ACK 包中的 id 必须与所确认的包一致。在参考实现中,Decoder.add 会对解码出的 BINARY_EVENT/BINARY_ACK 包先转换为 EVENT/ACK 并挂起重组,直到所有二进制附件收齐才通过 decoded 事件对外发出——确认回包同理受此机制保护。
历史演进:v3 在协议谱系中的位置
v3 规范文档末尾的 History 章节给出了完整的版本脉络,理解它对判断「哪些行为属于 v3、哪些是后来才有的」很关键:
- v3 与 v2 的差异:移除了使用 msgpack 编码含二进制对象包的做法。此前(v2)的二进制包用 msgpack 序列化,v3 起改为本文所述的「JSON + 占位符 + 独立二进制附件」方案,减少了对 msgpack 库的依赖,编码更透明。
- v2 与 v1 的差异:新增了
BINARY_EVENT包类型,这是 Socket.IO 1.0 开发期间为支持二进制对象而引入的。 - 初版(v1):是 Engine.IO 协议(WebSocket / 长轮询、心跳等低层管道)与 Socket.IO 协议分层的产物,从未随任何 Socket.IO 正式版本发布,但为后续迭代奠定了基础。
v3 之后的两个版本也值得对照:
- v4(
socket.io@1.0.3...latest,即此后长期使用的版本):新增BINARY_ACK包类型(类型 6)。在此之前 ACK 包始终被当作「可能含二进制」来处理,需要递归搜索二进制对象,可能拖累性能;v4 通过独立包类型消除了这一递归。另注意 v4 的编码中命名空间后固定带逗号(0/admin,),而 v3 示例中为0/admin,两者格式已有差异。 - v5(当前版本,对应 Socket.IO v3 及以上,
socket.io@3.0.0于 2020 年 11 月发布):移除默认命名空间的隐式连接、ERROR更名为CONNECT_ERROR、CONNECT可携带 payload(认证数据与sid)、CONNECT_ERROR的 payload 由字符串变为对象。当前仓库 socket.io-parser 中的protocol = 5与PacketType枚举(含CONNECT_ERROR、BINARY_ACK)即 v5 的体现。
因此,本文的 v3 规范应视为历史参考文档:它精确对应 socket.io@1.0.0...1.0.2 这一早期窗口;若你在维护 2014 年前后的 Socket.IO v1 早期版本并与非 JS 生态的自研实现对齐线上抓包,本文的包类型编号、编码格式与交换顺序即可作为权威依据。
小结
Socket.IO 协议 v3 以极简的「类型号 + 命名空间 + 确认 ID + JSON payload」字符串格式,在 Engine.IO 的 message 包之上实现了命名空间多路复用与事件确认两大特性;其二进制传输采用占位符加独立附件、附件计数校验的机制。规范中定义的每一种包与每条编码规则,都能在当前仓库的 packages/socket.io-parser/lib/index.ts 与 packages/socket.io-parser/lib/binary.ts 中找到一一对应的编解码实现,这也是该规范作为「参考实现(reference implementation)」契约价值的体现。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00