WebSockets 工作原理:实时连接架构与生命周期详解

在 Web 发展的早期,浏览器仅仅是一个简单的文档查看器。您请求一个页面,服务器渲染它,然后连接关闭。这种“请求-响应”循环是 HTTP (超文本传输协议) 的核心。

然而,随着 Web 应用程序演变为丰富、交互式的体验(如实时聊天、实时金融行情、协同编辑和多人游戏),传统的 HTTP 模型开始显现出它的局限性。

为了获取实时更新,开发人员最初依赖于折中方案:

  • 短轮询 (Short Polling): 浏览器每隔几秒就重复向服务器发送 HTTP 请求以获取新数据。这会产生巨大的请求头开销并浪费服务器资源。
  • 长轮询 (Long Polling / Comet): 浏览器发送请求,服务器保持连接打开,直到有新数据可用。一旦数据发送完毕,连接就会关闭,浏览器立即发起新的请求。这非常难以管理,且仍然存在高昂的连接建立开销。

WebSockets 通过引入一种基于单个 TCP 连接的持久、双向、全双工通信的标准化协议,彻底解决了这些限制。


什么是 WebSocket?

WebSockets(在 RFC 6455 中定义)与 HTTP 并存。HTTP 是一种无状态协议,只能由客户端发起请求;而 WebSocket 连接建立后会无限期保持打开状态,允许客户端和服务器随时以极低的延迟向对方发送数据。

这是 WebSockets 的基本规则:

一旦连接建立,任何一方都可以在任何时候发送消息,而无需发起新的连接请求。


步骤详解:连接生命周期

WebSocket 连接经历三个不同的阶段:握手 (Handshake)数据传输连接关闭

WebSocket Connection Lifecycle Diagram

1. HTTP 握手(协议升级)

由于防火墙和路由器通常配置为允许端口 80 (HTTP) 和 443 (HTTPS) 上的标准 Web 流量,因此 WebSockets 的建立首先始于标准的 HTTP/1.1 请求。这被称为升级握手 (Upgrade Handshake)

客户端请求

客户端发送一个带有特定请求头的 HTTP GET 请求,要求切换协议:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com
  • Upgrade: websocketConnection: Upgrade:告知服务器客户端希望切换协议。
  • Sec-WebSocket-Key:一个随机的、经 Base64 编码的 16 字节值。它用于证明服务器收到了握手请求并理解 WebSocket 协议。
  • Sec-WebSocket-Version:指定 WebSocket 协议版本(通常为 13)。
  • Origin:服务器用于决定是否允许该连接(防止未经授权的网站进行跨站连接的安全检查)。

服务器响应

如果服务器支持 WebSockets,它会验证请求并响应 HTTP 状态码 101 Switching Protocols(协议切换):

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
  • 服务器如何计算 Sec-WebSocket-Accept
    1. 服务器获取客户端的 Sec-WebSocket-Key(如 dGhlIHNhbXBsZSBub25jZQ==)。
    2. 将其与一个标准的魔术字符串 GUID 拼接:"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
    3. 计算拼接后字符串的 SHA-1 哈希值。
    4. 将生成的哈希值进行 Base64 编码。
    5. 客户端在验证该值符合预期后,握手即告成功,HTTP 连接切换为原始 TCP 套接字,双方转换到 WebSocket 协议。

2. 数据分帧与传输

与发送纯文本请求头后接主体的 HTTP 不同,WebSockets 在被称为帧 (frames) 的结构化二进制数据包中传输数据。

WebSocket 帧具有非常轻量级的头部(范围在 2 到 14 字节之间),后接有效载荷(payload)。该头部包含:

  • FIN 位 (1 bit): 指示这是否是消息的最后一帧。
  • Opcode 操作码 (4 bits): 定义帧的类型:
    • 0x1:文本帧(UTF-8 编码)
    • 0x2:二进制帧
    • 0x8:连接关闭请求
    • 0x9:Ping 帧
    • 0xA:Pong 帧
  • Mask 掩码位 (1 bit): 指定有效载荷数据是否经过掩码处理。
  • 有效载荷长度: 数据的大小。
  • 掩码键 (4 bytes): 关键安全要求: 从客户端发送到服务器的所有帧必须使用随机 4 字节键进行掩码(XOR 异或混淆)处理。这可以防止代理缓存读取流量或执行缓存污染攻击。从服务器发送到客户端的帧绝不能进行掩码处理。

心跳检测 (Ping/Pong)

为了防止路由器和负载均衡器关闭空闲连接,任何一方都可以发送 Ping 帧。接收方必须立即回复一个包含相同有效载荷的 Pong 帧。

3. 关闭连接

若要干净地关闭连接:

  1. 一方发送一个包含状态码(例如 1000 表示正常关闭,1006 表示异常关闭)和可选文本原因的 Close 帧。
  2. 另一方回应自己的 Close 帧。
  3. 底层的 TCP 套接字被关闭。

代码示例:Node.js WebSocket 实现

为了直观地看到 WebSockets 的运作,让我们编写一个简单的 Node.js 应用程序。我们将创建一个本地 WebSocket 服务器,它会回显接收到的任何消息,同时编写一个连接到该服务器的客户端脚本。

WebSocket 服务器 (server.js)

const { WebSocketServer } = require('ws');
const http = require('http');

// 1. 创建一个标准的 HTTP 服务器
const server = http.createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    res.end('HTTP Server running. Use WebSocket to connect.\n');
});

// 2. 将 WebSocket 服务器附加到 HTTP 服务器
const wss = new WebSocketServer({ server });

wss.on('connection', (ws, req) => {
    const clientIp = req.socket.remoteAddress;
    console.log(`[服务器] 新客户端连接,来自 ${clientIp}`);

    // 向客户端发送欢迎消息
    ws.send(JSON.stringify({ type: 'welcome', message: '已成功连接到 Ghaznix WebSocket 服务器!' }));

    // 监听来自该客户端的传入消息
    ws.on('message', (message) => {
        console.log(`[服务器] 收到消息: ${message}`);
        
        try {
            const data = JSON.parse(message);
            ws.send(JSON.stringify({
                type: 'echo',
                message: `服务器回显: ${data.text.toUpperCase()}`,
                timestamp: new Date().toISOString()
            }));
        } catch (e) {
            ws.send(JSON.stringify({ type: 'error', message: '无效的 JSON 格式' }));
        }
    });

    // 处理客户端断开连接
    ws.on('close', (code, reason) => {
        console.log(`[服务器] 客户端断开连接 (状态码: ${code}, 原因: ${reason.toString() || '无'})`);
    });

    ws.on('error', (error) => {
        console.error(`[服务器] 套接字错误: ${error.message}`);
    });
});

server.listen(8080, () => {
    console.log('WebSocket 服务器正在监听 ws://localhost:8080');
});

浏览器客户端 (客户端 JavaScript)

您可以直接在浏览器的控制台中运行此客户端:

// 1. 建立与服务器的连接
const socket = new WebSocket('ws://localhost:8080');

// 2. 连接打开的监听器
socket.addEventListener('open', (event) => {
    console.log('[客户端] 已连接到服务器。');
    
    const payload = JSON.stringify({ text: 'hello, server!' });
    socket.send(payload);
    console.log(`[客户端] 发送: ${payload}`);
});

// 3. 监听来自服务器的消息
socket.addEventListener('message', (event) => {
    const response = JSON.parse(event.data);
    console.log('[客户端] 收到来自服务器的消息:', response);
});

// 4. 监听连接关闭
socket.addEventListener('close', (event) => {
    console.log(`[客户端] 连接已关闭 (状态码: ${event.code})`);
});

// 5. 监听错误
socket.addEventListener('error', (error) => {
    console.error('[客户端] WebSocket 错误:', error);
});

HTTP 与 WebSockets 的详细对比

特性 HTTP/1.1 WebSockets
通信方式 单向 (客户端发起) 双向 (客户端或服务器均可发起)
连接模型 请求-响应 (短连接) 持久连接 (长连接)
开销 高 (每次请求都发送完整的 Header) 极低 (极少的数据分帧开销)
状态 无状态 有状态 (保持连接上下文)
协议协议 http://https:// ws://wss://
最适合的场景 获取静态页面/资源、REST API 实时聊天、仪表盘、游戏、实时数据流

WebSockets 的安全考量

由于 WebSockets 在握手后绕过了标准的 HTTP 路由,它们引入了独特的安全向量:

  1. 使用加密的 WebSocket Secure (wss://): 始终在 TLS/SSL(端口 443)上运行 WebSockets。WSS 对分帧载荷进行加密,以防窃听和中间人篡改。
  2. 同源验证 (Origin Validation): WebSockets 不受同源策略 (SOP) 的限制。在握手期间,必须始终在服务器端验证 Origin 请求头,以防止未经授权的跨站访问。
  3. 握手时的身份验证: 在连接建立前对用户进行身份验证。这通常通过在查询参数中传递令牌(如 JWT)或验证 Session Cookie 来实现。
  4. 输入净化: 将通过 WebSockets 接收的每条消息都视为不可信输入。对有效载荷进行验证和净化,以防止跨站脚本攻击 (XSS)。

总结

WebSockets 通过消除传统 HTTP 轮询的开销,彻底改变了实时 Web 应用的开发方式。通过维护单个持久 TCP 连接,它实现了即时的双向消息传递,为当今的实时仪表盘、多人游戏和聊天应用提供了强劲的底层支撑。理解 HTTP 升级机制、数据分帧架构以及关键的安全实践,可确保您构建出高效且安全的实时服务。


在 Ghaznix 博客上探索更多开发者教程和指南 →