siocat:像 wscat 一样调试 Socket.IO

如果你写过 Node.js 实时应用,大概率用过 wscat 调试 WebSocket。但如果服务端用的是 Socket.IO,wscat 就只能看到 40、42["event",data] 这样的 Engine.IO 封包,既不能直接发 Socket.IO 事件,也搞不定命名空间、ACK 回调和握手认证。

siocat 就是为这个问题做的:一个类似 wscat 的 Socket.IO 终端客户端,默认是一个带消息流、事件列表和命令菜单的 TUI,也可以退化成纯命令行模式。它支持 Socket.IO v2、v3、v4,能自动捕获服务端推来的任意事件,输入载荷还支持 JSON5。

一、为什么不用 wscat

Socket.IO 并不是标准 WebSocket。它在 WebSocket 之上又封了一层自己的协议:

  • Engine.IO 握手:连接建立前有 EIO=3 / EIO=4 这样的协议版本握手,还有 ping / pong 心跳。
  • Socket.IO 封包:42["event", data]、43["event", data, ackId] 这类封包,事件名、参数、ACK id 都是协议的一部分。
  • 命名空间与 ACK:namespace、auth、事件回调、二进制附件,这些都不是裸 WebSocket 的概念。
  • 版本差异:Socket.IO 2.x / 1.x 用的是 Engine.IO v3,3.x / 4.x 用的是 Engine.IO v4,握手参数和封包编码都不一样。

所以调试 Socket.IO 时,真正需要的是一个“懂 Socket.IO 协议栈”的终端客户端,而不是一个只会收发 WebSocket 文本的小工具。

二、siocat 是什么

一句话概括:siocat = wscat 的交互方式 + Socket.IO 协议栈 + TUI 仪表盘。

它有两种运行模式:

  • TUI 模式(默认):一个双栏仪表盘,左边是实时消息流,右边是已捕获事件列表和会话检查器,底部是可编辑的命令输入框。
  • CLI 模式(--no-tui):一个带彩色输出的交互式 Readline 会话,适合脚本、CI 或者不喜欢全屏 TUI 的场景。
1
2
3
4
5
siocat http://localhost:3000
│
├─ TUI:消息流 / 事件列表 / 会话检查器 / 命令菜单 / 行编辑器
│
└─ CLI:readline 交互 + 彩色事件输出

项目目前的结构很轻:

1
2
3
4
5
6
7
8
9
10
11
siocat/
├── bin/siocat.js # 命令行入口
├── lib/
│ ├── socket-client.js # Socket.IO 客户端封装、事件捕获、ACK
│ ├── commands.js # emit / ack / filter / stats 等命令
│ ├── tui.js # blessed TUI 仪表盘
│ ├── console-ui.js # 纯命令行模式
│ ├── line-editor.js # 带光标和语法高亮的行编辑器
│ └── format.js # 时间戳、载荷格式化、语法高亮
├── mock-server/ # 全协议兼容的测试服务端
└── test/ # Jest 测试

三、快速开始

项目还没有发布到 npm,直接 clone 下来本地跑即可。要求 Node.js >= 16。

1
2
3
4
5
6
7
8
9
10
git clone https://github.com/bubao/siocat.git
cd siocat
npm install

# 方式一:直接运行
node bin/siocat.js http://localhost:3000

# 方式二:注册为全局命令
npm link
siocat http://localhost:3000

几个常用连接示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 连接指定命名空间
siocat http://localhost:3000 -n /chat

# 连接 Socket.IO v2 服务端
siocat http://localhost:3000 -p 2

# 携带握手认证信息(支持 JSON5)
siocat http://localhost:3000 --auth '{ token: "my-jwt-token", env: "dev" }'

# 自定义请求头与查询参数
siocat http://localhost:3000 -H "Authorization: Bearer xyz" -q "userId=1001"

# 强制只用 WebSocket 传输
siocat http://localhost:3000 --transports websocket

# 纯命令行模式
siocat http://localhost:3000 --no-tui

四、TUI:一个仪表盘,而不只是一个输入框

TUI 是 siocat 最像“现代终端应用”的部分。它没有把所有命令都塞进帮助文本,而是在界面上留了足够的可视化反馈。

1. 实时消息流

所有上下行事件都会进入消息流,带毫秒级时间戳和状态徽标:

  • ▼ IN:服务端推来的事件
  • ▲ OUT:客户端发出的事件
  • ✓ ACK:服务端返回的 ACK
  • * SYS:连接、断开、重连、传输升级等系统消息
  • ✖ ERR:连接或发送错误

长文本会自动换行,鼠标滚轮可以滚动查看历史消息。

2. 已捕获事件列表

服务端推来了哪些事件,右侧列表会直接列出来。选中一个事件后按 Enter,或者用鼠标点击,就能以这个事件名预填一个 emit 表单,不用自己手敲事件名。

这个能力来自 Socket.IO 客户端的 wildcard / catch-all 机制:v3 / v4 用 onAny / onAnyOutgoing,v2 则通过 onevent 和 emit 做兼容 hook。所以不需要预先注册监听,服务端推什么都能看到。

3. 会话检查器

TUI 会实时显示当前连接的关键信息:

  • 连接状态(Connected / Disconnected)
  • 传输协议(websocket / polling)
  • Socket ID
  • 运行时长
  • 收包 / 发包数量
  • ACK 次数

排查“到底是连上了没有”“现在是走 WebSocket 还是轮询”这类问题时,不用再去翻日志。

4. 命令菜单与行编辑器

底部输入框集成了一组常用命令,按数字键 1 ~ 7 就能唤起对应表单:emit、ack、filter、stats、help 等。行编辑器本身支持:

  • ← / →、Home / End、鼠标点击定位光标
  • Ctrl+A / Ctrl+E 跳到行首 / 行尾
  • Ctrl+U / Ctrl+K 删除光标前 / 后的内容
  • Ctrl+W 删除前一个单词
  • ↑ / ↓ 翻阅历史命令
  • 实时语法高亮:命令名、事件名、键名、字符串、数字、布尔值颜色都不同

几个常用快捷键:

快捷键功能
Tab在命令菜单、输入框、消息视窗、事件列表之间循环切换焦点
1 ~ 7直接触发命令菜单中的操作
Enter(事件列表中)用选中事件名快速打开 emit 表单
F1 / ?打开 / 关闭帮助浮窗
F2打开实时过滤器
Ctrl+L清空消息视窗
Ctrl+C / :q退出

五、交互命令

无论 TUI 还是 CLI 模式,都可以直接输入这些命令:

命令说明
emit / send <event> [payload]发送一个事件
ack <event> [payload]发送事件并等待服务端 ACK 回调
filter [pattern]按正则或关键字过滤消息流,留空清除
stats / info查看连接详情和事件统计
clear / cls清空消息记录
help / ?查看帮助
exit / quit / :q断开连接并退出

示例:

1
2
3
4
5
6
emit chatMessage { room: "dev", text: "hello" }
ack queryStatus { id: 123 }
filter chat
filter /^order:/
stats
clear

六、宽松载荷输入

调试实时服务时,载荷格式往往很琐碎。siocat 在输入侧做了几层兼容:

  • JSON5:键名可以不加引号,也支持单引号字符串,写起来更像“草稿”。
  • 纯字符串:不是合法 JSON5 时,会退化成原始字符串。
  • 二进制前缀:hex: 和 base64: 可以直接发送 Buffer。
  • ACK 超时:默认 5000ms,可以用 --ack-timeout 调整。
1
2
3
4
5
6
emit joinRoom { room: "general", user: "alice" }
emit userLogin { name: 'alice', age: 20 }
emit ping "hello"
emit avatar hex:89504e470d0a1a0a
emit file base64:SGVsbG8gV29ybGQ=
ack queryStatus { id: 123 }

ACK 消息里还会计算往返耗时,方便判断服务端处理是不是有延迟。

七、多协议兼容

Socket.IO 的兼容问题是这个项目最实际的部分。服务端版本和客户端 SDK 版本如果对不上,常见现象是“能建立连接但握手失败”或者“客户端一直重连”。

siocat 通过 npm alias 同时装了三个版本的客户端:

1
2
3
"socket.io-client-2": "npm:socket.io-client@^2.5.0",
"socket.io-client-3": "npm:socket.io-client@^3.1.3",
"socket.io-client-4": "npm:socket.io-client@^4.8.1"

运行时根据 -p 参数动态加载:

1
2
3
4
5
6
7
8
switch (version) {
case 2:
return { io: require('socket.io-client-2'), version: 2 };
case 3:
return { io: require('socket.io-client-3'), version: 3 };
case 4:
return { io: require('socket.io-client-4'), version: 4 };
}

选参数时可以按这张表:

目标服务端推荐参数说明
Socket.IO 4.x默认 / -p 4Engine.IO v4,Socket.IO protocol v5
Socket.IO 3.x-p 3 或 -p 43.x 与 4.x 底层协议一致,通常都能互通
Socket.IO 2.x / 1.x-p 2Engine.IO v3,Socket.IO protocol v4
开启 allowEIO3: true 的 4.x任意服务端向下兼容旧版握手

一个容易踩的坑:-p 指的是 Socket.IO 客户端 SDK 的大版本,不是 Engine.IO 版本。所以看到 EIO=3 / EIO=4 时不要直接照抄,要对应到客户端的 -p 2 或 -p 3/4。

八、实现上值得看的几个点

1. 自动捕获任意事件

v3 / v4 的原生 wildcard API 之外,v2 没有 onAny,所以 siocat 直接 hook 了 onevent 和 emit:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (version >= 3) {
socket.onAny((event, ...args) => this._recordInEvent(event, args));
socket.onAnyOutgoing((event, ...args) => {
const displayArgs = args.filter((a) => typeof a !== 'function');
this._recordOutEvent(event, displayArgs);
});
} else {
const origOnevent = socket.onevent.bind(socket);
socket.onevent = (packet) => {
const [event, ...args] = packet.data || [];
if (event) this._recordInEvent(event, args);
return origOnevent(packet);
};

const origEmit = socket.emit.bind(socket);
socket.emit = (event, ...args) => {
const displayArgs = args.filter((a) => typeof a !== 'function');
this._recordOutEvent(event, displayArgs);
return origEmit(event, ...args);
};
}

这样消息流里看到的是已经解析过的事件名和参数,而不是原始封包。

2. ACK 等待与超时

ack 命令不是简单 emit 一下就算了,它会把 ACK 回调包成一个 Promise,并设置超时:

1
2
3
4
5
6
7
8
9
10
11
12
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`ACK timed out after ${timeout}ms for event: ${event}`));
}, timeout);

const ackCallback = (...responseArgs) => {
clearTimeout(timer);
resolve(responseArgs.length <= 1 ? responseArgs[0] : responseArgs);
};

this.socket.emit(event, data, ackCallback);
});

控制台里会同时显示响应内容和往返耗时,排查 ACK 延迟很直观。

3. 兼容 v2 的请求头

Socket.IO v2 客户端没有 extraHeaders,要传自定义 HTTP 头,得走 transportOptions.polling.extraHeaders。siocat 在 connect() 里按版本分支处理:

1
2
3
4
5
6
7
8
9
10
11
12
if (version >= 3) {
if (Object.keys(this.headers).length > 0) {
socketOpts.extraHeaders = this.headers;
}
if (this.auth) {
socketOpts.auth = this.auth;
}
} else {
socketOpts.transportOptions = {
polling: { extraHeaders: this.headers }
};
}

这类“同一件事,两个版本两套 API”的细节,正是 Socket.IO 调试工具真正需要处理的地方。

九、本地测试

项目自带一个同时支持 v2 / v3 / v4 的 mock server,服务端开启了 allowEIO3: true,所以旧版客户端也能连上:

1
2
3
4
5
# 运行 Jest 测试
npm test

# 启动 mock server(监听 3000 端口)
npm run test-server

然后另开一个终端连接:

1
2
3
4
5
6
7
8
# 默认 v4
node bin/siocat.js http://localhost:3000

# 用 v2 客户端连接
node bin/siocat.js http://localhost:3000 -p 2

# 纯 CLI 模式
node bin/siocat.js http://localhost:3000 --no-tui

十、参考资料与延伸阅读

十一、小结

调试 Socket.IO 时,最常用的动作其实就几个:连接、看事件、发事件、等 ACK、过滤噪音、确认当前传输和协议。siocat 做的事情就是把这些动作收进一个终端界面里,同时补上 Socket.IO 特有的协议细节。

它现在还没发布到 npm,作为本地开发工具使用,npm install 之后 node bin/siocat.js 就能跑。项目在 GitHub,MIT 协议,欢迎 issue 和 PR。


siocat:像 wscat 一样调试 Socket.IO
https://bubao.github.io/posts/cee08f9c.html
作者
一念
发布于
2026年11月7日
许可协议