用 WebSocket 做即时通讯,demo 很容易跑通:浏览器连接服务端,双向收发消息。但上线后问题集中在三处:token 鉴权不严谨导致连接被滥用;断线后离线消息丢失;多端在线时消息顺序不一致。本文按连接建立、消息帧设计、服务端推送、断线恢复的顺序给出可运行代码,并在每个环节标注与现成 IM 软件的差距,帮你在动手前判断自建是否划算。
建立WebSocket连接:客户端与服务端的握手与鉴权
客户端代码:
const socket = new WebSocket(
`wss://example.com/ws?token=${encodeURIComponent(token)}`
);
socket.onopen = () => console.log('connected');
socket.onerror = (e) => console.error('ws error', e);
服务端用 Node.js 的 ws 库处理升级:
const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ noServer: true });
server.on('upgrade', (req, socket, head) => {
const url = new URL(req.url, 'http://localhost');
const token = url.searchParams.get('token');
if (!verifyToken(token)) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
return;
}
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit('connection', ws, req);
});
});
不要把 token 塞进 Sec-WebSocket-Protocol,部分代理和客户端会丢弃或截断。正式环境可在查询参数中只放一次性 ticket,服务端校验后绑定用户身份并下发短时 session id;连接建立后客户端再通过首个消息携带签名完成二次校验。现成 IM SDK 通常已封装握手、鉴权和重连,开发者不需要处理这些边界。
定义统一消息帧:上行与下行结构
所有消息使用同一 JSON 结构,seq 由客户端生成并单调递增,用于确认与排序。
{
"type": "chat",
"seq": 1024,
"to": "user_42",
"payload": { "text": "hello" }
}
{
"type": "chat",
"seq": 1024,
"from": "user_7",
"to": "user_42",
"ts": 1710000000000,
"payload": { "text": "hello" }
}
服务端返回 ack 时携带相同 seq;客户端在收到 ack 前保留副本用于重试。群聊把 to 换成 room_id,服务端负责展开接收人。不要依赖 WebSocket 的单连接顺序:同一用户多端在线时,必须由服务端按 seq 排序后推送,否则多端可能显示顺序不一致。
服务端推送:从单连接到房间/用户定向
const clients = new Map(); // userId -> Set<WebSocket>
function addClient(userId, ws) {
if (!clients.has(userId)) clients.set(userId, new Set());
clients.get(userId).add(ws);
ws.on('close', () => {
const set = clients.get(userId);
if (!set) return;
set.delete(ws);
if (set.size === 0) clients.delete(userId);
});
}
function sendToUser(userId, message) {
const sockets = clients.get(userId);
if (!sockets) return false;
for (const ws of sockets) {
if (ws.readyState === 1) {
ws.send(JSON.stringify(message));
}
}
return true;
}
连接注册表必须处理多端在线和异常断开,否则 Set 会无限增长。单聊同时写入发送方与接收方;群聊先查询房间成员再广播。发送时若连接非 OPEN,应把消息写入离线队列,不能直接丢弃。服务端推送延迟升高时,按网络、服务端、客户端分层的定位方法见即时通讯延迟高怎么排查?从网络、服务端到客户端的分层定位步骤。
断线重连与心跳:消息不丢的边界
- 客户端每 30 秒发送
{"type":"heartbeat"};服务端 35 秒内未收到任何帧,判定连接失效并关闭。 - 客户端
onclose后按 1s、2s、4s、8s、封顶 30s 退避重连;重连成功后先拉取本地最后seq之后的消息。 - 服务端不为长离线用户补发消息,除非你在服务端持久化消息并维护游标;若要求离线必达,必须落盘、补拉、幂等去重。
WebSocket 只解决双向通道,不提供消息必达、离线存储或多端同步。这些能力的具体实现可参考保障消息必达的关键机制:即时通讯消息不丢失怎么实现和离线消息总丢失?即时通讯离线消息收不到的常见原因与修复。
自建WebSocket与现成IM软件功能对比评测
| 维度 | 自建 WebSocket | 现成 IM 软件 |
|---|---|---|
| 消息可靠性 | 需自己实现 ack、重试、幂等 | 内置确认、重试与补发策略 |
| 离线消息 | 需持久化并维护游标 | 默认提供离线队列与补拉 |
| 多端同步 | 需服务端按 seq 排序推送 | 已处理多端游标与已读状态 |
| 后台管理 | 需另建控制台 | 提供成员、群组、审计等后台 |
| 运维成本 | 长期监控、扩容、修复 | 供应商维护,按订阅付费 |
如果你的团队只有前端,或需要在两周内上线多端同步与离线消息,自建通常不划算;可先按预算有限的中小企业即时通讯选型,重点核对这5个维度验证需求。已经决定自建时,先算清长期成本,参考成本拆解:自建即时通讯需要多少钱,包含哪些隐性支出。
常见问题
WebSocket 能保证消息必达吗?
不能。WebSocket 只提供全双工通道,断线、服务端故障、客户端未 ack 都可能丢消息。必达需要持久化、seq 补拉、ack 重试和幂等去重。
心跳间隔多少合适?
桌面端通常 30 秒,服务端 35 秒无任何帧判定断线;移动端可放宽到 60 秒,但必须监听网络切换并主动重连。
没有后端经验能快速实现即时通讯吗?
能跑通单页面 demo,但处理离线消息、多端同步和运维需要后端与存储基础。没有后端建议直接使用现成 SDK 或商用 IM。
什么时候该停止自建,直接采购?
当需求包含离线必达、多端同步、后台管理、审计合规中的任意两项以上,或上线时间少于两周时,采购商用 IM 通常比自建更稳、更便宜。
