为什么我们需要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),服务器发客户端则不用。不过这些底层细节库都帮你处理好了,你只要关心 send 和 message 事件就行。
动手搭建一个聊天室
咱们用 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);
});
});
注意:
- 用
Set 存 ws 对象,简单高效。
- 消息统一用 JSON,方便解析。
- 广播时过滤掉发送者自己,如果你需要“回显”可以去掉
client !== ws。
- 别忘记
error 事件,否则挂掉的连接会导致内存泄漏。
客户端代码(完整可运行)
创建一个 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>
运行与测试
- 终端启动服务端:
node server.js
- 在浏览器打开
index.html(可以直接用 file://,或者用 live-server 等静态服务器)
- 开两个浏览器标签页,输入不同昵称,发送消息,看另一侧是不是实时收到了?
经验之谈提醒:如果你用 file:// 打开 HTML,Chrome 可能会限制某些 API,但 WebSocket 连接不受影响。不过建议用 http-server 之类的工具来模拟真实环境。
常见错误与解决方法
❌ 错误1:连接失败 `WebSocket connection to 'ws://...' failed`
原因:
- 服务端没启动或端口写错了
- 跨域?别误会,WebSocket 没有浏览器同源策略限制,但协议和端口必须严格匹配。如果前端是 HTTPS 页面,你不能连 WS(只能用 WSS)。同样,HTTP 页面不能连 WSS(混合内容阻塞)。
- 防火墙拦截或代理没有转发 WebSocket。
解决方案:
- 检查服务端是否在监听,
curl 或 telnet 测试端口。
- 如果是 HTTPS 页面,服务端也必须用 WSS(需要 SSL 证书),参考下面“最佳实践”中的启动方式。
- 看浏览器控制台的 Network 标签,WebSocket 帧是否正常。
❌ 错误2:消息发送后没反应,服务端没收到或报错 `Unexpected server response: 426`
原因:
- 你可能用了
socket.io 或其它库,但客户端和服务端不匹配。
- 服务端没有正确升级(比如用了 HTTP 服务器但没有处理 Upgrade 头)。
- 用了不安全的代理,比如某些 CDN 不支持 WebSocket。
解决方案:
- 检查服务端是否真的创建了
WebSocket.Server,而不是普通 HTTP 服务器。
- 如果你的
ws 实例需要绑定在 HTTP 服务器上(比如配合 Express),正确做法:
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),但如果客户端发的是 Uint8Array 或 Blob 且编码不是 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 发布/订阅 + 进程间通信来扩展,或者使用 ws 的 broadcast 模式(其实内部也是 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', ...)。
✅ 日志记录
记录连接、断开、错误、消息量,方便排查问题。可以结合 winston 或 pino。
✅ 优雅关闭
服务端重启时,应该先关闭所有 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。选对工具比写代码更重要。