深入理解 Socket.IO v4 协议:包类型、编码格式与完整线上传输流程
Socket.IO v4 协议建立在 Engine.IO v3(EIO=3)之上,在低层 WebSocket / HTTP long-polling 管道之上叠加了命名空间复用(multiplexing)与包确认(acknowledgement)两大能力。本文以仓库中的协议规范文档 docs/socket.io-protocol/v4.md 为骨架,逐字段拆解 v4 的 7 种包类型、字符串编码格式与完整交互流程,并结合当前仓库中 packages/socket.io-parser 的参考实现,说明这些设计如何落地为可运行的编解码逻辑,帮助读者读懂线上抓到的每一段 42["hello"] 类字节流。
协议定位与版本
v4 是 Socket.IO 协议的第 4 次修订,随 socket.io@1.0.3...latest 引入。它构建于 Engine.IO 协议的第 3 次修订之上,因此线上请求会携带 EIO=3 查询参数。
需要明确一个前提:当前仓库中的实现(socket.io、socket.io-client 版本均为 4.8.3,socket.io-parser 版本 4.2.7)已经演进到第 5 次修订(EIO=4)。v5 文档见 v5-current.md,v4 与 v5 的差异见本文末尾"协议历史"小节。阅读本文时应注意:v4 描述的隐式默认命名空间连接、ERROR 命名等细节,在当前代码中已由 v5 取代,但包类型编号与编码格式的核心结构保持一致。
v4 相对低层 Engine.IO 协议增加的能力:
- multiplexing(命名空间复用):同一条底层 WebSocket 连接可承载多个命名空间。
// server-side
const nsp = io.of("/admin");
nsp.on("connect", socket => {});
// client-side
const socket1 = io(); // 默认命名空间
const socket2 = io("/admin");
socket2.on("connect", () => {});
- 包确认(acknowledgement):发送方可附带回调,接收方通过
ACK包回传结果。
// on one side
socket.emit("hello", 1, () => { console.log("received"); });
// on the other side
socket.on("hello", (a, cb) => { cb(); });
包结构
一个 v4 包包含以下字段:
type:整数,包类型(见下节)nsp:命名空间(字符串)data(可选):payload(string 或 Array)id(可选):acknowledgment id(整数)
当前仓库的参考实现将包建模为如下接口(见 Packet 定义),其中还额外携带了二进制场景下的 attachments 计数:
export interface Packet {
type: PacketType;
nsp: string;
data?: any;
id?: number;
attachments?: number;
}
七种包类型
v4 定义了 7 种包类型,编号与语义如下。
0 - CONNECT
由客户端发起(请求访问某命名空间),也由服务端下发(接受连接)。不含 payload,也不含 acknowledgment id。
{ "type": 0, "nsp": "/admin" }
客户端可在命名空间字段中附加额外信息(例如鉴权参数):
{ "type": 0, "nsp": "/admin?token=1234&uid=abcd" }
说明:v4 通过
nsp字段里拼查询串来传递鉴权信息。v5 改为在data中携带结构化 payload,这是 v4 与 v5 的一处关键差异(见末尾历史小节)。
1 - DISCONNECT
当一方希望断开某个命名空间时使用。不含 payload 与 acknowledgment id。
{ "type": 1, "nsp": "/admin" }
2 - EVENT
当一方希望向另一方传输数据(不含二进制)时使用。包含 payload,可含 acknowledgment id。
{ "type": 2, "nsp": "/", "data": ["hello", 1] }
带 acknowledgment id:
{ "type": 2, "nsp": "/admin", "data": ["project:delete", 123], "id": 456 }
3 - ACK
当一方收到带 acknowledgment id 的 EVENT 或 BINARY_EVENT 时回传。它携带上一包收到的 acknowledgment id,并可能携带 payload(不含二进制)。
{ "type": 3, "nsp": "/admin", "data": [], "id": 456 }
4 - ERROR
由服务端在拒绝某命名空间的连接时发送。可能携带说明拒绝原因的 payload。
{ "type": 4, "nsp": "/admin", "data": "Not authorized" }
命名说明:v4 中该类型名为
ERROR,v5 更名为CONNECT_ERROR,编号仍为 4,语义不变(见 PacketType 枚举)。
5 - BINARY_EVENT
当一方希望传输含二进制的数据时使用。包含 payload,可含 acknowledgment id。
{ "type": 5, "nsp": "/", "data": ["hello", <Buffer 01 02 03>] }
带 acknowledgment id:
{ "type": 5, "nsp": "/admin", "data": ["project:delete", <Buffer 01 02 03>], "id": 456 }
6 - BINARY_ACK
当一方收到带 acknowledgment id 的 EVENT 或 BINARY_EVENT 时回传,且响应中包含二进制。
{ "type": 6, "nsp": "/admin", "data": [<Buffer 03 02 01>], "id": 456 }
包编码格式
这一节详述默认解析器(即随 Socket.IO 服务端与客户端一起分发的那个解析器)所使用的编码。其源码见 packages/socket.io-parser。
JS 服务端与客户端还支持自定义解析器,它们有各自的取舍,适合特定类型的应用(例如 msgpack 解析器,仓库 socket.io 客户端分发包中也提供了 socket.io.msgpack.min.js)。
关键一点:每个 Socket.IO 包都是作为一个 Engine.IO message 包(类型 4)发送的,因此在真正走线时(HTTP long-polling 的请求/响应体,或 WebSocket 帧中),编码结果前面会多一个前缀 4。
编码格式
<packet type>[<# of binary attachments>-][<namespace>,][<acknowledgment id>][JSON-stringified payload without binary]
+ binary attachments extracted
注意:命名空间仅在它不同于默认命名空间(/)时才被包含。
编码示例
- 默认命名空间的
CONNECT包
{ "type": 0, "nsp": "/" }
编码为 0。
/admin命名空间的CONNECT包
{ "type": 0, "nsp": "/admin" }
编码为 0/admin,。
/admin命名空间的DISCONNECT包
{ "type": 1, "nsp": "/admin" }
编码为 1/admin,。
EVENT包
{ "type": 2, "nsp": "/", "data": ["hello", 1] }
编码为 2["hello",1]。
- 带 acknowledgment id 的
EVENT包
{ "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>。
- 带 acknowledgment id 的
BINARY_EVENT包
{ "type": 5, "nsp": "/admin", "data": ["project:delete", <Buffer 01 02 03>], "id": 456 }
编码为 51-/admin,456["project:delete",{"_placeholder":true,"num":0}] + <Buffer 01 02 03>。
BINARY_ACK包
{ "type": 6, "nsp": "/admin", "data": [<Buffer 03 02 01>], "id": 456 }
编码为 61-/admin,456[{"_placeholder":true,"num":0}] + <Buffer 03 02 01>。
编码在源码中的实现
上述编码规则在参考实现中由 Encoder.encodeAsString 逐段拼出,顺序与文档完全一致:先写类型,再写(若有)附件数与 -,再写(若非 / 的)命名空间与逗号,再写(若有)id,最后写 JSON 字符串化的 payload。见 encodeAsString 实现:
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);
}
反向解码则由 Decoder.decodeString 负责,它按同样顺序解析类型、附件数、命名空间、id 与 JSON payload,并对非法输入抛出 unknown packet type、Illegal attachments、invalid payload 等错误,见 decodeString 实现。
二进制的"占位符"机制
文档示例中反复出现的 {"_placeholder":true,"num":0} 并非神秘语法,而是编码二进制包时的一种"提取"技巧:编码端先用带编号的占位符替换 payload 中的每一个二进制对象,把二进制数据本身抽出来单独附在字符串后面发送;解码端再按编号回填。
这一逻辑由 deconstructPacket / reconstructPacket 实现,见 binary.ts。其中 deconstructPacket 递归遍历 payload,遇到二进制就生成 { _placeholder: true, num: 序号 } 并把它 push 进缓冲区列表、记录 attachments 计数;reconstructPacket 则在收齐全部二进制块后,按 num 将缓冲区放回占位符位置。判定"什么算二进制"(ArrayBuffer、其视图、Blob、File)则封装在 is-binary.ts 的 isBinary / hasBinary 中。
解码侧还有一处值得注意的健壮性设计:Decoder 通过一个 maxAttachments(默认 10)上限来拒绝过大的附件声明,见 DecoderOptions 与解码。
交互协议(Exchange protocol)
连接默认命名空间
在连接建立时,服务端总是先发送一个默认命名空间(/)的 CONNECT 包。 也就是说,即便客户端请求的是非默认命名空间,它也会先收到默认命名空间的 CONNECT 包。
Server > { type: CONNECT, nsp: "/" }
此时不期望客户端有任何响应。
这是 v4 的标志性行为,也是 v5 的主要改动点:v5 取消了"隐式连接默认命名空间",要求客户端在任何情况下都主动发送
CONNECT包。阅读当前仓库代码时不会看到"服务端先自发默认 CONNECT"的行为,这正是 v5 的体现。
连接非默认命名空间
Client > { type: CONNECT, nsp: "/admin" }
Server > { type: CONNECT, nsp: "/admin" } (如果连接成功)
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 }
or
Server > { type: BINARY_ACK, nsp: "/admin", data: [ <Buffer 01 02 03> ], id: 456 }
反过来同理。
样例会话(Sample session)
下面展示当 Engine.IO 与 Socket.IO 两层协议叠加时,线上实际传输的完整过程(对应 v4 的 EIO=3)。
- 请求 1(open 包)
GET /socket.io/?EIO=3&transport=polling&t=N8hyd6w
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=UTF-8
96:0{"sid":"lv_VI97HAXpY6yYWAAAC","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000}2:40
细节:
96 => 首条消息的字符数(不是字节数)
: => 分隔符
0 => Engine.IO "open" 包类型
{"sid":... => Engine.IO 握手数据
2 => 第 2 条消息的字符数
: => 分隔符
4 => Engine.IO "message" 包类型
0 => Socket.IO "CONNECT" 包类型
注意:t 查询参数用于确保该请求不被浏览器缓存。
- 请求 2(message in)
在服务端执行 socket.emit('hey', 'Jude'):
GET /socket.io/?EIO=3&transport=polling&t=N8hyd7H&sid=lv_VI97HAXpY6yYWAAAC
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=UTF-8
16:42["hey","Jude"]
细节:
16 => 字符数
: => 分隔符
4 => Engine.IO "message" 包类型
2 => Socket.IO "EVENT" 包类型
[...] => 内容
- 请求 3(message out)
在客户端执行 socket.emit('hello'); socket.emit('world');:
POST /socket.io/?EIO=3&transport=polling&t=N8hzxke&sid=lv_VI97HAXpY6yYWAAAC
> Content-Type: text/plain; charset=UTF-8
11:42["hello"]11:42["world"]
< HTTP/1.1 200 OK
< Content-Type: text/plain; charset=UTF-8
ok
细节:
11 => 第 1 个包的字符数
: => 分隔符
4 => Engine.IO "message" 包类型
2 => Socket.IO "EVENT" 包类型
["hello"] => 第 1 个内容
11 => 第 2 个包的字符数
: => 分隔符
4 => Engine.IO "message" 包类型
2 => Socket.IO "EVENT" 包类型
["world"] => 第 2 个内容
- 请求 4(WebSocket 升级)
GET /socket.io/?EIO=3&transport=websocket&sid=lv_VI97HAXpY6yYWAAAC
< HTTP/1.1 101 Switching Protocols
WebSocket 帧:
< 2probe => Engine.IO probe 请求
> 3probe => Engine.IO probe 响应
> 5 => Engine.IO "upgrade" 包类型
> 42["hello"]
> 42["world"]
> 40/admin, => 请求访问 admin 命名空间(Socket.IO "CONNECT" 包)
< 40/admin, => 授予 admin 命名空间访问权
> 42/admin,1["tellme"] => 带确认的 Socket.IO "EVENT" 包
< 461-/admin,1[{"_placeholder":true,"num":0}] => 带占位符的 Socket.IO "BINARY_ACK" 包
< <binary> => 二进制附件(在下一帧发送)
... 一段时间无消息后
> 2 => Engine.IO "ping" 包类型
< 3 => Engine.IO "pong" 包类型
> 1 => Engine.IO "close" 包类型
协议历史
v4 与 v3 的区别
- 新增
BINARY_ACK包类型。
在此之前,ACK 包总被当作"可能含二进制对象"来处理,需要递归搜索这类对象,可能拖累性能。v4 引入独立的 BINARY_ACK 类型后,普通 ACK 与二进制 ACK 各司其职,避免了无谓的递归扫描。
v3 与 v2 的区别
- 移除了使用 msgpack 来编码含二进制对象的包。
v2 与 v1 的区别
- 新增
BINARY_EVENT包类型。
这是在向 Socket.IO 1.0 推进期间加入的,目的是支持二进制对象。当时的 BINARY_EVENT 包使用 msgpack 编码。
初次修订
第一次修订是 Engine.IO 协议(处理 WebSocket / HTTP long-polling 与心跳的低层管道)与 Socket.IO 协议分离的结果。它从未被纳入任何 Socket.IO 正式发布版本,却为后续迭代铺平了道路。
与当前仓库实现的对照
阅读当前仓库代码时,可以把 v4 的这些规范当作"祖源"来理解 v5。几处可直接对照的证据:
- 包类型编号一脉相承。当前 PacketType 枚举 中
CONNECT=0、DISCONNECT=1、EVENT=2、ACK=3、CONNECT_ERROR=4、BINARY_EVENT=5、BINARY_ACK=6,与 v4 完全对齐,唯一变化是把 v4 的ERROR更名为CONNECT_ERROR。 - 编码/解码格式不变。前缀
4、命名空间逗号分隔、二进制占位符与附件计数,这些在 encodeAsString / decodeString 中仍然生效。 - 错误包的载荷形态升级。v4 的
ERROR载荷是纯字符串(如"Not authorized"),v5 改为对象{ message, data }。当前服务端在下发CONNECT_ERROR时会按连接所用协议版本区分载荷形态——对protocol === 3(即旧版本)走字符串分支,否则走对象分支,见 namespace.ts 的错误处理。 - 默认命名空间不再隐式连接。v4 "服务端总是先发默认命名空间
CONNECT"的行为在 v5 中已被移除,客户端需显式发送CONNECT。 - 对旧 Engine.IO(EIO=3)客户端的兼容。当前服务端仍保留对 EIO3 客户端的支持,可通过
allowEIO3选项开启,相关行为见 v2 兼容性测试。
这些对照说明:v4 文档描述的包结构与编码格式是 Socket.IO 协议长期稳定的内核,即便协议修订号推进到 v5,理解 v4 仍能帮助你准确解读线上字节流。
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