最近在复习计算机网络和 LLM 相关技术时,我突然意识到一个很有意思的现象:现在的 AI 聊天大多用的是 SSE ,但提到真正的实时互动,还得看 WebSocket。
为了搞懂这玩意儿,我手写了一个简易版的聊天室。今天就把我的学习笔记、代码实现,还有那些让人头秃的协议对比,一次性全掏出来!
🤔 为什么 HTTP 不适合聊天?
咱们先聊聊背景。作为前端,我们最熟悉的是 HTTP 协议。
HTTP 就像是一个"高冷"的客服:
- 你问一句(Request),它答一句(Response)。
- 答完就挂电话(短连接),下次想问得重新拨号。
如果你想做一个聊天室,用 HTTP 怎么办?只能靠轮询:
scss
// 每隔 1 秒问一次服务器:"有新消息吗?有新消息吗?"
setInterval(() => {
fetch('/api/messages').then(...)
}, 1000);
这太蠢了,对吧?性能差,延迟高,服务器都要被问烦了。
那 SSE 呢?SSE 适合 LLM 那种"流式输出"(我一次提问,它一直吐字),它是单向 的。但聊天是双向的,我要发,你也要发。
所以,我们需要一个能建立"长连接"、双方都能主动说话的协议------WebSocket。
💡 WebSocket:一次握手,终身相伴
WebSocket 是 HTML5 提供的一种在单个 TCP 连接上进行全双工通讯的协议。
- 全双工:就像打电话,双方都可以同时说话,不需要等对方说完。
- 长连接:一旦建立,除非主动断开,否则一直连着。
📝 核心代码逻辑拆解
这里我用 Koa + koa-websocket 来实现。为了让大家看得更清楚,我把代码拆成三个关键步骤来讲。
步骤一:搭建舞台(服务端初始化)
首先,我们需要让 Koa 具备处理 WebSocket 的能力,并准备一个"花名册"来记录所有连进来的用户。
javascript
编辑
ini
const Koa = require('koa');
const websocket = require('koa-websocket');
// 1. 初始化 Koa 并赋予 WebSocket 能力
const app = websocket(new Koa());
// 2. 准备一个 Set 集合,用来存储所有连接的客户端
// 为什么用 Set?因为我们要保证连接对象的唯一性
const clients = new Set();
步骤二:派发请柬(处理 HTTP 请求)
WebSocket 连接通常是从一个网页开始的。所以,我们需要一个普通的 HTTP 中间件,返回给浏览器一个包含聊天界面的 HTML 页面。
javascript
编辑
xml
// 3. 处理普通 HTTP 请求:返回我们的聊天页面
app.use(async (ctx) => {
ctx.body = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Chat</title>
</head>
<body>
<div id="messages" style="height:300px;overflow-y:scroll; border:1px solid #ccc;"></div>
<input type="text" id="messageInput" placeholder="输入消息..."/>
<button onclick="sendMessage()">发送</button>
<script>
// 核心在这里:前端建立 WebSocket 连接
// 注意协议是 ws:// 而不是 http://
const ws = new WebSocket('ws://localhost:3000/ws');
// 监听服务器发来的消息
ws.onmessage = function(event){
const messageDiv = document.getElementById('messages');
messageDiv.innerHTML += '<div>' + event.data + '</div>';
}
function sendMessage(){
const input = document.getElementById('messageInput');
ws.send(input.value); // 发送消息
input.value = '';
}
</script>
</body>
</html>
`
})
步骤三:建立专线(处理 WebSocket 连接)
这是最关键的一步。当浏览器执行了 new WebSocket() 后,服务器会通过 app.ws.use 捕获到这个连接请求。
这里我们主要做三件事:登记用户 、监听消息 、广播消息。
javascript
编辑
javascript
// 4. 处理 WebSocket 连接
app.ws.use(async (ctx) => {
// A. 登记:将当前连接加入集合
clients.add(ctx.websocket);
console.log('当前在线人数:', clients.size);
// B. 监听:当收到某人的消息时
ctx.websocket.on('message', message => {
// C. 广播:把这条消息发给"花名册"里的每一个人
for(const client of clients){
client.send(message.toString());
}
});
// D. 离场:监听断开连接,把人从花名册里删掉
ctx.websocket.on('close', () => {
clients.delete(ctx.websocket);
console.log('有人离开了...');
});
})
app.listen(3000, () => {
console.log('🚀 服务器启动,请访问 http://localhost:3000');
});
运行效果:
打开浏览器访问 localhost:3000,你可以打开好几个标签页,在一个标签页发消息,所有标签页都会实时收到消息!这就是广播。

📊 一张表看懂 HTTP、SSE 与 WebSocket
为了面试(408 计算机网络)和工作,这三个协议的区别必须门儿清:
| 特性 | HTTP | SSE | WebSocket |
|---|---|---|---|
| 连接方式 | 短连接 (请求-响应) | 长连接 (单向推送) | 长连接 (双向通讯) |
| 通讯方向 | 客户端发起 | 服务端 -> 客户端 | 客户端 <-> 服务端 |
| 适用场景 | 网页加载、API 请求 | AI 流式输出、股票行情 | 聊天室、即时游戏、协作编辑 |
| 数据格式 | 文本/JSON/二进制 | 仅限文本 (text/event-stream) | 二进制帧/文本帧 |
- SSE:适合"我不动,你推给我"的场景(比如 LLM 打字机效果)。
- WebSocket:适合"你一句我一句"的场景。
❤️ 心跳机制:长连接的"异地恋"哲学
既然 WebSocket 是长连接,那就面临一个现实问题:网络是不稳定的。
路由器重启、手机进电梯、防火墙拦截......都可能导致连接"静默断开"。这时候,客户端以为连着,服务器以为断了,这就尴尬了。
怎么解决?------ 心跳机制
这就好比异地恋的情侣:
你们不能一直打电话(开销太大),但必须定期确认对方还在。
- 客户端:"宝,你在吗?"(Ping)
- 服务端:"在呢,活着呢。"(Pong)
如果客户端发了 Ping,过了 30 秒还没收到 Pong,那就判定为"分手"(连接断开),然后触发重连机制。
代码逻辑示意:
javascript
// 客户端
setInterval(() => {
if(ws.readyState === WebSocket.OPEN){
ws.send(JSON.stringify({type: 'ping'}));
}
}, 30000); // 每30秒问候一次
// 服务端
ws.on('message', (msg) => {
const data = JSON.parse(msg);
if(data.type === 'ping'){
ws.send(JSON.stringify({type: 'pong'})); // 秒回
}
});
📌 总结
今天我们从 HTTP 的局限性出发,手搓了一个基于 Koa 的 WebSocket 聊天室,顺便复习了 SSE 和心跳机制。
划重点:
- HTTP 是"一问一答",WebSocket 是"双向奔赴"。
- SSE 适合流式输出(AI),WebSocket 适合即时通讯(Chat)。
- 心跳机制是长连接保活的关键,防止"假死"连接。
希望这篇文章能帮你搞定 WebSocket!如果觉得有用,记得点个赞 👍,我们下期见!