HTML的Web Sockets API:实现全双工、低延迟通信协议的握手与数据帧结构

HTML WebSockets API:实现全双工、低延迟通信协议的握手与数据帧结构

大家好,今天我们来深入探讨HTML WebSockets API,一个在Web开发中至关重要的技术,它为我们提供了构建实时、全双工通信应用程序的能力。我们将重点关注WebSockets的握手过程以及数据帧结构,理解它们如何协同工作,实现低延迟的数据传输。

1. WebSockets 简介与优势

传统的HTTP协议是单向的,客户端发起请求,服务器响应请求。这种模式不适合需要实时更新的应用程序,例如在线游戏、股票行情、聊天应用等。为了解决这个问题,WebSockets应运而生。

WebSockets是一种在单个TCP连接上提供全双工通信通道的协议。这意味着一旦连接建立,客户端和服务器都可以主动发送数据,而无需像HTTP那样每次通信都需要重新建立连接。

WebSockets的主要优势包括:

  • 全双工通信: 客户端和服务器可以同时发送和接收数据。
  • 低延迟: 避免了HTTP协议中重复的头部信息和连接建立的开销,降低了延迟。
  • 持久连接: 连接一旦建立,就会保持打开状态,直到客户端或服务器主动关闭。
  • 二进制数据支持: 除了文本数据,WebSockets还可以传输二进制数据,例如图像、音频、视频等。
  • 跨域支持: WebSockets可以通过CORS机制处理跨域请求。

2. WebSockets 握手过程

WebSockets连接的建立并非直接通过HTTP协议,而是首先通过一个HTTP握手过程进行协商。这个握手过程是为了告诉服务器客户端希望升级到WebSockets协议。

2.1 客户端握手请求

客户端首先向服务器发送一个HTTP Upgrade请求。这个请求包含了特定的头部信息,告诉服务器客户端支持WebSockets协议,并请求升级连接。

以下是一个客户端握手请求的示例:

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

各个头部字段的含义如下:

  • GET /chat HTTP/1.1: 请求方法和路径。/chat 可以是任何服务器端点,用于标识 WebSockets 服务。
  • Host: example.com: 服务器的域名。
  • Upgrade: websocket: 表明客户端希望升级到 WebSockets 协议。
  • Connection: Upgrade:Upgrade 头部一起使用,表示这是一个升级请求。
  • Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==: 一个随机生成的 Base64 编码的密钥。服务器会使用这个密钥进行计算,并将结果返回给客户端,用于验证服务器是否支持 WebSockets 协议。这个密钥的目的是防止恶意的 HTTP 代理劫持 WebSockets 连接。
  • Origin: http://example.com: 客户端的源地址。服务器可以使用这个头部来验证客户端是否被允许连接。
  • Sec-WebSocket-Protocol: chat, superchat: 客户端希望使用的子协议列表。子协议用于在 WebSockets 连接之上定义特定的应用层协议。例如,chatsuperchat 可能分别代表不同的聊天功能。
  • Sec-WebSocket-Version: 13: 客户端支持的 WebSockets 协议版本。目前最新的版本是 13。

2.2 服务器握手响应

如果服务器支持 WebSockets 协议,并且接受客户端的握手请求,它会返回一个 HTTP 101 Switching Protocols 响应。

以下是一个服务器握手响应的示例:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiHLiPVdzATKcmvzhOO9s=
Sec-WebSocket-Protocol: chat

各个头部字段的含义如下:

  • HTTP/1.1 101 Switching Protocols: 表明服务器接受了客户端的升级请求,并切换到 WebSockets 协议。
  • Upgrade: websocket: 确认服务器已经升级到 WebSockets 协议。
  • Connection: Upgrade:Upgrade 头部一起使用,表示这是一个升级响应。
  • Sec-WebSocket-Accept: s3pPLMBiHLiPVdzATKcmvzhOO9s=: 使用客户端发送的 Sec-WebSocket-Key 计算出的一个哈希值,用于验证服务器是否支持 WebSockets 协议。这个值的计算方式如下:
    1. 将客户端发送的 Sec-WebSocket-Key 的值与字符串 "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 拼接起来。
    2. 对拼接后的字符串进行 SHA-1 哈希计算。
    3. 将哈希结果进行 Base64 编码。
  • Sec-WebSocket-Protocol: chat: 服务器选择的子协议。如果客户端在握手请求中发送了多个子协议,服务器可以选择其中一个。

2.3 密钥验证

客户端需要验证服务器返回的 Sec-WebSocket-Accept 头部的值是否正确。如果验证失败,客户端应该关闭连接。

验证过程如下:

  1. 将客户端发送的 Sec-WebSocket-Key 的值与字符串 "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 拼接起来。
  2. 对拼接后的字符串进行 SHA-1 哈希计算。
  3. 将哈希结果进行 Base64 编码。
  4. 将计算出的值与服务器返回的 Sec-WebSocket-Accept 的值进行比较。

如果两个值相等,则验证成功,表明服务器支持 WebSockets 协议。

2.4 代码示例 (JavaScript)

以下是一个简单的 JavaScript 示例,演示了如何创建一个 WebSockets 连接:

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

websocket.addEventListener('open', (event) => {
  console.log('Connected to WebSocket server');
  websocket.send('Hello from the client!');
});

websocket.addEventListener('message', (event) => {
  console.log('Received message:', event.data);
});

websocket.addEventListener('close', (event) => {
  console.log('Disconnected from WebSocket server');
});

websocket.addEventListener('error', (event) => {
  console.error('WebSocket error:', event);
});

在这个示例中:

  • new WebSocket('ws://example.com/chat') 创建一个新的 WebSockets 连接。ws:// 协议用于非加密的 WebSockets 连接,wss:// 协议用于加密的 WebSockets 连接。
  • websocket.addEventListener('open', ...) 监听 open 事件,该事件在连接成功建立后触发。
  • websocket.addEventListener('message', ...) 监听 message 事件,该事件在收到服务器发送的消息时触发。
  • websocket.addEventListener('close', ...) 监听 close 事件,该事件在连接关闭时触发。
  • websocket.addEventListener('error', ...) 监听 error 事件,该事件在发生错误时触发。
  • websocket.send('Hello from the client!') 向服务器发送一条消息。

3. WebSockets 数据帧结构

一旦 WebSockets 连接建立,客户端和服务器就可以通过发送和接收数据帧来交换数据。WebSockets 数据帧的结构定义了如何将数据打包成可传输的格式。

WebSockets 数据帧的结构如下:

  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|             |                               |
 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 |     Masking-key (if MASK set)                         |
 +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |   Payload Data                                              ...
 +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |   Extension Data (if any)                                 ...
 +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |   Application Data                                        ...
 +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
 |   Padding (if any)                                        ...
 +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +

各个字段的含义如下:

  • FIN (1 bit): 表示这是消息的最后一个片段。如果设置为 1,表示这是消息的最后一个片段;如果设置为 0,表示这是消息的一个中间片段。WebSockets 允许将一个大的消息分割成多个片段进行传输。
  • RSV1, RSV2, RSV3 (各 1 bit): 用于扩展。通常设置为 0,除非使用了 WebSockets 扩展。
  • Opcode (4 bits): 定义了有效载荷数据的类型。常见的 Opcode 值包括:
    • 0x0: 表示这是一个延续帧。用于分割消息的情况。
    • 0x1: 表示这是一个文本帧。有效载荷数据是 UTF-8 编码的文本。
    • 0x2: 表示这是一个二进制帧。有效载荷数据是二进制数据。
    • 0x8: 表示这是一个关闭帧。用于关闭 WebSockets 连接。
    • 0x9: 表示这是一个 Ping 帧。用于检测连接是否仍然可用。
    • 0xA: 表示这是一个 Pong 帧。用于响应 Ping 帧。
  • MASK (1 bit): 表示有效载荷数据是否被掩码。如果设置为 1,表示有效载荷数据被掩码;如果设置为 0,表示有效载荷数据未被掩码。客户端发送给服务器的数据必须被掩码。服务器发送给客户端的数据不能被掩码。
  • Payload length (7 bits, 7+16 bits, or 7+64 bits): 表示有效载荷数据的长度。
    • 如果该值为 0-125,则表示有效载荷数据的实际长度。
    • 如果该值为 126,则后面的 2 个字节表示有效载荷数据的长度(以 unsigned short 形式表示)。
    • 如果该值为 127,则后面的 8 个字节表示有效载荷数据的长度(以 unsigned long long 形式表示)。
  • Masking-key (32 bits): 用于掩码有效载荷数据的掩码密钥。只有在 MASK 位设置为 1 时才存在。
  • Payload data: 实际的有效载荷数据。
  • Extension data (if any): 扩展数据,如果使用了 WebSockets 扩展。
  • Application data: 应用数据。
  • Padding (if any): 填充数据,用于对齐数据。

3.1 掩码

为了防止恶意攻击,客户端发送给服务器的数据必须被掩码。掩码算法如下:

masked[i] = unmasked[i] XOR masking-key[i % 4]

其中:

  • masked[i] 是掩码后的字节。
  • unmasked[i] 是未掩码的字节。
  • masking-key[i % 4] 是掩码密钥的第 i % 4 个字节。

掩码密钥是一个 32 位的随机数,由客户端生成。

3.2 代码示例 (Node.js)

以下是一个简单的 Node.js 示例,演示了如何解析 WebSockets 数据帧:

const WebSocket = require('ws');

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

wss.on('connection', ws => {
  ws.on('message', message => {
    // 解析数据帧
    const fin = (message[0] & 0x80) === 0x80;
    const opcode = message[0] & 0x0f;
    const masked = (message[1] & 0x80) === 0x80;
    let payloadLength = message[1] & 0x7f;

    let maskingKey;
    let payloadData;
    let dataStartIndex;

    if (payloadLength === 126) {
      payloadLength = message.readUInt16BE(2);
      maskingKey = message.slice(4, 8);
      payloadData = message.slice(8);
      dataStartIndex = 8;
    } else if (payloadLength === 127) {
      payloadLength = Number(message.readBigUInt64BE(2)); // Use Number to handle BigInt for simplicity
      maskingKey = message.slice(10, 14);
      payloadData = message.slice(14);
      dataStartIndex = 14;
    } else {
      maskingKey = message.slice(2, 6);
      payloadData = message.slice(6);
      dataStartIndex = 6;
    }

    // 解码数据
    if (masked) {
      for (let i = 0; i < payloadData.length; i++) {
        payloadData[i] = payloadData[i] ^ maskingKey[i % 4];
      }
    }

    const decodedMessage = payloadData.toString('utf8');

    console.log('Received message:', decodedMessage);

    ws.send(`Server received: ${decodedMessage}`);
  });
});

console.log('WebSocket server started on port 8080');

在这个示例中:

  • WebSocket.Server 创建一个新的 WebSockets 服务器。
  • wss.on('connection', ...) 监听 connection 事件,该事件在有新的客户端连接时触发。
  • ws.on('message', ...) 监听 message 事件,该事件在收到客户端发送的消息时触发。
  • 代码解析了数据帧的各个字段,包括 FIN, Opcode, MASK, Payload length, Masking-key 和 Payload data。
  • 如果数据被掩码,代码会使用掩码密钥对数据进行解码。
  • 最后,代码将解码后的消息打印到控制台,并向客户端发送一条响应。

4. WebSockets 的应用场景

WebSockets 广泛应用于各种需要实时通信的场景,包括:

  • 在线游戏: WebSockets 可以用于实时同步游戏状态,例如玩家位置、动作、聊天信息等。
  • 股票行情: WebSockets 可以用于实时推送股票行情数据,例如价格、成交量等。
  • 聊天应用: WebSockets 可以用于实时传输聊天消息,例如文本、图片、表情等。
  • 协同编辑: WebSockets 可以用于实时同步文档内容,允许多个用户同时编辑同一个文档。
  • 实时监控: WebSockets 可以用于实时监控服务器状态,例如 CPU 使用率、内存使用率等。
  • 物联网 (IoT): WebSockets 可以用于实时传输传感器数据,例如温度、湿度、光照强度等。

5. 安全考虑

在使用 WebSockets 时,需要考虑以下安全问题:

  • 跨站脚本攻击 (XSS): WebSockets 连接容易受到 XSS 攻击。为了防止 XSS 攻击,应该对所有用户输入进行验证和过滤。
  • 跨站请求伪造 (CSRF): WebSockets 连接也容易受到 CSRF 攻击。为了防止 CSRF 攻击,应该使用 CSRF token 或其他验证机制。
  • 拒绝服务攻击 (DoS): WebSockets 服务器容易受到 DoS 攻击。为了防止 DoS 攻击,应该限制每个客户端的连接数和消息速率。
  • 加密: 应该使用 wss:// 协议进行加密通信,以防止数据被窃听。
  • 源验证: 服务器应该验证客户端的源地址,以防止未经授权的客户端连接。

6. 总结:理解握手和帧结构是构建稳定WebSocket应用的关键

WebSockets 通过握手过程建立持久连接,并利用特定的数据帧结构进行高效的数据传输。理解握手过程的各个头部字段以及数据帧的结构,能够帮助我们构建更加健壮和安全的实时Web应用。掌握数据帧的解析和构造,可以更灵活地处理各种类型的实时数据。

希望今天的讲解能够帮助大家更好地理解和使用 WebSockets API。 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注