跳到主要内容

Socket.IO

很多团队第一次做实时能力时,会先在两个方向里选一个:

  • 直接用原生 WebSocket
  • Socket.IO

这两条路线都能做实时通信,但关注点不一样。

  • 原生 WebSocket 更轻,更贴近协议本身
  • Socket.IO 更像一层更完整的实时通信框架,帮前后端补上房间、事件、自动重连、确认回调等常见能力

先说结论:它不是 WebSocket 的简单别名

Socket.IO 底层常会优先使用 WebSocket,但它不是浏览器原生 WebSocket API 的直接包装。

它额外提供了:

  • 事件模型
  • 自动重连
  • 心跳管理
  • rooms
  • namespaces
  • ack 回调
  • 中间件
  • 更完整的客户端和服务端协作方式
  • 在某些环境下的降级能力

所以更稳的理解是:

  • WebSocket 是协议和基础通道
  • Socket.IO 是一层更高的实时应用框架

什么时候更适合用 Socket.IO

更常见的场景通常是:

  • 聊天
  • 在线客服
  • 协同看板
  • 通知中心
  • 多人房间
  • 业务事件很多,前后端都想按事件名协作
  • 团队更在意交付速度,而不是尽量贴近底层协议

如果需求只是:

  • 单条稳定流式消息
  • 极简双向通信
  • 强控制协议细节

原生 WebSocket 仍然很值得优先考虑。

它在补什么短板

原生 WebSocket 真正难的地方,往往不在连上,而在连接之后:

  • 怎么做自动重连
  • 怎么做事件路由
  • 怎么做房间广播
  • 怎么做鉴权中间件
  • 怎么做客户端确认
  • 怎么做连接状态恢复

Socket.IO 这层就是在把这些高频工程问题标准化。

客户端最小示例

import { io } from 'socket.io-client'

const socket = io('https://example.com', {
transports: ['websocket'],
withCredentials: true,
})

socket.on('connect', () => {
console.log('connected', socket.id)
})

socket.on('message:new', (payload) => {
console.log(payload)
})

socket.emit('message:send', { text: 'hello' })

这里最直观的差异是:

  • 不是自己处理原始 message 字符串
  • 而是直接按事件名监听和发送

服务端最小示例

import { createServer } from 'node:http'
import { Server } from 'socket.io'

const httpServer = createServer()
const io = new Server(httpServer, {
cors: {
origin: 'http://localhost:3000',
credentials: true,
},
})

io.on('connection', (socket) => {
socket.on('message:send', (payload) => {
io.emit('message:new', payload)
})
})

httpServer.listen(8080)

事件模型为什么适合业务代码

Socket.IO 的日常心智模型更像“事件总线”:

  • message:send
  • message:new
  • room:join
  • room:left
  • presence:update

这在业务上很直观,因为前后端可以直接围绕事件名协作,而不是先定义一层通用 message 协议再自己分发。

emiton 之外,更常用的是 ack

很多业务消息并不是“发了就完”,而是需要确认服务端有没有收到、有没有处理成功。

socket.emit('order:create', payload, (result) => {
console.log('server ack', result)
})

服务端:

socket.on('order:create', async (payload, ack) => {
const order = await createOrder(payload)
ack({ ok: true, orderId: order.id })
})

这很适合:

  • 创建消息
  • 确认状态
  • 回包式实时操作

rooms 为什么是高频能力

聊天、协同、直播、多人看板这些场景里,消息很少是“发给所有人”。

更常见的是:

  • 某个会话房间
  • 某个项目空间
  • 某个用户私有频道

Socket.IO 的 room 能力就很实用:

socket.join(`room:${roomId}`)
io.to(`room:${roomId}`).emit('message:new', payload)

常见用法:

  • 房间消息广播
  • 用户个人通知
  • 同一用户多端同步

namespace 适合什么场景

namespace 比 room 更像“不同业务域的连接入口”。

例如:

  • /chat
  • /admin
  • /metrics
const chat = io.of('/chat')
const admin = io.of('/admin')

大多数业务先用 room 就够了。namespace 一般在:

  • 权限边界明显不同
  • 中间件和事件集合完全不同
  • 想把连接入口拆开

时才更值得单独用。

鉴权通常怎么接

如果前后端同域或已处理跨域凭证,Cookie 会是比较省心的一种方式。

2. 握手时传 token

const socket = io('https://example.com', {
auth: {
token: accessToken,
},
})

服务端中间件:

io.use((socket, next) => {
const token = socket.handshake.auth.token
if (!token) return next(new Error('unauthorized'))

const user = verifyToken(token)
socket.data.user = user
next()
})

3. 连接建立后再发认证事件

也能做,但一般不如握手阶段直接校验清楚。

中间件为什么重要

Socket.IO 比较实用的一点,就是能在连接建立阶段统一做:

  • 鉴权
  • 限流
  • 租户识别
  • 用户信息挂载
io.use((socket, next) => {
try {
socket.data.traceId = crypto.randomUUID()
next()
} catch (error) {
next(error as Error)
}
})

这样业务事件里就不需要反复做同一层前置检查。

重连通常比原生 WebSocket 省心

Socket.IO 自带重连能力,常见配置包括:

const socket = io('https://example.com', {
reconnection: true,
reconnectionAttempts: 10,
reconnectionDelay: 1000,
reconnectionDelayMax: 8000,
})

前端仍然要关心这些问题:

  • 重连期间 UI 怎么提示
  • 重连成功后要不要补拉数据
  • 某些关键房间要不要重新 join
  • token 过期后怎么刷新再连

React 里更稳的封装方式

import { io, Socket } from 'socket.io-client'
import { useEffect, useMemo } from 'react'

export function useChatSocket(token: string) {
const socket = useMemo(
() =>
io('https://example.com/chat', {
auth: { token },
transports: ['websocket'],
}),
[token],
)

useEffect(() => {
return () => {
socket.close()
}
}, [socket])

return socket
}

比较关键的点:

  • 避免每次 render 都新建连接
  • 卸载时显式关闭
  • 不要把所有业务逻辑直接写进组件里

Vue 里也更适合拆成 composable

import { io } from 'socket.io-client'
import { onBeforeUnmount } from 'vue'

export function useSocket(token: string) {
const socket = io('https://example.com', {
auth: { token },
})

onBeforeUnmount(() => {
socket.close()
})

return socket
}

大量事件时,消息组织要尽早定规则

建议尽早统一:

  • 事件命名
  • 负载结构
  • 错误格式
  • ack 格式
  • room 命名

例如:

{
type: 'message:new',
payload: {
roomId: 'r_1',
text: 'hello'
}
}

即使用 Socket.IO 的事件模型,也建议 payload 结构保持稳定,这样调试和日志都更容易看。

服务端扩缩容时,单机思路会失效

一旦进入多实例部署,比较典型的问题是:

  • 用户连接在不同实例上
  • room 广播只发到了本机
  • 某个实例重启后在线状态乱掉

这时通常要配 adapter,例如 Redis adapter,来同步跨实例事件和房间信息。

这也是为什么实时系统不能只看前端接法,后面一定会碰到服务端拓扑问题。

和原生 WebSocket 怎么选

更适合先选 Socket.IO

  • 业务事件很多
  • room / namespace / ack 都需要
  • 想更快落地实时能力
  • 服务端也愿意围绕 Socket.IO 编排

更适合先选原生 WebSocket

  • 协议要尽量轻
  • 想保留完全自定义的消息模型
  • 依赖最少
  • 服务端不是 Node,或者不想绑定 Socket.IO 生态

和 SSE 怎么分工

如果是:

  • 大模型流式输出
  • 单向通知流
  • 服务端推、客户端主要看

SSE 仍然很顺手。

如果是:

  • 双向消息
  • 在线状态同步
  • 房间广播
  • 高频交互

Socket.IO 或 WebSocket 更合适。

常见误区

1. 把它当成“实时版 HTTP”

实时系统的问题重点不在请求成功这一点,而在连接生命周期、状态同步和恢复。

2. 只做自动重连,不做数据恢复

连接恢复了,不代表业务状态也恢复了。

3. room 设计过晚

等事件越来越多之后再补 room 规则,成本会明显更高。

4. 不考虑多实例

本地单机能跑,不代表生产环境的广播和在线状态就对。

更稳的落地顺序

  1. 先确认实时需求是否真的需要双向通信
  2. 先定事件命名和 payload 结构
  3. 先做好鉴权中间件
  4. 再接 room 和广播
  5. 再补重连、恢复、补拉和扩缩容方案

和这组其他文章怎么配合看

  • 想先看底层双向通信:看 WebSocket
  • 想看服务端单向流:看 SSE
  • 想做整体方案判断:看 轮询 / 长轮询 / 实时通信选型

参考来源