Socket.IO — 用 Engine.IO 传输层承载命名空间事件
已复核Socket.IO 是一个建立在 Engine.IO 之上的实时事件框架。日常类比:Engine.IO 负责把电话线路接通并在线路不好时换通道;Socket.IO 负责会议号(namespace)、房间和“说一句等回执”。
你写:
import { Server } from "socket.io";
const io = new Server(3000);io.on("connection", (socket) => { socket.on("chat", (msg) => io.emit("chat", msg));});固定 4.8.3 会先挂 Engine.IO,默认路径 /socket.io,再把底层连接包成 Client。业务 Socket 属于某个 namespace,默认是 /。
不读固定 4.8.3 源码,下面这些合同很容易被旧教程带偏:
- 为什么“连上 WebSocket”并不等于已经加入某个 namespace
- 为什么握手 query 必须带
EIO=4,旧客户端默认会被拒 - 为什么
socket.id不再等于 Engine.IO 的 sid - 为什么本机
io.emit不能自动打到另一台进程上的客户端
固定版本的主链可以拆成五步:
-
挂到 HTTP server:
new Server(3000)或attach(httpServer)。默认path为/socket.io(写入 Engine.IO 时去掉尾斜杠)、serveClient为真、connectTimeout为 45000 ms。 -
Engine.IO 握手与升级:同仓
engine.io@6.6.5默认transports=["polling","websocket"],allowUpgrades=true。polling可升级到websocket或webtransport。EIO!=="4"被当成协议 3;默认allowEIO3=false时直接拒绝。心跳默认pingInterval=25000、pingTimeout=20000,升级超时 10000 ms,maxHttpBufferSize=1e6。 -
Client 必须加入 namespace:底层
connection之后,Client启动 45 秒定时器;若还没有任何 namespace,就关闭连接。io.of("/admin")有独立事件、房间和 middleware。 -
事件、ack 与房间:
emit若最后一个参数是函数,就登记 ack;socket.timeout(ms)会让 ack 过期。join/to(room).emit走 adapter。默认是进程内Adapter;开启connectionStateRecovery才换成SessionAwareAdapter,并默认备份 120 秒、恢复时跳过 middleware。 -
身份与多节点:协议 4 的
socket.id由base64id.generateId()生成,源码写明不复用 Engine.IO sid。默认 emit 只覆盖本进程;跨进程要换 Redis 等 adapter,这不属于 4.8.3 核心默认。
案例 1:房间广播仍停在本进程
Section titled “案例 1:房间广播仍停在本进程”import { Server } from "socket.io";
const io = new Server(3000, { cors: { origin: "*" } });
io.on("connection", (socket) => { socket.on("join", (room) => socket.join(room)); socket.on("msg", ({ room, text }) => { io.to(room).emit("msg", { from: socket.id, text }); });});socket.id 是 Socket.IO 会话 id,不是 Engine.IO sid。io.to(room).emit 只问当前 adapter;没换分布式 adapter 时,另一台机器上的成员收不到。
案例 2:带超时的 ack
Section titled “案例 2:带超时的 ack”socket.emit("save", { id: 7 }, (ack) => { if (ack?.ok) console.log("saved");});
socket.on("save", async (data, cb) => { await db.put(data); cb({ ok: true });});
const reply = await socket.timeout(1000).emitWithAck("ping", "hi");最后一个函数参数变成 ack;timeout 只约束这次等待,不是 Engine.IO 心跳。
案例 3:连接状态恢复不是默认行为
Section titled “案例 3:连接状态恢复不是默认行为”const io = new Server(3000, { connectionStateRecovery: { maxDisconnectionDuration: 2 * 60 * 1000, skipMiddlewares: true, },});只有显式打开时,adapter 才会换成 SessionAwareAdapter,并尝试恢复 rooms、data 和错过的包。socket.recovered 为真才表示这次走了恢复路径。
-
把 Engine.IO 连上当成业务连上:Client 若 45 秒内不加入 namespace,服务端会关连接。只监听
io.on("connection")时,别忘了客户端也要完成 namespace handshake。 -
默认拒绝 EIO3:旧 v2 客户端或漏掉
EIO=4的握手会得到 unsupported protocol version。allowEIO3默认是 false,不是兼容开关常开。 -
多节点仍用内存 adapter:默认广播只在本进程。上线多副本却不配 sticky session 和外部 adapter 时,长轮询的 sid 可能落到另一台机器,表现为反复重连或丢房间消息。
-
CORS / cookie 不是默认合同:Engine.IO 默认
cors=false且不发 cookie。跨域带 cookie 必须显式配置;源码默认不能推断“已经能跨域”。
适用 vs 不适用场景
Section titled “适用 vs 不适用场景”适用:
- 浏览器与 Node 都要双向事件,并希望传输层能从 polling 升级到 WebSocket
- 需要 namespace / 房间 / ack,而不想自己实现扇出语义
- 已有 Socket.IO 客户端协议,两端版本能对齐到 4.x
不适用:
- 只要 RFC 6455 帧,不需要自定义协议 —— 直接用 ws
- 只要服务端单向推送 —— SSE 通常更简单
- 需要严格跨语言、标准 WebSocket only 的客户端生态
- 把未测量的“十万连接”或“比裸 ws 慢/快”写成默认事实
固定版本边界
Section titled “固定版本边界”- 本文绑定
socketio/socket.io@9978574e...,包版本4.8.3,要求 Node >=10.2.0。 - 同提交里
packages/engine.io自报6.6.5,依赖声明为ws@~8.18.3;不把后来的engine.io@6.6.9或独立审查的ws@8.21.3当成此页默认。 - 默认不恢复连接状态、不发 cookie、不启用
perMessageDeflate、不允许 EIO3。 - 本文未安装依赖、未跑上游测试或压测,状态保持
UNVERIFIED。
- 传输层和语义层是两份合同——Engine.IO 管握手、升级、心跳;Socket.IO 管 namespace、房间、ack。
- 默认 adapter 只覆盖本进程——房间是字符串集合,不是跨机器对象。
- 协议版本写在 query 里——
EIO=4与allowEIO3决定旧客户端能不能进门。 - id 分层是有意的——协议 4 不再把 Engine.IO sid 暴露成
socket.id。
- 客户端只完成 Engine.IO 握手、45 秒内不加入任何 namespace,服务端会怎样?
- 握手 query 没有
EIO=4,默认配置会接受吗? - 两台机器都跑默认 adapter,A 上的
io.to("room").emit能到达 B 上的成员吗?
检查点:
Client定时器到期后关闭底层连接。- 不会。源码把非
EIO=4当成协议 3,默认allowEIO3=false拒绝。 - 不能。默认内存 adapter 只广播本进程。
- 官方文档:socket.io/docs/v4
- 固定源码:socketio/socket.io —— 本文绑定提交
9978574e4f1d4e21593497f94c40053cd0fff359 - Engine.IO 协议:github.com/socketio/engine.io-protocol
- ws —— Engine.IO 默认 WebSocket 引擎所基于的 Node 库
- express —— 常见的共享 HTTP server 挂载对象
- ws —— 更薄的 RFC 6455 实现,没有房间和自动降级
- express —— 常把 Socket.IO attach 到同一 HTTP server
- fastify —— 同样能挂,但要自己拿到底层 server
- nestjs ——
@WebSocketGateway默认走 Socket.IO - redis —— 官方多节点 adapter 的常见 pub/sub 后端
- centrifugo —— 独立实时网关,业务后端不自己 hold 连接
- soketi —— Pusher 协议兼容的自托管 WebSocket 服务
- centrifugo —— Centrifugo — Go 写的开源实时消息服务器
- etherpad-lite —— Etherpad — 经典协作文本编辑器
- node-js —— Node.js — 服务端 JS 运行时之父
- soketi —— Soketi — 自己跑一台 Pusher,把实时通信费砍到零头
- ws —— ws — Node.js 上的 RFC 6455 WebSocket 实现