WebSocket 是现代 Web 应用实现实时通信的核心技术。与 HTTP 的「请求-响应」模式不同,WebSocket 提供了全双工、低延迟的通信能力。然而,WebSocket 连接的建立并非凭空产生——它需要一个精心设计的握手过程。这个握手过程不仅是技术实现的需要,更是协议设计哲学的体现。
一、HTTP 轮询的困境
在 WebSocket 出现之前,Web 应用实现「实时」功能主要依赖 HTTP 轮询技术。先理解这些方案及其局限性。
1.1 短轮询(Short Polling)
最直观的方案是客户端定时发送请求:
// 短轮询:每隔一段时间主动询问setInterval(async () => { const response = await fetch("/api/messages"); const data = await response.json(); if (data.hasNew) { updateUI(data.messages); }}, 3000); // 每 3 秒轮询一次问题:
假设服务端每 10 秒才有一次更新,那么 70% 的请求都是无效的。更糟糕的是,每次请求都携带完整的 HTTP 头部:
GET /api/messages HTTP/1.1Host: example.comUser-Agent: Mozilla/5.0...Accept: application/jsonCookie: session=abc123...Authorization: Bearer xxx...这些头部数据可能高达几百字节,而实际有效载荷可能只有几个字节。
1.2 长轮询(Long Polling)
为了减少无效请求,长轮询应运而生:
// 长轮询:服务端持有请求直到有数据或超时async function longPoll() { const response = await fetch("/api/messages?timeout=30"); const data = await response.json(); updateUI(data.messages); longPoll(); // 立即发起新的长轮询}长轮询减少了请求次数,但仍有显著缺陷:
| 问题 | 影响 |
|---|---|
| 连接占用 | 服务端需要维护大量挂起的连接 |
| 超时处理 | 请求超时后需要重新建立连接 |
| 头部开销 | 每次新请求仍需发送完整 HTTP 头 |
| 双向延迟 | 服务端有数据时无法主动推送,需等客户端发起请求 |
1.3 对比总结
WebSocket 的核心优势:一次握手后,建立持久的双向通道,无需重复发送头部,服务端可随时主动推送数据。
二、WebSocket 的设计目标
WebSocket 协议(RFC 6455)的设计有几个关键目标:
2.1 兼容 HTTP 基础设施
WebSocket 的设计者们面临一个现实约束:现有的 Web 基础设施(代理、防火墙、负载均衡器)都基于 HTTP。
如果 WebSocket 使用全新的端口或协议格式,将面临:
- 企业防火墙可能阻止非标准端口
- 代理服务器无法正确处理
- 需要部署全新的基础设施
解决方案:WebSocket 连接始于 HTTP 请求,通过 HTTP Upgrade 机制完成协议切换。
2.2 全双工通信
与 HTTP 的半双工(一次只能一个方向传输)不同,WebSocket 是真正的全双工:
2.3 低开销帧格式
WebSocket 定义了轻量级的帧格式,最小帧头部仅 2 字节:
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - +相比之下,一个典型的 HTTP 请求头部可能占用 200-2000 字节。
三、握手协议详解
WebSocket 握手是整个协议最精妙的部分——它巧妙地复用 HTTP 完成协议升级。
3.1 完整握手流程
3.2 客户端握手请求
一个标准的 WebSocket 握手请求如下:
GET /chat HTTP/1.1Host: server.example.comUpgrade: websocketConnection: UpgradeSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==Sec-WebSocket-Version: 13Origin: http://example.com关键字段解析:
| 字段 | 必需 | 说明 |
|---|---|---|
Upgrade: websocket | 是 | 声明要升级到 WebSocket 协议 |
Connection: Upgrade | 是 | 告知代理/服务器这是一次协议升级请求 |
Sec-WebSocket-Key | 是 | Base64 编码的 16 字节随机值,用于安全验证 |
Sec-WebSocket-Version | 是 | 协议版本,当前为 13 |
Origin | 是(浏览器) | 防止跨站 WebSocket 劫持 |
3.3 服务端握手响应
服务端验证请求后,返回 101 状态码:
HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=状态码 101 的含义是「协议切换」,表示服务器理解并同意切换到 WebSocket 协议。
3.4 为什么需要 Sec-WebSocket-Key?
这是设计的核心问题。既然握手基于 HTTP,为什么不直接发送 Upgrade: websocket 就完成切换?
原因一:确保对方支持 WebSocket
如果服务端不支持 WebSocket,它会返回普通的 HTTP 响应(如 200 OK 或 404 Not Found),客户端可以据此判断。
原因二:防止缓存代理误判
Sec-WebSocket-Key 的存在确保代理不会返回缓存的 HTTP 响应。
原因三:提供基本的安全验证
虽然不是加密,但验证了对方确实理解 WebSocket 协议。
四、Sec-WebSocket-Key 的计算
4.1 计算过程
服务端收到 Sec-WebSocket-Key 后,必须按照 RFC 6455 规定的算法计算 Sec-WebSocket-Accept:
import base64import hashlib
def compute_accept_key(websocket_key): # RFC 6455 定义的 GUID GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
# 拼接 Key 和 GUID combined = websocket_key + GUID
# SHA-1 哈希 sha1_hash = hashlib.sha1(combined.encode()).digest()
# Base64 编码 accept_key = base64.b64encode(sha1_hash).decode()
return accept_key
# 示例client_key = "dGhlIHNhbXBsZSBub25jZQ=="accept_key = compute_accept_key(client_key)print(accept_key) # s3pPLMBiTxaQ9kYGzzhZRbK+xOo=4.2 为什么要拼接固定 GUID?
这个 GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 是 RFC 6455 规定的固定值,它的作用:
- 防止简单回显:如果只是原样返回 Key,任何 HTTP 服务器都能「假装」支持 WebSocket
- 协议标识:只有实现了 WebSocket 协议的服务器才知道要加上这个 GUID
- 非加密验证:提供一种轻量级的协议确认机制
4.3 安全性说明
重要:WebSocket 握手本身不提供加密。如果需要安全传输,必须使用 wss://(WebSocket Secure),即基于 TLS 的 WebSocket。
五、帧格式与掩码机制
握手完成后,通信切换到 WebSocket 帧协议。帧格式的设计体现了协议的精巧。
5.1 帧结构详解
5.2 Opcode 帧类型
| Opcode | 类型 | 说明 |
|---|---|---|
| 0x0 | Continuation | 连续帧,前一个帧未完成 |
| 0x1 | Text | 文本数据(UTF-8) |
| 0x2 | Binary | 二进制数据 |
| 0x8 | Close | 关闭连接 |
| 0x9 | Ping | 心跳探测 |
| 0xA | Pong | 心跳响应 |
5.3 掩码机制:解决缓存污染攻击
WebSocket 协议规定:客户端发送给服务端的数据必须掩码,服务端发送给客户端的数据不掩码。
为什么这样设计?
缓存污染攻击原理:
攻击者可以构造特殊的数据,使得缓存代理误以为这是 HTTP 请求,从而污染缓存。
掩码如何防护:
def apply_mask(payload, masking_key): """应用掩码""" masked = bytearray() for i, byte in enumerate(payload): masked.append(byte ^ masking_key[i % 4]) return bytes(masked)
# 示例masking_key = [0x12, 0x34, 0x56, 0x78]original = b"GET / HTTP/1.1"masked = apply_mask(original, masking_key)print(f"原数据: {original}")print(f"掩码后: {masked}")# 掩码后的数据不再是有效的 HTTP 请求掩码后的数据看起来是随机的,无法被解析为有效的 HTTP 请求,从而防止了缓存污染攻击。
5.4 为什么要单向掩码?
服务端是可信的,不会发起缓存污染攻击,因此不需要掩码。
六、心跳与保活策略
WebSocket 是持久连接,需要机制来检测连接状态。
6.1 为什么需要心跳?
TCP 连接可能因为网络中断而「假死」——双方都不知道连接已断开,但数据无法传递。
6.2 Ping/Pong 机制
WebSocket 定义了 Ping/Pong 帧:
6.3 心跳策略实现
// 客户端心跳实现示例class WebSocketWithHeartbeat { constructor(url) { this.ws = new WebSocket(url); this.heartbeatInterval = null; this.missedHeartbeats = 0; this.maxMissedHeartbeats = 3;
this.ws.onopen = () => this.startHeartbeat(); this.ws.onclose = () => this.stopHeartbeat(); }
startHeartbeat() { this.heartbeatInterval = setInterval(() => { if (this.ws.readyState === WebSocket.OPEN) { this.missedHeartbeats++; if (this.missedHeartbeats > this.maxMissedHeartbeats) { // 多次心跳无响应,关闭连接 this.ws.close(); return; } // 发送 Ping(浏览器自动响应 Pong) this.ws.send(JSON.stringify({ type: "ping" })); } }, 30000); // 30 秒间隔 }
stopHeartbeat() { if (this.heartbeatInterval) { clearInterval(this.heartbeatInterval); } }}6.4 心跳时机选择
| 时机 | 间隔 | 说明 |
|---|---|---|
| 应用层空闲 | 30-60 秒 | 常见选择 |
| 移动网络 | 3-5 分钟 | 省电考虑 |
| 即时通讯 | 10-30 秒 | 快速检测断连 |
| 游戏应用 | 1-5 秒 | 极低延迟要求 |
七、与其他技术对比
WebSocket 不是实现实时通信的唯一选择,对比各种方案。
7.1 技术对比总览
7.2 WebSocket vs SSE
Server-Sent Events (SSE) 是 HTML5 提供的服务器推送技术:
| 特性 | WebSocket | SSE |
|---|---|---|
| 通信方向 | 全双工 | 单向(服务端→客户端) |
| 数据格式 | 文本、二进制 | 仅文本 |
| 协议 | ws/wss | HTTP |
| 重连机制 | 需自行实现 | 浏览器自动重连 |
| 浏览器支持 | IE10+ | IE 不支持 |
| 适用场景 | 聊天、游戏、协作 | 通知、新闻推送 |
7.3 WebSocket vs 长轮询
7.4 选型指南
八、安全考虑
WebSocket 的安全性设计是协议设计的重要部分。
8.1 Origin 验证
跨站 WebSocket 劫持攻击:
// 恶意网站尝试连接受害者的 WebSocket 服务const ws = new WebSocket("wss://victim-bank.com/ws");
// 如果服务端不验证 Origin,恶意网站可以建立连接// 并读取受害者的敏感数据(因为浏览器会自动携带 Cookie)防御措施:
// 服务端验证 Originconst WebSocket = require("ws");const wss = new WebSocket.Server({ port: 8080 });
wss.on("connection", (ws, req) => { const origin = req.headers.origin; const allowedOrigins = ["https://example.com", "https://app.example.com"];
if (!allowedOrigins.includes(origin)) { ws.close(1008, "Origin not allowed"); return; }
// 正常处理连接});8.2 使用 WSS 加密
WSS = WebSocket over TLS,与 HTTPS 类似,提供:
- 数据加密
- 服务器身份验证
- 防篡改
8.3 认证与授权
WebSocket 握手是 HTTP 请求,可以使用标准的 HTTP 认证:
// 方式一:URL 参数(不推荐)const ws = new WebSocket("wss://example.com/ws?token=xxx");
// 方式二:子协议(Subprotocol)传递 Tokenconst ws = new WebSocket("wss://example.com/ws", ["bearer", "xxx"]);
// 方式三:Cookie(推荐,使用 WSS)// 浏览器自动携带 Cookieconst ws = new WebSocket("wss://example.com/ws");服务端验证:
wss.on("connection", (ws, req) => { // 从 Cookie 中提取会话 const cookies = req.headers.cookie; const session = parseCookies(cookies).session;
// 验证会话 const user = validateSession(session); if (!user) { ws.close(1008, "Unauthorized"); return; }
ws.user = user;});8.4 消息验证
WebSocket 是应用层协议,消息格式需要自行定义和验证:
// 定义消息 Schemaconst messageSchema = { type: "string", // message, ping, pong, etc. payload: "object", // 实际数据 timestamp: "number", // 时间戳};
// 验证消息function validateMessage(data) { try { const msg = JSON.parse(data);
// 类型检查 if (typeof msg.type !== "string") { throw new Error("Invalid type"); }
// 大小限制 if (data.length > 1024 * 1024) { // 1MB throw new Error("Message too large"); }
return msg; } catch (e) { throw new Error("Invalid message format"); }}
ws.on("message", data => { try { const msg = validateMessage(data); handleMessage(msg); } catch (e) { ws.close(1003, "Invalid data"); }});九、连接状态与生命周期
9.1 状态转换
9.2 优雅关闭
WebSocket 定义了关闭握手:
关闭帧格式:
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Close code (2 bytes) | |I|S|S|S| (8) |A| (7) | | |N|V|V|V| |S| | Reason (UTF-8 string) | +-+-+-+-+-------+-+-------------+-------------------------------+9.3 关闭代码
| 代码 | 名称 | 说明 |
|---|---|---|
| 1000 | Normal | 正常关闭 |
| 1001 | Going Away | 端点离开(如页面关闭) |
| 1002 | Protocol Error | 协议错误 |
| 1003 | Unsupported Data | 不支持的数据类型 |
| 1008 | Policy Violation | 策略违规 |
| 1009 | Message Too Big | 消息过大 |
| 1010 | Mandatory Extension | 缺少必需扩展 |
| 1011 | Internal Error | 服务器内部错误 |
十、总结
WebSocket 握手的设计是协议工程的典范,它在约束条件下找到了最优解:
核心设计原则:
- 渐进式升级:从 HTTP 到 WebSocket 的平滑过渡
- 最小化开销:握手后无需重复发送头部
- 安全优先:掩码机制防止协议混淆攻击
- 向后兼容:利用现有 HTTP 基础设施
理解 WebSocket 握手的设计原理,不仅有助于正确使用 WebSocket,更能帮助我们理解网络协议设计的权衡艺术。在设计自己的协议或系统时,这些原则同样适用:兼容性、安全性、效率的平衡永远是核心考量。
参考引用
- RFC 6455: The WebSocket Protocol — WebSocket 协议标准
- MDN: WebSocket API — 浏览器 WebSocket API 文档
- HTML5 Rocks: WebSockets — WebSocket 入门教程
- High Performance Browser Networking — WebSocket 性能分析
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






