WebSocket 深度解析

打破 HTTP 的枷锁,开启实时双向通信的新纪元

RFC 6455 · 2024.12

1. 什么是 WebSocket?

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它允许服务端主动向客户端推送数据,彻底改变了 HTTP “请求-响应”的单向通信模式。

核心特点:

  • 建立在 TCP 之上:基于可靠的 TCP 协议,保证数据传输的稳定性和完整性。
  • 复用 HTTP 握手:通过 HTTP Upgrade 机制建立连接,兼容现有网络基础设施。
  • 全双工:数据可以同时双向流动,服务端和客户端地位平等。
  • 轻量级:数据帧头部最小仅 2 字节,相比 HTTP 几百字节头部大大减少开销。
  • 持久连接:一次握手,长久使用,避免频繁建立连接的开销。

协议标识:ws:// 表示普通 WebSocket 连接(端口 80),wss:// 表示加密连接(端口 443),类似于 HTTP 和 HTTPS 的关系。

2. 为什么需要 WebSocket?

在 WebSocket 出现之前,实现“实时通信”必须依赖以下两种变通方案:

短轮询 (Short Polling)

客户端每隔固定时间(如 3 秒)发起 HTTP 请求查询新数据。

  • 大量空请求浪费带宽
  • 实时性差(有轮询间隔延迟)
  • 服务器压力大

长轮询 (Long Polling)

客户端发起请求,服务器挂起直到有新数据才返回。

  • 服务器维护大量挂起连接
  • 每次重连都要重发 HTTP 头
  • 仍是单向通信模式

WebSocket 解决方案

一次握手,建立持久连接,数据双向实时流动,彻底解决上述痛点。

HTTP 短轮询 HTTP 长轮询 WebSocket Req Empty 频繁连接,浪费资源 Req Hold... Data 服务器挂起等待 Upgrade 101 持久连接,双向互通

3. WebSocket 工作原理

WebSocket 的生命周期分为两个阶段:握手升级数据传输

握手过程 (Handshake)

客户端发送带有 Upgrade: websocket 头的 HTTP 请求,服务器若支持则返回 101 Switching Protocols,协议即从 HTTP 升级为 WebSocket。

关键请求头字段:

字段 说明
Upgrade: websocket 声明协议升级类型
Connection: Upgrade 表示需要升级连接
Sec-WebSocket-Key Base64 编码的随机值,用于安全验证
Sec-WebSocket-Version 协议版本,当前为 13

readyState 连接状态

WebSocket 对象的 readyState 属性表示当前连接状态:

常量 说明
0 CONNECTING 正在建立连接
1 OPEN 连接已建立,可正常通信
2 CLOSING 连接正在关闭
3 CLOSED 连接已关闭

数据帧结构 (Data Frame)

连接建立后,双方通过“帧”交换数据。每帧包含极小的头部(最小 2 字节):

字段 位数 说明
FIN 1 bit 1 表示这是消息的最后一帧
RSV 1-3 3 bits 保留位,用于扩展
Opcode 4 bits 操作类型:0x1=文本, 0x2=二进制, 0x8=关闭, 0x9=Ping, 0xA=Pong
MASK 1 bit 客户端发送的数据必须设置为 1
Payload Length 7+ bits 数据长度(可扩展至 16 或 64 位)
1. 协议升级握手 (HTTP Upgrade) GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13 HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo= 2. 数据帧结构 (Data Frame) FIN RSV Opcode(4) MASK Payload Len(7) Extended Payload Length [Optional] Masking Key (32 bits) [If MASK=1] Payload Data 字段说明: FIN: 1=消息最后一帧 Opcode: 1=文本, 2=二进制, 8=关闭 MASK: 客户端发送必须掩码 9=Ping, 10(0xA)=Pong

4. 协议对比分析

HTTP vs WebSocket vs SSE

特性 HTTP SSE (Server-Sent Events) WebSocket
通信方向 单向(客户端→服务器) 单向(服务器→客户端) 双向(全双工)
连接类型 短连接(每次请求新建) 长连接 持久连接
数据格式 文本/二进制 仅文本 文本/二进制
协议开销 每次携带完整头部 仅首次携带头部 最小 2 字节帧头
浏览器支持 全部 大部分(IE 不支持) 全部现代浏览器
自动重连 N/A 内置支持 需手动实现
适用场景 普通网页请求 新闻推送、通知 实时聊天、游戏、协作

选择建议:如果只需服务器向客户端推送数据(如新闻流、股价更新),SSE 是更简单的选择;如果需要双向实时通信,WebSocket 是最佳方案。

性能对比基准

指标 HTTP 轮询 WebSocket 改善幅度
单次消息延迟 100-500ms 1-10ms 50-100x
每条消息开销 ~800 字节 2-14 字节 50-400x
服务器并发能力 ~1万 QPS ~100万 连接 100x
CPU 利用率 高(频繁连接) 低(持久连接) 5-10x

5. WebSocket 主要优势

低延迟

告别 HTTP 的“请求-响应”往返,服务器有数据直接推送,毫秒级触达。

减少带宽

数据帧头部极小(2-14 字节),HTTP 头部通常几百字节,节省 90%+ 开销。

全双工

类似电话通话,双方可同时发言,无需等待对方说完。

持久连接

一次握手长久使用,避免 TCP 三次握手的重复开销。

二进制支持

原生支持二进制数据传输,适合图片、音频、文件等场景。

跨平台

所有现代浏览器、Node.js、Python 等均有完善支持。

6. 典型应用场景

实时聊天

微信、Slack、Discord 等即时通讯应用的核心技术,消息秒达。

在线游戏

实时同步玩家位置、操作、状态,毫秒必争的竞技场景。

金融行情

股票、加密货币 K 线图实时跳动,行情数据零延迟推送。

协同编辑

Google Docs、Figma 多人同时编辑,实时看到对方光标和改动。

IoT 设备控制

智能家居设备实时状态上报和远程控制指令下发。

监控报警

服务器状态、应用日志实时推送到运维面板。

弹幕系统

B站、直播平台弹幕实时滚动,万人同步。

在线客服

实时聊天、消息已读回执、正在输入提示等功能。

7. 客户端代码示例

基础连接示例

在浏览器中使用 WebSocket 非常简单,原生 API 就足够强大:

// 1. 创建连接
const ws = new WebSocket('wss://echo.websocket.org');

// 2. 连接成功回调
ws.onopen = () => {
    console.log('连接已建立!');
    ws.send('Hello Server!'); // 发送数据
};

// 3. 接收消息回调
ws.onmessage = (event) => {
    console.log('收到消息:', event.data);
};

// 4. 连接关闭回调
ws.onclose = (event) => {
    console.log(`连接关闭:码=${event.code} 原因=${event.reason}`);
};

// 5. 错误处理
ws.onerror = (error) => {
    console.error('连接错误:', error);
};

完整示例(带重连与心跳)

class WebSocketClient {
    constructor(url) {
        this.url = url;
        this.ws = null;
        this.reconnectAttempts = 0;
        this.maxReconnectAttempts = 5;
        this.heartbeatInterval = null;
    }
    
    connect() {
        this.ws = new WebSocket(this.url);
        
        this.ws.onopen = () => {
            console.log('连接成功');
            this.reconnectAttempts = 0;
            this.startHeartbeat();
        };
        
        this.ws.onclose = () => {
            this.stopHeartbeat();
            this.reconnect();
        };
    }
    
    startHeartbeat() {
        this.heartbeatInterval = setInterval(() => {
            if (this.ws.readyState === WebSocket.OPEN) {
                this.ws.send(JSON.stringify({ type: 'ping' }));
            }
        }, 30000); // 每 30 秒发送心跳
    }
    
    reconnect() {
        if (this.reconnectAttempts < this.maxReconnectAttempts) {
            const delay = Math.pow(2, this.reconnectAttempts) * 1000; // 指数退避
            setTimeout(() => {
                this.reconnectAttempts++;
                this.connect();
            }, delay);
        }
    }
}

8. 服务端代码示例

Node.js 示例(使用 ws 库)

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

wss.on('connection', (ws, req) => {
    const ip = req.socket.remoteAddress;
    console.log(`客户端连接: ${ip}`);
    
    // 接收消息
    ws.on('message', (data) => {
        console.log(`收到: ${data}`);
        // 广播给所有客户端
        wss.clients.forEach(client => {
            if (client.readyState === WebSocket.OPEN) {
                client.send(data);
            }
        });
    });
    
    // 心跳检测
    ws.isAlive = true;
    ws.on('pong', () => { ws.isAlive = true; });
});

// 定时检查连接状态
setInterval(() => {
    wss.clients.forEach(ws => {
        if (!ws.isAlive) return ws.terminate();
        ws.isAlive = false;
        ws.ping();
    });
}, 30000);

Python 示例(使用 websockets 库)

import asyncio
import websockets

async def handler(websocket, path):
    async for message in websocket:
        print(f"收到: {message}")
        await websocket.send(f"回复: {message}")

async def main():
    async with websockets.serve(handler, "localhost", 8080):
        await asyncio.Future()  # 永远运行

asyncio.run(main())

9. 安全性详解

WSS 加密传输

生产环境必须使用 wss:// 协议,它是 WebSocket 的 TLS 加密版本,类似于 HTTPS:

  • 数据加密传输,防止窃听
  • 防止中间人攻击 (MITM)
  • 确保身份验证

Origin 来源验证

服务端应该检查 HTTP 握手时的 Origin 头,防止跨站 WebSocket 劫持 (CSWSH):

// Node.js 服务端验证 Origin
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;
    }
});

认证机制

WebSocket 不支持自定义头部,常用认证方式:

URL 参数传递 Token

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

首条消息认证

ws.onopen = () => {
    ws.send(JSON.stringify({
        type: 'auth',
        token: 'xxx'
    }));
};

注意:通过 URL 传递 Token 可能被记录在服务器日志中,建议使用短期 Token 或首条消息认证方式。

10. 调试技巧

浏览器开发者工具

  • Chrome DevTools:打开 Network 面板 → 筛选 WS → 查看 Messages 选项卡
  • Firefox:网络面板 → 筛选 WS → 可查看实时消息流
  • 在线测试工具:websocket.org/echopiesocket.com/websocket-tester

常用关闭码参考

状态码 含义
1000 正常关闭
1001 端点离开(如页面关闭)
1002 协议错误
1003 不支持的数据类型
1006 异常关闭(未发送关闭帧)
1008 策略违规(如 Origin 不允许)
1011 服务器内部错误

11. 避坑指南

必须使用 WSS

生产环境务必使用 wss:// 加密连接,防止数据被窃听篡改。

心跳保活

网络环境复杂,防火墙可能切断空闲连接。定时发 Ping/Pong 保持活跃。

断线重连

网络波动是常态,必须实现自动重连(推荐指数退避算法)。

消息序列化

统一使用 JSON 或 Protocol Buffers 序列化,定义清晰的消息类型。

负载均衡

WebSocket 是有状态连接,需要粘性会话或使用 Redis Pub/Sub 跨节点通信。

资源清理

及时关闭无效连接,避免内存泄漏和连接数耗尽。

12. 子协议与扩展

WebSocket 子协议

客户端可在握手时通过 Sec-WebSocket-Protocol 头声明支持的子协议:

const ws = new WebSocket('wss://api.com/ws', ['graphql-ws', 'json']);

// 检查服务器选择的子协议
ws.onopen = () => {
    console.log('Selected protocol:', ws.protocol);
};

常见子协议

子协议 用途
graphql-ws GraphQL 订阅
mqtt IoT 消息队列
stomp 简单文本消息协议
wamp RPC 和 Pub/Sub

扩展 (Extensions)

permessage-deflate 是最常用的扩展,提供消息压缩功能,可减少 60-80% 带宽消耗。

浏览器兼容性

浏览器 最低版本 备注
Chrome 16+ 完全支持
Firefox 11+ 完全支持
Safari 7+ 完全支持
Edge 12+ 完全支持
IE 10+ 基本支持,建议放弃
Node.js 0.10+ 使用 ws 库

13. 常见问题 (FAQ)

Q: WebSocket 和 Socket.IO 有什么区别?

Socket.IO 是一个库,底层可用 WebSocket,但也支持轮询回退、自动重连、房间等高级功能。纯 WebSocket 更轻量,Socket.IO 功能更丰富。

Q: WebSocket 最大消息大小是多少?

协议理论上支持单条消息最大 2^63 字节。实际中,服务器/客户端通常有限制(如 Node.js ws 库默认 100MB),建议大文件分片传输。

Q: 如何处理高并发 WebSocket 连接?

使用异步框架(Node.js、Go、Rust),单机可轻松撑 10万+ 连接。配合 Redis Pub/Sub 实现跨进程/跨机器消息广播。

Q: WebSocket 能穿透代理和防火墙吗?

使用 wss:// 的 443 端口通常可以。HTTP 代理需要支持 CONNECT 方法,部分企业防火墙可能阻断。

Q: 服务器如何主动关闭连接?

调用 ws.close(code, reason),其中 code 是关闭码(1000-4999),reason 是可选的关闭原因字符串。

Q: WebSocket 如何发送二进制数据?

使用 ws.binaryType = 'arraybuffer''blob' 设置接收格式,发送时直接传入 ArrayBuffer 或 Blob 对象即可。

Q: 如何实现 WebSocket 服务的水平扩展?

常见方案:1) 使用 Redis Pub/Sub 跨实例广播;2) 使用 Kafka/RabbitMQ 作为消息中心;3) 使用粘性会话 (Sticky Session) 配合负载均衡。

Q: 为什么客户端发送的数据必须掩码?

这是 RFC 6455 的安全要求,防止代理服务器缓存污染攻击。服务端发送的数据不需要掩码,客户端发送必须掩码。