在前端面试的深水区,WebSocket 始终是一个绕不开的高频考点。尤其是面对像字节跳动、腾讯这样对计算机网络基础有严苛要求的面试官时,仅仅回答"它能实现实时通信"或者"比轮询性能好"是远远不够的。
面试官真正想听到的,是你对于协议升级机制 、全双工通信模型 以及工程化落地难点的深度理解。
今天,我们将结合一个基于 Koa + koa-websocket 的实战案例,从底层协议握手、代码实现细节,到心跳保活机制,全方位拆解 WebSocket。
一、 面试官心理战:为什么是 WebSocket?
在开始写代码之前,我们必须先解决"为什么"的问题。这通常是面试的开场白,也是考察你对技术选型思考深度的第一关。
面试官问:"Web Socket 和 SSE 有什么区别?为什么聊天室要用 WebSocket?"
我们需要从HTTP 的局限性切入,构建一个完美的回答逻辑:
- HTTP 轮询的痛点 :传统的 HTTP 协议是基于"请求-响应"模式的。要实现实时聊天,客户端必须不断轮询(
setInterval+fetch)。这种方式不仅延迟高(取决于轮询间隔),而且极度浪费带宽。因为每次 HTTP 请求都携带着冗长的 Header(Cookie、User-Agent 等),而有效载荷可能只有几个字节的聊天内容。 - SSE 的单向性 :SSE 是基于 HTTP 的长连接,但它本质上是单向的(服务端 -> 客户端)。它非常适合 LLM 流式输出、股票行情推送等场景,但无法处理用户主动发送消息的需求。
- WebSocket 的全双工优势 :HTML5 提供的全双工通信协议。它基于 TCP,一旦握手成功,客户端和服务端就可以互相主动推送数据,且头部开销极小(仅 2-10 字节)。
核心考点:WebSocket 是双向长连接,SSE 是单向长连接,HTTP 是短连接(通常情况)。在需要高频双向交互(如 IM、MOBA 游戏、协同编辑)的场景下,WebSocket 是唯一解。
二、 核心原理:101 Switching Protocols 握手详解
这是面试中最硬核的部分。面试官希望听到你对握手过程的精准描述,而不是模糊的"建立连接"。
当我们在前端执行 new WebSocket('ws://localhost:3000/ws') 时,浏览器实际上是在利用 HTTP 协议"欺骗"服务器,完成一次协议升级。
面试官希望听到的标准答案:
WebSocket 的连接建立依赖于一次特殊的 HTTP 请求。客户端首先发送一个 HTTP GET 请求,该请求包含特殊的头部信息,告知服务器希望将协议从 HTTP 升级为 WebSocket。
关键请求头字段:
Upgrade: websocket:明确告知服务器,客户端希望将协议升级到websocket。Connection: Upgrade:表明这是一个协议升级请求,而不是普通的 HTTP 请求。Sec-WebSocket-Key:这是一个由客户端生成的随机 Base64 编码字符串(16字节)。它的作用是防止缓存代理服务器误将 WebSocket 请求当作普通 HTTP 请求处理。Sec-WebSocket-Version:通常为13,表明客户端支持的 WebSocket 协议版本。
服务端响应:
如果服务器支持 WebSocket 协议,它会返回状态码 101 Switching Protocols 。同时,服务器会将客户端发来的 Sec-WebSocket-Key 与一个固定的 GUID 拼接,进行 SHA-1 哈希计算后再 Base64 编码,生成 Sec-WebSocket-Accept 返回给客户端。
一旦客户端验证通过,连接就从 HTTP 升级为 WebSocket 长连接,后续通信不再遵循 HTTP 的请求-响应模型,而是基于二进制帧进行传输。
三、 实战:基于 Koa 的多人聊天室
理论讲完,必须落地到代码。我们将使用 Koa 框架配合 koa-websocket 插件来实现一个简单的聊天室。
项目依赖安装:
bash
pnpm i koa koa-websocket
服务端代码实现 (server.js):
javascript
const Koa = require('koa');
const websocket = require('koa-websocket');
// 1. 初始化 Koa 应用并赋予 WebSocket 能力
// koa-websocket 插件扩展了 app 对象,使其拥有了 .ws 属性
const app = websocket(new Koa());
// 用于存储所有活跃的客户端连接,方便广播消息
// 在生产环境中,这里可能需要使用 Redis 来维护连接池
const clients = new Set();
// 2. 处理普通 HTTP 请求:返回一个简易的前端页面
app.use(async (ctx) => {
// ctx 是 Koa 的上下文对象,封装了 req 和 res
ctx.body = `
<!DOCTYPE html>
<html>
<head><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 id="sendButton">发送</button>
<script>
// 3. 前端建立 WebSocket 连接
// 注意协议头是 ws:// 而不是 http://
const ws = new WebSocket('ws://localhost:3000/ws');
// 监听服务端消息
ws.onmessage = function(event) {
const messages = document.getElementById('messages');
messages.innerHTML += '<div>' + event.data + '</div>';
messages.scrollTop = messages.scrollHeight; // 自动滚动到底部
};
// 发送消息
function sendMessage() {
const input = document.getElementById('messageInput');
ws.send(input.value);
input.value = '';
}
document.getElementById('sendButton').onclick = sendMessage;
</script>
</body>
</html>
`;
});
// 4. 处理 WebSocket 连接:使用 app.ws.use 注册中间件
app.ws.use(async (ctx, next) => {
// ctx.websocket 是当前连接的实例
const ws = ctx.websocket;
// 将新连接加入集合
clients.add(ws);
console.log('当前在线人数:', clients.size);
// 监听当前连接的消息
ws.on('message', (data) => {
// 收到消息后,广播给所有在线用户
// 注意:data 可能是 Buffer,通常需要 .toString()
const message = data.toString();
clients.forEach(client => {
// 排除自己(可选),或者直接广播
client.send(message);
});
});
// 监听连接关闭
ws.on('close', () => {
clients.delete(ws);
console.log('连接关闭,当前在线人数:', clients.size);
});
});
// 5. 启动服务器
app.listen(3000, () => {
console.log(' Server is running at http://localhost:3000');
});
代码关键点解析:
websocket(new Koa()):这一步非常关键,它让 Koa 的app对象拥有了.ws属性。本质上,它在同一个 TCP 端口上同时监听 HTTP 和 WebSocket 升级请求。app.ws.use():这是koa-websocket的核心。它允许我们像写普通 Koa 中间件一样处理 WebSocket 连接。clients集合 :WebSocket 本身是无状态的。为了实现"群聊",服务端必须维护一个活跃连接的列表(Set或Map),以便进行消息广播。
四、 进阶:心跳检测与断线重连
在真实的网络环境中,TCP 连接可能会因为网络波动、路由器重启或防火墙策略而"假死"。虽然物理连接还在,但数据已经无法传输。
为了解决这个问题,我们需要心跳机制。
类比:就像异地恋的情侣打电话,每隔一段时间问一句"你在吗?",如果对方回答"在",说明连接正常;如果长时间没声音,就挂断重拨。
实现思路:
- 客户端定时发送 Ping :使用
setInterval每隔 30 秒发送一个特定格式的消息(如{"type": "ping"})。 - 服务端响应 Pong :服务端收到
ping后,立即回复pong。 - 超时检测与重连 :如果客户端在一定时间内没收到
pong,则认为连接已断开,触发重连逻辑。
客户端心跳代码示例:
javascript
const ws = new WebSocket('ws://localhost:3000/ws');
let heartbeatTimer = null;
let reconnectTimer = null;
const startHeartbeat = () => {
heartbeatTimer = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
console.log('发送心跳 Ping');
}
}, 30000); // 30秒一次
};
ws.onopen = () => {
console.log('连接成功');
startHeartbeat();
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === 'pong') {
console.log('心跳正常,服务器在线');
// 这里可以重置超时定时器,防止服务端假死
} else {
// 处理普通聊天消息
console.log('收到消息:', data);
}
};
ws.onclose = () => {
clearInterval(heartbeatTimer);
console.log('连接关闭,准备重连...');
// 简单的指数退避重连策略
reconnectTimer = setTimeout(() => {
// 重新建立连接逻辑...
}, 3000);
};
五、 总结
通过这篇教程,我们不仅实现了一个功能完备的聊天室,还深入理解了 WebSocket 的核心机制。
在面试中,当你被问到 WebSocket 时,可以按照这个逻辑闭环来回答:
- 场景:聊天、协作编辑,需要双向实时通信。
- 原理:基于 TCP,通过 HTTP 升级协议(101状态码)建立长连接。
- 实现 :前端
new WebSocket,后端维护连接池进行广播。 - 优化 :必须配合心跳检测 和断线重连机制,保证连接的稳定性。
掌握了这些,你就不仅仅是在"调 API",而是在真正理解网络通信的本质。