首页
/ 深入解析 socket.io-parser:Socket.IO 协议 v5 的编解码参考实现

深入解析 socket.io-parser:Socket.IO 协议 v5 的编解码参考实现

2026-09-04 22:07:53作者:邓越浪Henry

socket.io-parser 是 Socket.IO 体系中负责报文层编解码的参考实现,它定义了客户端与服务端如何在一条 WebSocket 或 HTTP 长轮询连接上表示"事件、确认、连接、二进制附件"等语义。本文以 packages/socket.io-parser/Readme.md 的说明为骨架,结合 lib/index.tslib/binary.ts 源码与 docs/socket.io-protocol/v5-current.md 协议规范,完整讲清其 Encoder/Decoder API、线上报文格式、二进制占位符机制与 payload 校验规则,读完你可以独立阅读抓包结果并定位协议层问题。

1. 包定位与版本兼容

按 Readme 的描述,socket.io-parser 是一个符合 Socket.IO 协议 v5 的 JavaScript 编解码器,被 socket.io 服务端与 socket.io-client 直接依赖使用。Readme 给出的兼容性对照表是:

Parser 版本 Socket.IO 服务端版本 协议修订版
3.x 1.x / 2.x 4
4.x 3.x 5

当前仓库中该包的版本为 4.2.7(见 package.json),对应协议第 5 修订版。源码中也能直接找到协议版本常量:

// lib/index.ts
export const protocol: number = 5;

包只依赖两个运行时库:@socket.io/component-emitter(事件发射器,Decoder 即基于它)与 debug(调试日志)。发布形态为 CJS/ESM 双入口:main 指向 ./build/cjs/index.jsmodule/exports.import 指向 ./build/esm/index.js,浏览器端 Node 环境还会优先使用带 debug 日志的 build/esm-debug 构建(均在 package.jsonexports 字段中声明)。

需要特别说明的是:Readme 中的示例代码沿用了旧版(回调式 encoder.encode(packet, cb))的 API 形态。当前 v5 参考实现的 Encoder.encode()同步方法,返回字符串或"字符串 + 二进制缓冲"数组;且 EVENT/ACK 包本身不再携带 id 字段(事件名移入 data 数组首位,与 v5 协议文档 一致)。因此下文的示例均以当前源码为准,并给出等价于 Readme 示例的用法。

2. 包类型:PacketType 枚举与协议 v5 的对应关系

编解码的基本单元是"包"(Packet)。lib/index.ts 中定义了类型枚举与包结构:

export enum PacketType {
  CONNECT,        // 0
  DISCONNECT,     // 1
  EVENT,          // 2
  ACK,            // 3
  CONNECT_ERROR,  // 4
  BINARY_EVENT,   // 5
  BINARY_ACK,     // 6
}

export interface Packet {
  type: PacketType;
  nsp: string;
  data?: any;
  id?: number;
  attachments?: number; // 仅用于二进制包在编码/解码的中间态
}

这套编号与 v5 协议文档 的包类型表完全一致:

类型 ID 用途
CONNECT 0 连接某个命名空间(namespace)
DISCONNECT 1 断开某个命名空间
EVENT 2 向对端发送数据
ACK 3 确认(acknowledge)一个事件
CONNECT_ERROR 4 连接被拒绝
BINARY_EVENT 5 携带二进制数据的事件
BINARY_ACK 6 携带二进制数据的确认

相对旧修订版,v5 有几处值得注意的语义变化(见协议文档 History 一节):

  • 取消了"默认命名空间的隐式连接",客户端无论连接哪个命名空间都必须先发送 CONNECT 包;
  • ERROR 类型改名为 CONNECT_ERROR(编号 4 不变);
  • CONNECT 包现在可以携带 payload(用于鉴权,如 { "token": "123" }),服务端成功响应会携带 Socket.IO 层的会话 ID(sid),因此 Socket.IO 会话 ID 与底层 Engine.IO 连接 ID 从此不同。

3. Encoder API:把一个 Packet 编成线上字符串

Encoder 的核心入口是同步方法 encode(obj: Packet)lib/index.ts#L63-L80):

public encode(obj: Packet) {
  // EVENT / ACK 且 payload 含二进制时,改写为 BINARY_EVENT / BINARY_ACK
  if (obj.type === PacketType.EVENT || obj.type === PacketType.ACK) {
    if (hasBinary(obj)) {
      return this.encodeAsBinary({ type: /* 5 或 6 */, nsp, data, id });
    }
  }
  return [this.encodeAsString(obj)];
}

调用方只需传入 EVENT 类型的包并携带二进制数据,解析器会自动把类型升级为 BINARY_EVENT 并拆出二进制附件——这正是 Readme 中 "encoding and decoding a packet with binary data" 示例对应的能力。

encodeAsStringlib/index.ts#L86-L116)按如下顺序拼装字符串:

  1. 类型编号:包的类型 ID,一个字符;
  2. 附件计数:仅 BINARY_EVENT/BINARY_ACK 有,格式 <attachments>-
  3. 命名空间:仅当不是默认命名空间 / 时追加,格式 <nsp>,
  4. 确认 ID:非 null 时追加(当前 v5 中主要用于 CONNECT/CONNECT_ERROR 之外的内部场景,见第 4 节格式);
  5. JSON 序列化后的 payload:使用可选的 replacer 传给 JSON.stringify

Encoder 构造函数接受一个可选的 replacer(透传给 JSON.stringify),可自定义序列化行为。

3.1 编解码一个普通事件包

对应 Readme "Encoding and decoding a packet" 示例的现代写法:

const { PacketType, Encoder, Decoder } = require("socket.io-parser");

const encoder = new Encoder();
const packet = {
  type: PacketType.EVENT,
  nsp: "/",
  data: ["test-packet", "arg1"],
};

const encodedPackets = encoder.encode(packet); // 同步返回 ["2[\"test-packet\",\"arg1\"]"]

const decoder = new Decoder();
decoder.on("decoded", (decodedPacket) => {
  // decodedPacket.type === PacketType.EVENT
  // decodedPacket.data === ["test-packet", "arg1"]
});
for (let i = 0; i < encodedPackets.length; i++) {
  decoder.add(encodedPackets[i]);
}

注意 v5 中事件名是 data 数组的第一个元素(协议文档规定 EVENT payload 必须是非空数组),Readme 旧示例中的独立 id: 13 字段在当前实现里对应的是 CONNECT/CONNECT_ERROR 等包的确认语义。

3.2 编解码一个带二进制的包

对应 Readme 第二个示例(二进制数据版本)。Node 环境下:

const { PacketType, Encoder, Decoder } = require("socket.io-parser");

const encoder = new Encoder();
const packet = {
  type: PacketType.EVENT,          // 会被自动升级为 BINARY_EVENT
  nsp: "/",
  data: ["upload", { i: Buffer.alloc(1234) }],
};

// encode 返回 [ "51-[\"upload\",{...占位符...}]", <Buffer ...> ]
const encodedPackets = encoder.encode(packet);

const decoder = new Decoder();
decoder.on("decoded", (decodedPacket) => {
  // decodedPacket.type === PacketType.EVENT(解码后还原为非二进制类型)
  // Buffer.isBuffer(decodedPacket.data[1].i) === true
});
for (const chunk of encodedPackets) {
  decoder.add(chunk);
}

浏览器端 ArrayBuffer/Blob/File 同样受支持,lib/is-binary.tsisBinary() 会按运行环境探测 ArrayBuffer(含其 View)、BlobFilehasBinary() 则递归遍历数组/对象(必要时走 toJSON())判断是否存在二进制。

4. 线上格式(Wire Format)与真实报文样例

lib/index.ts 的拼装逻辑对应协议文档中定义的通用格式:

<packet type>[<# of binary attachments>-][<namespace>,][<acknowledgment id>][JSON-stringified payload without binary]
+ binary attachments extracted

即:类型编号 [附件数-] [命名空间,] [确认ID] [去除二进制后的 JSON payload] + 抽取出的二进制附件,其中命名空间仅在非 / 时出现。结合协议文档的编码示例,常见报文长这样(注意:每个 Socket.IO 包在传输时还会被包进 Engine.IO 的 message 包,线上实际会多一个前缀字符 4):

包对象 编码结果
{ type: CONNECT, namespace: "/" } 0
{ type: CONNECT, namespace: "/admin", data: { sid: "oSO0OpakMV_3jnilAAAA" } } 0/admin,{"sid":"oSO0OpakMV_3jnilAAAA"}
{ type: CONNECT_ERROR, namespace: "/", data: { message: "Not authorized" } } 4{"message":"Not authorized"}
{ type: EVENT, namespace: "/", data: ["foo"] } 2["foo"]
{ type: EVENT, namespace: "/admin", data: ["bar"] } 2/admin,["bar"]
{ type: BINARY_EVENT, namespace: "/", data: ["baz", <Buffer 01 02 03 04>] } 51-["baz",{"_placeholder":true,"num":0}] + <Buffer 01 02 03 04>
{ type: BINARY_EVENT, namespace: "/admin", data: ["baz", <Buffer 01 02>, <Buffer 03 04>] } 52-/admin,["baz",{...num:0},{...num:1}] + 两个二进制块
{ type: EVENT, namespace: "/", data: ["foo"], id: 12 } 212["foo"]
{ type: DISCONNECT, namespace: "/" } 1
{ type: DISCONNECT, namespace: "/admin" } 1/admin,

一份完整的握手样例(长轮询 + WebSocket 升级)在 v5-current.md 的 "Sample session" 一节给出,例如请求阶段 40(Engine.IO message + Socket.IO CONNECT)、应答 40{"sid":"wZX3oN0bSVIhsaknAAAI"}、事件下发 42["hey","Jude"],以及二进制确认帧 461-/admin,1[{"_placeholder":true,"num":0}] 之后紧跟的二进制帧。如果你抓包看到 51-...4... 这类前缀,可以直接用上述格式逆向解析。

5. 二进制包:占位符、拆包与重组

二进制支持是 parser 最核心的部分,实现在 lib/binary.ts

拆包(编码侧)deconstructPacket() 递归遍历 payload,把每个 Buffer/ArrayBuffer/Blob/File 替换为形如 { _placeholder: true, num: <索引> } 的占位符,并把真实二进制依次收集进 buffers 数组;同时把 attachments 字段设为二进制总数。Encoder.encodeAsBinarylib/index.ts#L124-L131)随后把字符串包放在数组最前面,输出 ["51-[...占位符...]", <bin1>, <bin2>, ...]

重组(解码侧)Decoder.add() 收到字符串后(lib/index.ts#L181-L212):

  • 若解析出 BINARY_EVENT/BINARY_ACK,先把类型还原为 EVENT/ACK,并创建 BinaryReconstructor 实例保存该包与待收附件数;
  • 随后每收到一段原始二进制,就调用 reconstructor.takeBinaryData(binData)lib/index.ts#L359-L368)累积;
  • 当累积数量等于 attachments 时,reconstructPacket() 按占位符 num 把缓冲填回原位置(并删除 attachments 字段),触发 decoded 事件。

reconstructPacket 会对占位符索引做边界校验(num 必须是 [0, buffers.length) 内的整数),否则抛出 illegal attachmentsDecoder.destroy() 会在连接断开时清理未完成的重组状态(调用 finishedReconstruction()),且销毁后 Decoder 可以继续解码新的非二进制包,test/parser.js 中 "should resume decoding after calling destroy()" 用例验证了这一点。

6. 校验与防御:payload 合法性、保留事件名与 maxAttachments

parser 不只是格式转换,还内置了相当严格的输入校验,这些规则都由测试固化(test/parser.js):

payload 与包类型的匹配isPayloadValidlib/index.ts#L301-L321):

包类型 合法 payload
CONNECT 纯对象(isObject
DISCONNECT 必须为 undefined(不允许携带数据)
CONNECT_ERROR 字符串或对象
EVENT / BINARY_EVENT 数组,且首元素为数字(整数事件名)或非保留的字符串事件名
ACK / BINARY_ACK 数组

不符合时 decodeString 抛出 invalid payload。测试覆盖了诸如 2/admin, 后跟对象、2["connect"](保留事件名作为事件名)、2[true,"foo"](首元素非字符串/数字)等非法输入。

保留事件名lib/index.ts#L11-L18 定义了 RESERVED_EVENTS = ["connect", "connect_error", "disconnect", "disconnecting", "newListener", "removeListener"],注释明确说明这些字符串有框架层特殊语义,不能作为业务事件名——解码器会把 2["connect"] 直接判为非法 payload。

附件数上限(DoS 防护)DecoderOptions 提供 maxAttachments 选项,默认 10lib/index.ts#L140-L150)。解码 BINARY_EVENT/BINARY_ACK 时会校验附件计数:不是正整数、或超过上限时分别抛出 Illegal attachments / too many attachments。测试 "throws an error when receiving too many attachments" 用 new Decoder({ maxAttachments: 2 }) 配合 3 个占位符的包验证了该行为。

未知输入:类型编号超出 0–6 抛 unknown packet type Nadd() 收到既非字符串也非二进制/ base64 的内容时抛 Unknown type: ...;正在重组二进制包期间突然收到纯文本,会抛 got plaintext data when reconstructing a packet——这类"状态不一致"错误在排查多路复用连接的粘包/串包问题时很有用。

自定义 reviverDecoder 构造函数支持函数或 { reviver, maxAttachments } 两种传参方式(lib/index.ts#L164-L173),reviver 透传给 JSON.parse,可用于反序列化时的自定义还原(如还原 Date)。测试 "decodes with a custom reviver" 演示了将键 a 的值转大写的用法。

发送侧合法性:除解码校验外,模块还导出 isPacketValid(packet)lib/index.ts#L425-L431),按"命名空间必须是字符串、确认 ID 必须是整数或 undefined、data 与类型匹配"三条规则检查待发送包,服务端/客户端可用它在编码前快速自检。

7. 测试套件如何验证编解码

test/ 目录(test/index.js)按运行环境动态装配用例:浏览器支持 Blob 时加载 blob.js,存在 ArrayBuffer 时加载 arraybuffer.js,Node 环境加载 buffer.js,共同复用 test/helpers.jstest(单包编解码往返)与 test_bin(二进制包逐段喂给 decoder.add)两个辅助函数。

典型验证路径为:encoder.encode(obj) → 依次 decoder.add(encoded[i]) → 在 decoded 事件中断言还原出的包与原始包 eqltest/parser.js 还额外覆盖了:

  • 各包类型(connect / disconnect / event / ack / connect error 的字符串与对象两种形态)的往返编码;
  • 循环引用对象在 JSON.stringify 时抛出异常;
  • 第 6 节列出的全部非法输入;
  • destroy() 后恢复解码。

运行方式(在 packages/socket.io-parser 目录下):

npm run compile        # 先编译 CJS/ESM 构建
npx mocha --reporter dot --bail test/index.js   # 即 package.json 中的 test:node

package.jsontest 脚本会在 BROWSERS=1 时切换到 WebdriverIO 跑浏览器用例(wdio,配置见 wdio.conf.js)。

8. 在整体架构中的位置与扩展点

从源码结构看,socket.io-parser 位于协议栈的中间层:它只负责 Socket.IO 报文(06 类型字符 + JSON + 二进制附件),底层的传输细节(WebSocket 升级、长轮询、心跳 ping/pong)由 Engine.IO 协议承担——协议文档中每条线上报文都带有 Engine.IO message 前缀字符 4,正是这一层叠放的直接体现。

由此可以推断出两类扩展方向:

  1. 自定义解析器:由于编解码被抽象成独立的 Encoder/Decoder 形态,服务端与客户端支持替换默认的 JSON 解析器。协议文档提到的例子包括面向 JSON 严格模式的 socket.io-json-parser 与面向二进制压缩的 socket.io-msgpack-parser;本仓库 client-dist 目录 中也预置了 socket.io.msgpack.min.js 客户端构建,examples/custom-parsers/ 目录则演示了自定义 parser 的接入方式。
  2. 协议兼容性测试:仓库 docs/socket.io-protocol/v5-test-suite/ 提供了协议 v5 的合规测试套件(Node 下 npm ci && npm test,浏览器直接打开 index.html),任何第三方 Socket.IO 服务端实现都可以用它自检;而 socket.io-parser 本身就是该协议的参考实现,二者互为印证。

9. 小结

  • socket.io-parser(当前 4.2.7)是 Socket.IO 协议 v5 的参考编解码实现,同步的 Encoder.encode() 与基于事件的 Decoder.add() 构成核心 API;
  • 线上格式为 <type>[<attachments>-][<nsp>,][<id>][JSON payload] + 二进制附件,非默认命名空间才写入 nsp,每个包外层再包一个 Engine.IO 4 前缀;
  • 二进制数据通过 {_placeholder: true, num} 占位符 + 附件计数实现"字符串包打头、二进制块随后"的传输,BinaryReconstructor 负责按索引重组;
  • 防御性规则(保留事件名、maxAttachments 默认 10、payload 与类型严格匹配、isPacketValid)使其可直接暴露在不可信网络输入下使用。

延伸阅读:packages/socket.io-parser/Readme.mdlib/index.tslib/binary.tsdocs/socket.io-protocol/v5-current.mdtest/parser.js

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
527
590
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
904
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
889
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.52 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.33 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
981
502
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384