跳到主要内容

WebSocket 协议

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,由 RFC 6455 定义。它解决了 HTTP 无法实现服务器主动推送的问题,成为实时 Web 应用的核心技术。

WebSocket 概述

为什么需要 WebSocket?

在 WebSocket 出现之前,实现实时通信有以下方案:

短轮询:客户端定期发送 HTTP 请求,检查是否有新数据。问题:大量无效请求,浪费带宽和服务器资源。

长轮询:客户端发送请求,服务器保持连接直到有数据或超时。问题:连接开销大,实现复杂。

HTTP 流:服务器保持连接打开,持续发送数据。问题:只能单向通信,无法实现真正的双向通信。

这些方案都基于 HTTP,存在固有的局限性:HTTP 是请求-响应模型,服务器无法主动向客户端发送数据。

WebSocket 的特点

全双工通信:客户端和服务器可以同时发送和接收数据,真正的双向通信。

单一 TCP 连接:一次握手后,持续使用同一个连接,避免重复建立连接的开销。

低开销:数据帧首部最小只有 2 字节,比 HTTP 头部小得多。

实时性:服务器可以主动推送数据,无需客户端轮询。

与 HTTP 兼容:握手阶段使用 HTTP,可以复用端口和基础设施。

WebSocket 与 HTTP 的关系

WebSocket 起源于 HTTP,但独立于 HTTP:

客户端                                    服务器
| |
| HTTP GET /chat (Upgrade: websocket) |
|--------------------------------------->|
| |
| HTTP 101 Switching Protocols |
|<---------------------------------------|
| |
| WebSocket 数据帧 |
|<-------------------------------------->|
| WebSocket 数据帧 |
|<-------------------------------------->|

握手成功后,协议从 HTTP 切换到 WebSocket,后续通信使用 WebSocket 协议。

WebSocket 握手

客户端请求

WebSocket 握手使用 HTTP GET 请求,包含特殊的头部:

GET /chat HTTP/1.1
Host: example.com:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Extensions: permessage-deflate

关键头部

  • Upgrade: websocket:请求协议升级
  • Connection: Upgrade:表示这是协议升级请求
  • Sec-WebSocket-Key:Base64 编码的 16 字节随机值,用于握手验证
  • Sec-WebSocket-Version:WebSocket 版本,当前为 13
  • Origin:请求来源,用于安全验证
  • Sec-WebSocket-Protocol:子协议列表(可选)
  • Sec-WebSocket-Extensions:扩展列表(可选)

服务器响应

服务器同意升级时返回 101 状态码:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Extensions: permessage-deflate

Sec-WebSocket-Accept 计算

Sec-WebSocket-Accept = Base64(SHA1(Sec-WebSocket-Key + GUID))

GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"

服务器将客户端发送的 Key 与固定 GUID 拼接,进行 SHA-1 哈希后 Base64 编码。客户端验证这个值,确保服务器真正支持 WebSocket。

握手验证示例

import base64
import hashlib

def compute_accept(key):
GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
sha1 = hashlib.sha1((key + GUID).encode()).digest()
return base64.b64encode(sha1).decode()

key = "dGhlIHNhbXBsZSBub25jZQ=="
accept = compute_accept(key)
print(accept) # s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

WebSocket 数据帧

帧格式

WebSocket 数据帧格式:

 0               1               2               3
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-------+-+-------------+-------------------------------+
|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(1 位):表示是否是消息的最后一个帧。1 表示最后一帧,0 表示还有后续帧。

RSV1-3(各 1 位):保留位,用于扩展。如果协商了扩展,可以非零。

Opcode(4 位):操作码,表示帧类型:

Opcode含义
0x0继续帧
0x1文本帧
0x2二进制帧
0x3-7保留
0x8关闭帧
0x9Ping 帧
0xAPong 帧
0xB-F保留

MASK(1 位):是否使用掩码。客户端发送的帧必须掩码,服务器发送的帧不掩码。

Payload length(7 位):载荷长度:

  • 0-125:实际长度
  • 126:后 2 字节为实际长度
  • 127:后 8 字节为实际长度

Masking-key(32 位):掩码密钥,仅当 MASK=1 时存在。

Payload Data:实际数据,如果 MASK=1,需要解码。

掩码算法

客户端发送的数据必须掩码:

def mask_data(data, masking_key):
result = bytearray(len(data))
for i in range(len(data)):
result[i] = data[i] ^ masking_key[i % 4]
return bytes(result)

# 示例
masking_key = bytes([0x12, 0x34, 0x56, 0x78])
data = b"Hello"
masked = mask_data(data, masking_key)
unmasked = mask_data(masked, masking_key) # 解码使用相同算法

分片

大消息可以分成多个帧传输:

帧1: FIN=0, Opcode=0x1, 数据="Hel"
帧2: FIN=0, Opcode=0x0, 数据="lo "
帧3: FIN=1, Opcode=0x0, 数据="World"

第一帧使用实际的操作码,后续帧使用继续帧(0x0)。只有最后一帧 FIN=1。

控制帧

Ping 和 Pong

Ping 用于心跳检测,收到 Ping 必须回复 Pong:

客户端 -> 服务器: Ping (Opcode=0x9, 可携带数据)
服务器 -> 客户端: Pong (Opcode=0xA, 相同的数据)

Pong 也可以主动发送,表示仍然存活。

关闭帧

关闭帧用于优雅关闭连接:

 0               1               2               3
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Status code (if present) |
|I|S|S|S| (8) |A| (7) | (16) |
|N|V|V|V| |S| | |
| |1|2|3| |K| | Reason |
+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +

常见状态码

状态码含义
1000正常关闭
1001端点离开
1002协议错误
1003不支持的数据类型
1007无效数据
1008策略违规
1009消息过大
1010缺少扩展
1011内部错误
1012服务重启
1013稍后重试

WebSocket 编程

JavaScript 客户端

浏览器原生支持 WebSocket API:

const ws = new WebSocket('wss://example.com/chat');

ws.onopen = function(event) {
console.log('连接已建立');
ws.send('Hello Server!');
};

ws.onmessage = function(event) {
console.log('收到消息:', event.data);

if (event.data instanceof Blob) {
// 处理二进制数据
} else {
// 处理文本数据
}
};

ws.onerror = function(error) {
console.error('WebSocket 错误:', error);
};

ws.onclose = function(event) {
console.log('连接已关闭:', event.code, event.reason);

if (event.wasClean) {
console.log('正常关闭');
} else {
console.log('异常关闭');
}
};

ws.send('文本消息');
ws.send(new ArrayBuffer(10)); // 二进制数据
ws.send(new Blob(['blob data']));

ws.close(1000, '正常关闭');

Python 服务器

使用 websockets 库:

import asyncio
import websockets

async def handler(websocket):
print(f"客户端连接: {websocket.remote_address}")

try:
async for message in websocket:
print(f"收到消息: {message}")
await websocket.send(f"Echo: {message}")
except websockets.exceptions.ConnectionClosed:
print("客户端断开连接")

async def main():
async with websockets.serve(handler, "localhost", 8765):
print("WebSocket 服务器启动在 ws://localhost:8765")
await asyncio.Future()

asyncio.run(main())

Python 客户端

import asyncio
import websockets

async def client():
uri = "ws://localhost:8765"
async with websockets.connect(uri) as websocket:
await websocket.send("Hello Server!")
response = await websocket.recv()
print(f"收到响应: {response}")

asyncio.run(client())

Node.js 服务器

使用 ws 库:

const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', function connection(ws, req) {
const ip = req.socket.remoteAddress;
console.log(`客户端连接: ${ip}`);

ws.on('message', function message(data) {
console.log(`收到消息: ${data}`);
ws.send(`Echo: ${data}`);
});

ws.on('close', function close(code, reason) {
console.log(`客户端断开: ${code} ${reason}`);
});

ws.on('error', function error(err) {
console.error(`错误: ${err}`);
});

ws.send('欢迎连接!');
});

console.log('WebSocket 服务器启动在 ws://localhost:8080');

WebSocket 子协议

WebSocket 允许使用子协议,定义消息格式和语义:

STOMP

STOMP(Simple Text Oriented Messaging Protocol)是基于帧的消息协议:

CONNECT
accept-version:1.2
host:stomp.example.com

^@

SUBSCRIBE
id:0
destination:/queue/messages
ack:client

^@

SEND
destination:/queue/messages
content-type:text/plain

Hello World^@

WAMP

WAMP(WebSocket Application Messaging Protocol)支持 RPC 和发布订阅:

// RPC 调用
[48, 123456, "com.example.add", [1, 2]]

// RPC 结果
[50, 123456, 3]

// 发布消息
[16, "com.example.topic", {"message": "hello"}]

WebSocket 扩展

压缩扩展

permessage-deflate 扩展支持消息压缩:

Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

压缩可以显著减少数据传输量,但增加 CPU 开销。

心跳机制

WebSocket 本身没有内置心跳,需要应用层实现:

let heartbeatInterval;

function startHeartbeat() {
heartbeatInterval = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.ping();
}
}, 30000);
}

ws.on('pong', () => {
console.log('收到 Pong');
});

ws.on('close', () => {
clearInterval(heartbeatInterval);
});

重连机制

连接断开后自动重连:

let ws;
let reconnectInterval = 1000;
let maxReconnectInterval = 30000;

function connect() {
ws = new WebSocket('wss://example.com/chat');

ws.onopen = () => {
console.log('连接成功');
reconnectInterval = 1000;
};

ws.onclose = () => {
console.log('连接断开,准备重连...');
setTimeout(connect, reconnectInterval);
reconnectInterval = Math.min(reconnectInterval * 2, maxReconnectInterval);
};
}

connect();

WebSocket 安全

wss 协议

使用 TLS 加密的 WebSocket 连接:

ws://example.com/chat    # 不安全
wss://example.com/chat # 加密

Origin 验证

服务器验证请求来源:

async def handler(websocket):
origin = websocket.request_headers.get('Origin')
if origin not in allowed_origins:
await websocket.close(1008, 'Origin not allowed')
return

认证

WebSocket 没有内置认证,需要在握手时处理:

HTTP 头部认证

async def handler(websocket):
auth = websocket.request_headers.get('Authorization')
if not validate_auth(auth):
await websocket.close(1008, 'Unauthorized')
return

URL 参数认证

const ws = new WebSocket('wss://example.com/chat?token=xxx');

Cookie 认证

浏览器发送 WebSocket 请求时会携带同域 Cookie。

WebSocket vs HTTP 长轮询

特性WebSocketHTTP 长轮询
连接持久连接每次请求建立连接
通信方式全双工半双工
延迟
带宽低开销HTTP 头部开销
实现复杂度中等简单
浏览器支持现代浏览器所有浏览器
代理兼容性可能有问题兼容性好

小结

WebSocket 是实时 Web 应用的核心技术:

  1. 全双工通信:客户端和服务器可以同时发送数据
  2. 握手过程:基于 HTTP 升级,使用 Sec-WebSocket-Key 验证
  3. 数据帧:支持文本和二进制数据,支持分片
  4. 控制帧:Ping/Pong 心跳,Close 关闭连接
  5. 安全:使用 wss 加密,验证 Origin

WebSocket 适用于聊天应用、实时协作、在线游戏、股票行情等需要实时通信的场景。

练习

  1. 描述 WebSocket 握手过程和 Sec-WebSocket-Accept 的计算方法
  2. 解释 WebSocket 数据帧格式和掩码算法
  3. 实现 WebSocket 客户端和服务器的心跳机制
  4. 比较 WebSocket 和 HTTP 长轮询的优缺点