游客发表
去年帮朋友做一个内部工具,现聊需求很简单:五六个人同时在线,天室发消息能立刻看到。现聊我第一反应是天室轮询,每秒发一次请求查新消息。现聊跑了两天,天室服务器日志里全是现聊无意义的空响应,带宽浪费不说,天室消息延迟还忽高忽低。现聊换成 WebSocket 之后,天室代码量少了一半,现聊延迟直接降到几十毫秒。天室这就是现聊它存在的理由——一次握手,之后全是天室双向对话。
HTTP 是现聊请求-响应模型。客户端问,服务器答,问完就断。聊天室要的是服务器主动推消息,HTTP 干不了这活。WebSocket 在 TCP 之上开了一条持久通道,握手阶段借用 HTTP 的 Upgrade 头,连上之后协议就切换了。浏览器里写起来就一行:
const ws = new WebSocket('ws://localhost:3000'); 连接建立后,ws.send() 发数据,ws.onmessage 收数据。没有请求头,没有 Cookie 重传,每帧数据只多 2 到 10 字节的帧头。对比 HTTP 每次请求动辄几百字节的头部,差距很明显。
服务端我用 Node.js 的 ws 库,不到三十行就能跑起来。核心逻辑是维护一个客户端集合,收到消息后遍历广播:
const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 3000 }); wss.on('connection', (ws) => { ws.on('message', (data) => { wss.clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { client.send(data.toString()); } }); }); }); 就这么简单。但直接广播原始消息有个问题——谁发的?什么时间?前端拿到 JSON 字符串得自己解析。我在消息体里加了 type 字段区分系统通知和用户消息,又塞了时间戳和昵称。后来发现客户端断线重连时,旧连接可能还留在 wss.clients 里,导致广播报错。加了 close 事件清理,才算稳了。
前端部分更琐碎。连接状态得显示出来,不然用户不知道消息发没发出去。我监听 onopen、onclose、onerror 三个事件,用一个状态灯表示。断线后自动重连,间隔从 1 秒开始翻倍,最多重试 5 次。这些细节不做,体验就很糙。
有个坑我踩过:WebSocket 连接可能被代理服务器掐断。公司内网用了 Nginx,默认 60 秒没数据就断。解决办法是加心跳,客户端每 30 秒发一个 ping,服务端回 pong。代码就几行,但没它的话,用户挂机十分钟回来发现连接早没了。
安全性也别忽略。ws:// 是明文传输,上线必须换 wss://。我见过有人把鉴权放在 URL 参数里,ws://host?token=xxx,日志一翻全暴露了。正确做法是连接建立后先发一条认证消息,服务端验证通过再允许广播。
这套方案支撑几十个人同时聊天完全够用。真要上万人,单机广播就吃力了,得引入 Redis 的 pub/sub 做多进程分发。但那是另一个话题。对大多数内部工具、小型聊天室、实时通知场景,原生 WebSocket 加几十行代码,比任何轮询方案都干净。
我后来把那个工具开源了,地址在 GitHub 上。有人提 issue 说想加消息历史,我让他自己接个 SQLite。WebSocket 只管实时通道,持久化是数据库的事,别混在一起。职责清晰,代码才好维护。
友情链接