为什么我们需要WebSocket

做 Web 开发,你肯定遇到过需要实时推送的场景:聊天消息、游戏对战、股票行情、协同编辑……传统的 HTTP 请求是“请求-响应”模式,客户端不拉,服务器就不推,想去实现“实时”只能靠轮询(Polling)——定时发请求问一句“有新消息吗?”这玩意儿效率低、延迟高、浪费带宽,尤其在高并发下简直就是灾难。

WebSocket 就是来干掉轮询的。它在客户端和服务器之间建立一条长连接,双方可以随时互相发消息,真正的全双工通信。而且握手阶段用 HTTP 升级协议,之后的数据帧开销极小(只有 2 字节的头部),非常适合高频实时场景。

换个角度看,WebSocket 就是个“永不挂断的电话线”,你随时可以说话,对方随时能听到,不用每次都拨号。

先搞懂核心概念

握手升级

WebSocket 的连接建立不走普通 HTTP 请求,而是发一个特殊的 Upgrade 头部:

GET /chat HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

服务器响应:

HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

之后这条连接就从 HTTP 变成了 WebSocket 协议,双方可以自由发送数据帧。注意:WebSocket 的端口可以和 HTTP 一样(80/443),但它们是不同的协议,Nginx 等代理需要额外配置。

数据帧与掩码

WebSocket 消息是分帧发送的,客户端发往服务器的帧必须进行掩码(Masking),服务器发客户端则不用。不过这些底层细节库都帮你处理好了,你只要关心 sendmessage 事件就行。

动手搭建一个聊天室

咱们用 Node.js 的 ws 库来写服务端,浏览器原生 WebSocket API 当客户端,实现一个最简聊天室:谁都能发言,消息广播给所有人。

环境准备

先装好 Node.js(v16+),然后新建项目:

mkdir ws-chat cd ws-chat npm init -y npm install ws

服务端代码(完整可运行)

创建 server.js

const WebSocket = require('ws'); const wss = new WebSocket.Server({ port: 8080 }); console.log('WebSocket server started on ws://localhost:8080'); // 存储所有连接的用户(这里用 Set) const clients = new Set(); wss.on('connection', (ws) => { console.log('新用户连接'); clients.add(ws); // 给新用户发送欢迎消息 ws.send(JSON.stringify({ type: 'system', content: '欢迎加入聊天室!' })); // 收到客户端消息 ws.on('message', (data) => { let parsed; try { parsed = JSON.parse(data); } catch (e) { // 非 JSON 消息,丢弃或当作普通文本 console.log('收到非 JSON 消息,跳过'); return; } // 广播给所有其他客户端 const message = JSON.stringify({ type: 'chat', username: parsed.username || '匿名', content: parsed.content, timestamp: Date.now() }); clients.forEach((client) => { if (client !== ws && client.readyState === WebSocket.OPEN) { client.send(message); } }); }); // 连接关闭时清理 ws.on('close', () => { console.log('用户断开连接'); clients.delete(ws); }); // 处理错误,避免进程崩溃 ws.on('error', (err) => { console.error('WebSocket 错误:', err.message); clients.delete(ws); }); });

注意:

客户端代码(完整可运行)

创建一个 index.html,直接用浏览器原生 API:

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>聊天室</title> <style> body { font-family: sans-serif; max-width: 600px; margin: 20px auto; } #messages { height: 400px; overflow-y: auto; border: 1px solid #ccc; padding: 10px; margin-bottom: 10px; } input { padding: 8px; margin-right: 8px; } button { padding: 8px 16px; } </style> </head> <body> <h2>WebSocket 聊天室</h2> <div id="messages"></div> <input id="username" placeholder="昵称" value="用户" /> <input id="input" placeholder="说点什么…" /> <button onclick="sendMessage()">发送</button> <script> const ws = new WebSocket('ws://localhost:8080'); const messagesDiv = document.getElementById('messages'); const input = document.getElementById('input'); const usernameInput = document.getElementById('username'); // 收到消息 ws.onmessage = (event) => { let msg; try { msg = JSON.parse(event.data); } catch (e) { // 非 JSON 文本直接显示 appendMessage('系统', event.data); return; } if (msg.type === 'system') { appendMessage('系统', msg.content); } else if (msg.type === 'chat') { appendMessage(msg.username, msg.content); } }; ws.onopen = () => { appendMessage('系统', '已连接到服务器'); }; ws.onclose = () => { appendMessage('系统', '连接断开'); }; ws.onerror = (err) => { console.error('WebSocket 错误:', err); appendMessage('系统', '连接异常,请检查控制台'); }; function appendMessage(sender, content) { const div = document.createElement('div'); div.textContent = `[${sender}] ${content}`; messagesDiv.appendChild(div); messagesDiv.scrollTop = messagesDiv.scrollHeight; } function sendMessage() { const content = input.value.trim(); if (!content) return; const username = usernameInput.value.trim() || '匿名'; const msg = { username, content }; ws.send(JSON.stringify(msg)); input.value = ''; } // 按回车发送 input.addEventListener('keypress', (e) => { if (e.key === 'Enter') sendMessage(); }); </script> </body> </html>

运行与测试

经验之谈提醒:如果你用 file:// 打开 HTML,Chrome 可能会限制某些 API,但 WebSocket 连接不受影响。不过建议用 http-server 之类的工具来模拟真实环境。

常见错误与解决方法

❌ 错误1:连接失败 `WebSocket connection to 'ws://...' failed`

原因

解决方案

❌ 错误2:消息发送后没反应,服务端没收到或报错 `Unexpected server response: 426`

原因

解决方案

const express = require('express'); const http = require('http'); const WebSocket = require('ws'); const app = express(); const server = http.createServer(app); const wss = new WebSocket.Server({ server }); server.listen(8080);

❌ 错误3:中文消息变乱码或丢失

原因:默认 send 发送的是字符串(UTF-8),但如果客户端发的是 Uint8ArrayBlob 且编码不是 UTF-8,则可能乱码。

解决方案:统一约定消息格式为 JSON 字符串,服务端和客户端都 JSON.stringify / JSON.parse。不要直接发二进制,除非你有特殊需求(如文件传输)。

经验之谈心得:那些年我踩过的 WebSocket 坑

1. 心跳要自己搞

WebSocket 协议没有强制心跳,很多服务器(比如 Nginx)配置了超时空闲连接自动断开(比如 60 秒无数据就断开)。我们的聊天室如果大家都不说话,连接就断了。解决办法:服务端每隔 30 秒发一个 ping 帧,客户端回 pong,或者互相发空消息。

ws 库自带了 ping/pong 方法,可以启动一个定时器:

const interval = setInterval(() => { wss.clients.forEach((ws) => { if (ws.readyState === WebSocket.OPEN) { ws.ping(); // 发送 ping } }); }, 30000); wss.on('connection', (ws) => { ws.on('pong', () => { // 收到了 pong,说明对方还在 }); });

但是注意:ws 库 8.x 之后 ping 方法默认不自动触发 pong 回复,你需要自己监听 pong 事件来判断存活。更简单的做法是定期发一个 { type: 'ping' } 文本消息。

2. 关闭连接时的清理

客户端关闭网页,close 事件会触发。但如果是网络闪断,服务器可能等到 TCP 超时才发现。所以你的服务端要加一个“心跳超时”机制,比如 1 分钟内没收到任何消息(包括 ping/pong),就主动关闭这个连接。

3. 广播性能问题

当连接数很多(比如 10 万+),循环 clients.forEach 发送会给 Node 的事件循环造成压力。生产环境应该用 Redis 发布/订阅 + 进程间通信来扩展,或者使用 wsbroadcast 模式(其实内部也是 forEach,但可以配合 cluster 分片)。

小项目用 Set 没问题,但记得检查 readyState,避免往关闭的连接里写数据。

最佳实践建议

✅ 使用 WSS 而不是 WS

生产环境绝对要用 WSS(WebSocket Secure),也就是基于 TLS 的 WebSocket。它和 HTTPS 一样,数据加密,防止中间人攻击。用 Node.js 自带 https 模块或 Nginx 反向代理都可以。

https 模块+ ws 示例:

const https = require('https'); const fs = require('fs'); const WebSocket = require('ws'); const server = https.createServer({ cert: fs.readFileSync('/path/to/cert.pem'), key: fs.readFileSync('/path/to/key.pem') }); const wss = new WebSocket.Server({ server }); server.listen(443);

✅ 做好消息校验与限流

不要信任客户端的任何输入。消息内容要限制长度,防止有人发 10MB 字符串搞崩溃;频率也要限制,比如 1 秒内最多发 5 条消息,超过就断开连接。

✅ 错误处理要全覆盖

ws 的每个 ws 对象都可能触发 error,如果没监听,Node 进程会直接 crash。所以一定要加 ws.on('error', ...)

✅ 日志记录

记录连接、断开、错误、消息量,方便排查问题。可以结合 winstonpino

✅ 优雅关闭

服务端重启时,应该先关闭所有 WebSocket 连接并通知客户端,然后再退出进程。监听 SIGTERM 信号:

process.on('SIGTERM', () => { wss.clients.forEach((ws) => { ws.close(1001, 'Server is shutting down'); }); wss.close(() => { process.exit(0); }); });

✅ 考虑使用 Socket.IO 作为进阶

原生 WebSocket 很底层,很多功能(自动重连、房间、ack)需要自己实现。如果项目复杂,推荐 Socket.IO,它封装了 WebSocket 并降级到轮询,体验更好。但是纯 WebSocket 场景(比如游戏、高频交易)还是原生更可控。

总结

WebSocket 让你的应用拥有真正的双向实时通信能力,实现起来并不复杂。你只需要一个服务端库和浏览器端几行代码。

但别只停留在跑通 Demo,多看异常情况:网络闪断、内存泄漏、心跳、限流……这些才是线上稳定运行的关键。

现在就去敲代码吧,把上面的聊天室跑起来,试试多开几个标签页,感受一下实时聊天的乐趣。遇到问题别慌,先看控制台,再回头看本文的“常见错误”,大部分都能解决。

技术选型参考:简单聊天场景用原生WebSocket即可;需要房间、自动重连、ack机制时推荐Socket.IO;对性能要求极高的场景(游戏、高频交易)可以考虑 uWebSockets.js。选对工具比写代码更重要。