[HTTP/Web] WebSocket : 全双工、长连接、低延迟、双向实时通信场景的解决方案
1 概述: WebSocket : 全双工、长连接、低延迟、双向实时通信场景的解决方案
1.1 为什么需要 WebSocket?
传统 HTTP 的困境
- 想象你在开发一个股票行情页面,需要实时显示股价变化:
// 方式1:传统轮询(Polling)- 低效!
setInterval(() => {
fetch('/api/stock-price')
.then(res => res.json())
.then(data => updatePrice(data));
}, 1000); // 每秒请求一次,大部分返回都是"没变化"
- 问题:
- 大量无效请求(99% 返回 304 Not Modified)
- 延迟高(最快也要等下次轮询)
- 服务器压力大(N 个客户端 × M 次请求)
// 方式2:长轮询(Long Polling)- 稍好但仍有问题
function longPoll() {
fetch('/api/wait-for-update')
.then(data => {
updateUI(data);
longPoll(); // 递归继续
});
}
- 问题:
- 连接频繁断开重建
- 服务器需要维护大量挂起连接
- 仍受 HTTP 协议开销拖累
WebSocket 的解决方案: HTTP 轮询 vs. WebSocket

| 特性 | HTTP 轮询 | WebSocket |
|---|---|---|
| 通信方式 | 客户端主动拉取 | 服务端主动推送 |
| 连接 | 短连接,频繁创建 | 长连接,一次握手 |
| 延迟 | 秒级(受轮询间隔限制) | 毫秒级(实时) |
| 开销 | HTTP 头重复传输 | 帧头仅 2-14 字节 |
| 全双工 | ❌ 半双工 | ✅ 全双工 |
1.2 WebSocket 核心原理
1. 握手升级(Upgrade)
- WebSocket 不是独立的协议,而是 HTTP 协议的升级:
客户端请求(标准 HTTP):
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket ← 关键头:要求升级
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== ← Base64 随机密钥
Sec-WebSocket-Version: 13
服务端响应(101 Switching Protocols):
HTTP/1.1 101 Switching Protocols ← 状态码 101 表示协议切换
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= ← 密钥验证
关键点:
Sec-WebSocket-Key+ 魔法字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11→ SHA-1 → Base64 =Sec-WebSocket-Accept- 成功后,TCP 连接从 HTTP 模式切换为 WebSocket 帧传输模式
2. 帧结构(Frame)
握手后,数据以 帧(Frame) 为单位传输,不再是 HTTP 报文:
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 |
+-------------------------------- - - - - - - - - - - - - - - -+
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
简化理解:
FIN:是否为最后一帧(分片时用)opcode:帧类型(1=文本,2=二进制,8=关闭,9=ping,10=pong)MASK:客户端→服务端必须掩码(防止代理缓存污染攻击)Payload:实际数据(文本是 UTF-8,二进制原样传输)
3. 生命周期状态
CONNECTING (0) → 连接正在建立中
↓
OPEN (1) → 连接成功,可以通信
↓
CLOSING (2) → 连接正在关闭(收到/发送关闭帧)
↓
CLOSED (3) → 连接已关闭或无法建立
1.3 浏览器端 API 详解
基础用法
// 1. 创建连接
const ws = new WebSocket('wss://echo.websocket.org'); // wss = WebSocket Secure (TLS)
// 2. 事件监听
ws.onopen = (event) => {
console.log('连接建立', event);
ws.send('Hello Server!'); // 发送文本
};
ws.onmessage = (event) => {
console.log('收到消息:', event.data); // 可能是字符串或 Blob
};
ws.onerror = (error) => {
console.error('连接错误:', error);
};
ws.onclose = (event) => {
console.log('连接关闭', event.code, event.reason);
// code: 1000(正常关闭), 1006(异常断开), 1011(服务器错误)等
};
// 3. 主动关闭
ws.close(1000, '用户主动退出'); // code 和 reason 可选
进阶技巧
// ===== 发送二进制数据 =====
const buffer = new ArrayBuffer(128);
const view = new Uint8Array(buffer);
view[0] = 0x01;
ws.send(buffer); // 自动识别为二进制帧
// 或发送 Blob(文件)
const file = document.getElementById('file').files[0];
ws.send(file);
// ===== 接收二进制数据 =====
ws.binaryType = 'arraybuffer'; // 默认 'blob'
ws.onmessage = (e) => {
if (e.data instanceof ArrayBuffer) {
const view = new DataView(e.data);
console.log(view.getInt32(0)); // 解析二进制协议
}
};
// ===== 心跳检测(防止 NAT 超时断开)=====
let heartbeatInterval;
function startHeartbeat() {
heartbeatInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000); // 30秒一次
}
// ===== 断线重连 =====
class ReconnectingWebSocket {
constructor(url, protocols = []) {
this.url = url;
this.protocols = protocols;
this.reconnectInterval = 1000;
this.maxReconnectInterval = 30000;
this.ws = null;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url, this.protocols);
this.ws.onclose = () => {
setTimeout(() => this.connect(), this.reconnectInterval);
this.reconnectInterval = Math.min(
this.reconnectInterval * 2,
this.maxReconnectInterval
);
};
// 代理其他方法...
this.send = (data) => this.ws.send(data);
}
}
1.4 服务端实现(Python)
使用 websockets 库(推荐)
import asyncio
import websockets
import json
connected = set()
async def handler(websocket, path):
# 注册客户端
connected.add(websocket)
print(f"客户端加入,当前在线: {len(connected)}")
try:
async for message in websocket:
# 解析消息
data = json.loads(message)
print(f"收到: {data}")
# 广播给所有客户端(除发送者)
for conn in connected:
if conn != websocket:
await conn.send(json.dumps({
"from": data.get("user"),
"text": data.get("text"),
"time": asyncio.get_event_loop().time()
}))
except websockets.exceptions.ConnectionClosed:
print("客户端断开")
finally:
connected.remove(websocket)
print(f"客户端离开,当前在线: {len(connected)}")
# 启动服务器
start_server = websockets.serve(handler, "localhost", 8765)
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()
使用 FastAPI(现代选择)
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.responses import HTMLResponse
app = FastAPI()
class ConnectionManager:
def __init__(self):
self.active_connections: list[WebSocket] = []
async def connect(self, websocket: WebSocket):
await websocket.accept()
self.active_connections.append(websocket)
def disconnect(self, websocket: WebSocket):
self.active_connections.remove(websocket)
async def broadcast(self, message: str):
for connection in self.active_connections:
await connection.send_text(message)
manager = ConnectionManager()
@app.websocket("/ws/{client_id}")
async def websocket_endpoint(websocket: WebSocket, client_id: int):
await manager.connect(websocket)
try:
while True:
data = await websocket.receive_text()
await manager.broadcast(f"用户{client_id}: {data}")
except WebSocketDisconnect:
manager.disconnect(websocket)
await manager.broadcast(f"用户{client_id} 离开了")
1.5 实际应用场景
| 场景 | 为什么用 WebSocket | 替代方案对比 |
|---|---|---|
| 在线游戏 | 低延迟操作同步(<50ms) | UDP 更快但需自定义可靠性 |
| 股票/加密货币行情 | 服务端主动推送价格变动 | SSE(Server-Sent Events)仅单向 |
| 协同编辑 | 实时同步光标位置和文本 | 轮询冲突严重,OT 算法需实时 |
| 即时通讯 | 消息实时送达+已读回执 | MQTT 更适合物联网弱网环境 |
| IoT 设备控制 | 双向控制指令+状态上报 | CoAP 更轻量但生态较小 |
| 直播弹幕 | 高并发实时消息广播 | 长轮询服务器扛不住 |
选型决策树 : WebSocket | TCP/UDP Socket | HTTP SSE | HTTP API (必读)
需要双向实时通信?
├── 是 → 需要浏览器支持?
│ ├── 是 → WebSocket ✅
│ └── 否 → 原生 TCP/UDP Socket
└── 否 → 仅需服务端推送?
├── 是 → SSE(更简单,自动重连,基于 HTTP)
└── 否 → 普通 HTTP API
1.6 常见问题与最佳实践
❌ 初级工程师常犯错误
// 错误1:不检查连接状态就发送
ws.send('data'); // 可能 CONNECTING 或 CLOSED,会抛异常
// 正确做法:
if (ws.readyState === WebSocket.OPEN) {
ws.send('data');
}
// 错误2:同步发送大量数据(阻塞事件循环)
for (let i = 0; i < 100000; i++) {
ws.send(bigData); // 内存暴涨,连接可能断开
}
// 正确做法:分片 + 流控
async function sendLargeData(dataChunks) {
for (const chunk of dataChunks) {
ws.send(chunk);
await new Promise(r => setTimeout(r, 10)); // 让出事件循环
}
}
// 错误3:忽略错误处理导致崩溃
ws.onerror = (e) => {
throw e; // 不要这样做!
};
// 正确做法:
ws.onerror = (e) => {
console.error('WebSocket error:', e);
// 触发重连逻辑
};
✅ 生产环境 checklist
- 使用
WSS(WebSocket Secure):生产环境必须用 TLS 加密(wss://),防止中间人攻击 - 鉴权:在 URL 参数或子协议中传递 Token,握手时验证
const ws = new WebSocket('wss://api.example.com/ws?token=xyz'); - 限流:防止单个客户端发送过多消息(每秒 N 条)
- 分片:单帧大小限制(如 1MB),超大消息分多帧发送
- 优雅关闭:发送 Close 帧后再断开,避免数据丢失
- 监控:记录连接数、消息吞吐量、延迟分布
1.7 调试工具推荐
| 工具 | 用途 |
|---|---|
| Chrome DevTools → Network → WS | 查看帧级别的收发数据 |
| Postman / Insomnia | 手动测试 WebSocket 接口 |
wscat (npm install -g wscat) |
命令行调试:wscat -c ws://localhost:8765 |
| Wireshark | 抓包分析 WebSocket 帧结构(过滤 websocket) |
1.X 总结
WebSocket 的核心价值在于 "用 HTTP 兼容的方式实现 TCP 级别的实时双向通信"。作为初级工程师,你需要记住:
- 握手阶段是 HTTP,成功后变成轻量级帧传输
- 全双工 = 客户端和服务端可同时主动发送数据
- 长连接 = 需要处理断线重连、心跳保活
- 应用场景 = 游戏、聊天、协同、实时数据推送
2 案例实践
CASE 基于 python + websocket 的简易Web版聊天室
Z FAQ for Websocket
Y 推荐文献
X 参考文献
本文作者:
千千寰宇
本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!
本文链接: https://www.cnblogs.com/johnnyzen
关于博文:评论和私信会在第一时间回复,或直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
日常交流:大数据与软件开发-QQ交流群: 774386015 【入群二维码】参见左下角。您的支持、鼓励是博主技术写作的重要动力!

浙公网安备 33010602011771号