Websocket
参考资料
- https://m.elecfans.com/article/2362854.html
- https://www.ruanyifeng.com/blog/2017/05/websocket.html
- https://blog.csdn.net/Rocky006/article/details/140705966
简介
一个即时聊天、多人协作的 web 应用, 其客户端和服务器之间常见的 双向数据交换方式 有:
- 短轮询
- 长轮询
- SSE(Server-Sent-Events)
以上方法存在着 效率、网络带宽利用率 等等的问题
WebSocket 是一种 在单个 TCP 连接上实现全双工(双向)通信 的协议
它由 RFC 6455(2011 年)正式定义, 旨在解决传统 HTTP 请求/响应模型在实时交互场景下的低效问题
为什么需要 WebSocket
HTTP 协议有一个缺陷:通信只能由客户端发起, 服务器无法主动向客户端推送信息, 即单向请求
如果服务器有连续的状态变化, 客户端要获知就非常麻烦
而 轮询 效率低, 且浪费资源(客户端每隔一段时间就发出一个询问)
WebSocket 应运而生
- 常见使用场景
场景 典型需求 WebSocket 优势 即时聊天 双向低延迟文本传输 实时推送、低开销 多人在线游戏 高频率的二进制状态更新 二进制帧、低延迟 实时仪表盘/监控 推送监控数据、图表刷新 服务器主动推送 协同编辑(文档、代码) 同步编辑操作 多用户即时广播 物联网(IoT) 设备状态上报、指令下发 支持子协议(如 MQTT) 金融行情 高频行情推送 减少网络延迟、带宽占用低 推送通知 服务器主动下发消息 取代轮询/长轮询, 提高效率
WebSocket 特点
- 全双工:客户端和服务器可以同时主动发送数据, 而不需要像 AJAX 那样每次都发起新的 HTTP 请求
- 低开销:建立一次连接后, 后续的数据帧只包含很少的协议头部, 避免了重复的 TCP/HTTP 握手开销
- 实时性:适用于聊天、在线游戏、股票行情、物联网数据推送等需要 低延迟 的场景
- 没有同源限制, 客户端可以与任意服务器通信
- 协议标识符是 ws(如果加密, 则为 wss), 服务器网址就是 URL, 例如:
wss://echo.websocket.org
| 对比维度 | HTTP(请求/响应) | WebSocket |
|---|---|---|
| 通信模式 | 单向(客户端发起请求, 服务器返回响应) | 双向(任意一端都可主动发送) |
| 连接复用 | 每次请求都要建立 TCP 连接(或使用 Keep‑Alive 复用, 但仍是请求/响应) | 建立一次后保持长连接, 持续通信 |
| 协议开销 | 每次请求都携带完整的 HTTP 头部 | 只在握手阶段有 HTTP 头部, 后续帧仅几字节 |
| 实时性 | 轮询/长轮询需要额外延迟 | 立即推送, 毫秒级延迟 |
| 安全 | 支持 HTTP/HTTPS | 支持 ws://(明文) 和 wss://(TLS) |
建立连接的握手过程
客户端
使用 HTTP/1.1 的 Upgrade 机制进行一次“升级”握手
客户端一旦发送了连接握手请求, 就必须等待服务器的响应
客户端连接握手请求的 header 部分字段:
|
|
请求 URI 格式
ws://host:port/path?query / wss://host:port/path?query
端口号和查询值是可选的
Sec-WebSocket-Key 是客户端为本次建连随机生成的 16 字节的 base64 编码的字符串
Sec-WebSocket-Version 是客户端拟使用协议版本号, 值必须为 13
Sec-WebSocket-Protocol 是客户端支持的一个或多个以逗号分割的子协议, 按优先级排序
Sec-WebSocket-Extensions 客户端拟使用协议扩展, 例如多路复用扩展、压缩扩展
服务器端
服务器收到客户端的连接握手请求时, 必须进行回复
返回 101 Switching Protocols 响应
回复 header 部分字段如下:
|
|
HTTP/1.1 101 Switching Protocols 表示接受客户端的请求
若服务器想要停止处理握手, 可以返回 401 等类型的错误代码的 HTTP 响应
服务器接受客户端的请求后, 先将客户端请求头的 Sec-WebSocket-Key 值与 全局唯一标识 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接
然后对拼接的字符串进行 SHA-1 哈希, 再进行 base64-encoded, 最终得到 Sec-WebSocket-Accept
消息传输
建连握手成功后, 连接从 HTTP 切换为 WebSocket
客户端和服务器就可以开始数据传输了, 进入帧(frame)层的通信
WebSocket 帧格式
WebSocket 把每一条消息切分为 帧, 帧的二进制结构如下
|
|
-
FIN
1 bit, 表示是否是一条消息的最后一帧
1 表示结束, 0 表示还有后续帧 -
RSV1, RSV3, RSV3
分别为 1 bit, 扩展功能未使用的情况下, 默认为 0
可用于扩展如 permessage-deflate -
opcode
4 bit, 用于表示帧 payload data 的数据类型
例如十进制、二进制、文本, 还有连接关闭、心跳帧等等0x0 - Continuation 续帧
0x1 - Text utf-8 文本
0x2 - Binary 二进制
0x8 - Close lia 连接关闭
0x9 - Ping 心跳帧
0xA - Pong -
MASK
1 bit, 是否进行掩码, 是为 1, 否为 0
客户端发送的帧必须进行掩码, 服务器端发送到可以不用 -
Payload length 和 Extended payload length
Payload length 为 7 位
Extended payload length 为 16 或者 64 位
如果 payload data 的长度 <= 125, 则使用 Payload length 表示其长度
如果 payload data 的长度 == 126, 则使用 Extended payload length 的后 16 位表示
如果 payload data 的长度 == 127, 则使用 Extended payload length 的后 64 位表示 -
Masking-key 掩码
32 位, 是由 客户端生成并发送给服务器 的掩码
用于在数据传输过程中对数据进行掩码处理:为了防止 代理缓存污染 攻击, 掩码必须来自强大的熵源, 不可被预测
常规的算法:以字节为步长遍历载荷数据, 对于载荷数据的第 i 个字节, 对 4 取模得到 j(只能为 0、1、2、3)
然后将第 i 个字节与 masking-key 的第 j 个字节做 按位异或 操作(相同为 0, 不同为 1), 混淆原始数据代理缓存污染攻击
缓存污染攻击是一种安全攻击, 攻击者通过发送 恶意构造的数据包, 使得 代理服务器 错误地将这些数据存储在缓存中
从而可能导致其他用户接收到错误的数据 -
payload data
真正的业务数据, 为 文本或者二进制, 分为:扩展数据、应用数据
心跳 / Keep‑Alive(Ping‑Pong)
- Ping:服务器(或客户端)发送 opcode=0x9 的 Ping 帧, 携带可选负载。
- Pong:对方必须在收到 Ping 后 立刻回送 Pong 帧(opcode=0xA), 负载必须相同(若有)。
作用:
- 防止 NAT/防火墙在长时间无数据时关闭连接。
- 检测对端是否仍然活跃。
- 在
ws(Node)等库中可以配置heartbeat机制(如每 30 s 发送一次 Ping)。
消息分片
将概念上的一条消息通过多个数据帧发送
允许发送未知大小的消息, 而不必缓冲整条消息
并且可以结合多路复用协议的扩展, 实现分割消息为更小的分段以共享输出通道
要求分片数据帧 按顺序 发送到另一端
断连握手
客户端和服务器都可以发送包含指定控制序列的 Close 控制帧
一方发送出关闭控制帧后, 另一方接收到只需要发送一个关闭帧作为响应
然后关闭连接
常用子协议(Subprotocol)
在握手阶段, 客户端可以通过 Sec-WebSocket-Protocol 头部列出它支持的 子协议(例如 chat、json、mqtt), 服务器从中挑选一个并在响应中返回
子协议本质上是 在同一个连接上协商的业务层协议, 常见的有:
graphql-ws(GraphQL 实时订阅)mqtt(IoT 场景)wamp(Web Application Messaging Protocol)json-rpc(JSON‑RPC over WebSocket)
如果不需要子协议, 直接省略该头部即可
安全
-
ws://明文协议, 等同于普通 TCP, 数据在网络上未加密
适用于局域网或安全已受信的环境 -
wss://在 TLS(HTTPS)之上运行的 WebSocket, 数据在传输过程中经过加密
生产环境、跨公网通信、涉及敏感信息时强烈推荐使用。
TLS 握手 将在原始 TCP 握手完成后进行, 随后进入 WebSocket 握手阶段
多数现代浏览器仅在 https:// 页面中允许 wss:// 连接, 以防混合内容(mixed content)安全风险
常见错误码(Close Code)
| 码 | 含义 | 参考 |
|---|---|---|
| 1000 | 正常关闭(Normal Closure) | 正常业务结束 |
| 1001 | 因端点离开(Going Away) | 服务器关闭、页面刷新 |
| 1002 | 协议错误(Protocol error) | 握手或帧格式错误 |
| 1003 | 不接受的数据类型(Unsupported Data) | 收到不支持的二进制/文本 |
| 1006 | 非法关闭(Abnormal Closure) (不在帧中出现) | 连接异常断开 |
| 1007 | 无效负载数据(Invalid payload data) | 例如不是有效 UTF‑8 |
| 1011 | 服务器内部错误(Server error) | 服务器内部异常 |
| 1015 | TLS 握手失败 (仅在 TLS 中出现) | TLS 失败或证书错误 |
建议:在业务层捕获关闭事件, 并依据返回码执行相应的重连或清理逻辑。
性能与扩展
| 场景 | 优化点 |
|---|---|
| 高并发(大量连接) | 使用 epoll/kqueue 事件模型(Node.js、Go、Rust)或 Nginx + ngx_http_websocket_module 做负载均衡。 |
| 大消息(文件、二进制) | 开启 permessage-deflate(压缩)或自行切分成更小的帧;使用 binaryType = 'arraybuffer' 减少内存拷贝。 |
| 分布式广播 | 使用 消息队列(Kafka、Redis Pub/Sub)配合多实例 WebSocket 服务器实现全局广播。 |
| 安全 | 使用 JWT 在握手时通过 Sec-WebSocket-Protocol 传递 token, 或在 HTTP 升级请求里使用 Cookie/Authorization Header。 |
| 流控 | 在服务器端对每个连接设定 发送速率限制, 防止单个客户端占用过多带宽。 |
| 横向扩展 | 将 WebSocket 服务器放在 容器/微服务 中, 配合 Ingress(K8s)或 云负载均衡 实现弹性伸缩。 |
与其他实时技术的对比
| 技术 | 双向通信 | 传输层 | 编码/协议 | 典型场景 |
|---|---|---|---|---|
| WebSocket | ✅(全双工) | TCP | 自定义二进制/文本帧 | 聊天、游戏、实时监控 |
| Server‑Sent Events (SSE) | ❌(服务器 → 客户端) | HTTP/1.1(长连接) | 纯文本流(UTF‑8) | 实时日志、新闻推送 |
| Long Polling | ✅(伪双向) | HTTP | 标准请求/响应 | 兼容旧浏览器 |
| HTTP/2 Push | ✅(单向) | HTTP/2 | 二进制帧(SETTINGS, PUSH_PROMISE) | 资源预加载 |
| gRPC‑Web | ✅(双向) | HTTP/2 | protobuf | 微服务间的高效 RPC |
| WebRTC DataChannel | ✅(双向+P2P) | UDP + SCTP | 任意二进制 | 浏览器点对点文件传输、多人游戏 |
速查表
| 检查项 | 一句话记忆 |
|---|---|
| 握手 | Key + Magic → SHA-1 → Base64, 返回 101 |
| 掩码 | 浏览器 → 服务器必须掩码, 方向反了直接 1002 |
| 心跳 | 30 s 一次 Ping, 无 Pong 触发重连 |
| 关闭码 | 1000 正常, 1006 异常, 1011 服务端崩溃 |
| 子协议 | 前后端名单必须交集非空, 否则握手失败 |
| 生产地址 | 一律 wss://, 配 TLS 1.3 + ALPN=h2 |
| 高并发 | 单进程 1 万连接 ≈ 240 MB 内存(epoll + permessage-deflate 关) |
| 断线重连 | 指数退避:1 s → 2 s → 4 s … 上限 30 s |
| 广播 | 本地集合适用于单机, 多机用 Redis Pub/Sub |
| 安全头 | Content-Security-Policy: connect-src wss://*.example.com |
示例
前端 JS
|
|
-
常用 API
方法 / 属性 说明 new WebSocket(url, protocols?)url必须以ws://或wss://开头;protocols可选, 用于子协议协商。socket.readyState0(CONNECTING)、1(OPEN)、2(CLOSING)、3(CLOSED)。 socket.send(data)data可以是String(文本帧)或Blob/ArrayBuffer/TypedArray(二进制帧)。socket.close([code, reason])主动关闭连接, 可提供关闭码(如 1000 正常关闭)和原因。 socket.binaryType默认为 'blob', 可改为'arraybuffer'控制收到二进制时的类型。 -
处理断线重连(Reconnection)
WebSocket 本身不提供自动重连, 需要在业务层自行实现。常见策略:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28let socket; let reconnectAttempts = 0; const maxAttempts = 5; function connect() { socket = new WebSocket("wss://example.com/ws"); socket.addEventListener("open", () => { console.log("已连接"); reconnectAttempts = 0; // 成功后重置计数 }); socket.addEventListener("close", () => { if (reconnectAttempts < maxAttempts) { const delay = Math.pow(2, reconnectAttempts) * 1000; // 指数退避 console.log(`将在 ${delay}ms 后重连…`); setTimeout(() => { reconnectAttempts++; connect(); }, delay); } else { console.warn("已超过最大重连次数"); } }); // 其它事件(message、error)同理 } connect();- 指数退避(Exponential Backoff)可以防止在网络故障时产生洪水式请求。
- 保留未发送的消息:在断线期间缓存待发送的数据, 待重连成功后再发送。
后端 Node.js
|
|
|
|
-
常见配置项
参数 说明 { server }将 WebSocket.Server“挂载”到已有的 HTTP/HTTPS 服务器上。port直接创建独立的 WS 服务器(不依赖 HTTP)。 verifyClient用于在握手阶段进行身份验证(返回 true/false或调用回调)。handleProtocols用于子协议协商, 自定义返回子协议名称。 perMessageDeflate启用/禁用 permessage-deflate(压缩扩展), 默认开启。生产环境推荐:使用 HTTPS + wss, 并在握手阶段进行 身份认证(如 JWT、Session Cookie), 防止未授权的连接。
后端 Python
|
|
websockets 是一个基于 asyncio 的库
|
|
基本功能
-
创建服务器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17# server.py import asyncio import websockets async def handler(websocket, path): print(f"Client connected: {path}") await websocket.send("Welcome from Python server!") async for message in websocket: print(f"Received: {message}") await websocket.send(f"Echo: {message}") start_server = websockets.serve(handler, "localhost", 8765, ssl=None) # 若要 wss, 需要提供 SSLContext asyncio.get_event_loop().run_until_complete(start_server) print("WebSocket server listening on ws://localhost:8765") asyncio.get_event_loop().run_forever() -
创建客户端
1 2 3 4 5 6 7 8 9 10 11# client.py import asyncio import websockets async def hello(): async with websockets.connect("ws://localhost:8765") as ws: await ws.send("Hello Server") reply = await ws.recv() print(f"Server replied: {reply}") asyncio.run(hello()) -
广播
将消息发送给所有连接到服务器的客户端 通过设置一个集合, 存放连接的客户端 每次发送消息的时候, 遍历客户端集合进行 send 最后记得将套接字移出集合
-
SSL/TLS 加密
通过导入 ssl 库对内容进行加密 然后 websockets.serve() 可以接受一个 ssl 参数
-
自定义协议
用于扩展 websocket 的功能 通过继承 websockets.WebSocketServerProtocol 自定义类
实际应用
-
实时聊天应用
采用上面说的广播机制
-
实时数据流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17import asyncio import websockets import random import time async def data_stream(websocket, path): while True: # 模拟数据流 data = random.randint(1, 100) await websocket.send(str(data)) await asyncio.sleep(1) start_server = websockets.serve(echo, 'localhost', 6789) asyncio.get_event_loop().run_until_complete(start_server) asyncio.get_event_loop().run_forever()
手动实现 WebSocket 协议
|
|
小练习
把“手动实现”改成 asyncio + no external lib, 单文件跑通浏览器回声测试, 加深帧边界处理印象
用 Chrome DevTools → Network → WS 过滤, 故意发一个 126 长度帧, 抓包确认 Extended payload length 字段字节序
给 Node 示例加上 verifyClient 做 JWT 校验, 拒绝无 token 连接, 体验“握手阶段即鉴权”