WebSocket 协议详解 (RFC 6455)

WebSocket 协议详解 (RFC 6455)

目录


概述

WebSocket 协议(RFC 6455)是一种在单个 TCP 连接上进行全双工通信的协议。它提供了一种机制,使浏览器中的应用程序能够与远程服务器进行双向通信,而无需打开多个 HTTP 连接。

核心特点

  • 全双工通信:客户端和服务器可以同时发送和接收数据
  • 基于 TCP:建立在可靠的 TCP 连接之上
  • HTTP 兼容:握手过程兼容 HTTP,可以共享端口
  • 低开销:相比 HTTP 轮询,显著减少协议开销
  • 实时性:支持实时双向数据传输

协议版本

  • 当前标准版本:13(RFC 6455)
  • 协议标识Sec-WebSocket-Version: 13

协议背景

历史问题

在 WebSocket 出现之前,实现双向通信需要:

  1. HTTP 轮询:客户端定期向服务器发送请求
  2. 长轮询:客户端发送请求,服务器保持连接直到有数据
  3. HTTP 流:服务器保持连接打开,持续发送数据

这些方法的问题

  • 服务器被迫为每个客户端使用多个 TCP 连接
  • 协议开销高,每个消息都包含完整的 HTTP 头
  • 客户端脚本需要维护从出站连接到入站连接的映射
  • 延迟高,无法实现真正的实时通信

WebSocket 的解决方案

WebSocket 协议通过以下方式解决这些问题:

  • 单一 TCP 连接:双向通信使用同一个连接
  • 低开销:帧格式最小化协议开销
  • 实时性:支持真正的实时双向通信

协议概述

WebSocket 协议由两个主要部分组成:

  1. 握手(Handshake):建立连接的 HTTP 升级请求
  2. 数据传输(Data Transfer):基于帧的双向数据传输

握手示例

客户端请求

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

服务器响应

http 复制代码
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

数据传输

握手成功后,数据以**帧(Frame)**的形式传输。每条消息可以由一个或多个帧组成。

WebSocket URI

WebSocket 使用两种 URI 方案:

ws URI

复制代码
ws-URI = "ws:" "//" host [ ":" port ] path [ "?" query ]
  • 默认端口:80
  • 示例ws://example.com/chat

wss URI

复制代码
wss-URI = "wss:" "//" host [ ":" port ] path [ "?" query ]
  • 默认端口:443
  • 加密连接:使用 TLS 加密
  • 示例wss://example.com/chat

URI 组件

  • host:服务器主机名或 IP 地址
  • port:端口号(可选,有默认值)
  • path:资源路径
  • query:查询参数(可选)

注意 :片段标识符(#)在 WebSocket URI 中无意义,不应使用。

握手过程

客户端握手

客户端发送一个 HTTP 升级请求,包含以下必需字段:

必需字段
  1. 请求行

    复制代码
    GET /resource HTTP/1.1
  2. Host 头

    复制代码
    Host: server.example.com
  3. Upgrade 头

    复制代码
    Upgrade: websocket
  4. Connection 头

    复制代码
    Connection: Upgrade
  5. Sec-WebSocket-Key

    复制代码
    Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
    • 16 字节随机值,base64 编码
    • 每次连接必须使用新的随机值
  6. Sec-WebSocket-Version

    复制代码
    Sec-WebSocket-Version: 13
    • 当前标准版本为 13
可选字段
  1. Origin

    复制代码
    Origin: http://example.com
    • 浏览器客户端必须包含
    • 用于服务器验证请求来源
  2. Sec-WebSocket-Protocol

    复制代码
    Sec-WebSocket-Protocol: chat, superchat
    • 客户端希望使用的子协议列表
    • 多个协议用逗号分隔
  3. Sec-WebSocket-Extensions

    复制代码
    Sec-WebSocket-Extensions: extension1, extension2
    • 客户端支持的扩展列表

服务器握手

服务器必须验证客户端握手,并返回相应的响应:

成功响应(101 Switching Protocols)
http 复制代码
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

必需字段

  1. 状态码 :必须是 101 Switching Protocols

  2. Upgrade 头

    复制代码
    Upgrade: websocket
  3. Connection 头

    复制代码
    Connection: Upgrade
  4. Sec-WebSocket-Accept

    复制代码
    Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
    • 计算方式:base64(sha1(Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
    • 用于验证服务器理解 WebSocket 协议

可选字段

  1. Sec-WebSocket-Protocol

    复制代码
    Sec-WebSocket-Protocol: chat
    • 服务器选择的子协议(必须来自客户端请求的列表)
  2. Sec-WebSocket-Extensions

    复制代码
    Sec-WebSocket-Extensions: extension1
    • 服务器选择的扩展(必须来自客户端请求的列表)
错误响应

如果握手失败,服务器返回相应的 HTTP 错误码:

  • 400 Bad Request:握手格式错误
  • 403 Forbidden:服务器拒绝连接(如 Origin 验证失败)
  • 404 Not Found:请求的资源不存在
  • 426 Upgrade Required:需要升级协议版本

握手验证

Sec-WebSocket-Accept 计算

服务器必须正确计算 Sec-WebSocket-Accept 值:

算法

  1. 获取客户端发送的 Sec-WebSocket-Key
  2. 将固定字符串 "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" 追加到该值
  3. 对结果进行 SHA-1 哈希
  4. 将哈希值进行 base64 编码

示例

javascript 复制代码
// 客户端发送
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

// 服务器计算
const key = "dGhlIHNhbXBsZSBub25jZQ==";
const magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
const accept = base64(sha1(key + magic));
// 结果: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

代码实现(Node.js):

javascript 复制代码
const crypto = require('crypto');

function generateAccept(key) {
  const magic = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
  const hash = crypto.createHash('sha1')
    .update(key + magic)
    .digest('base64');
  return hash;
}

数据帧格式

帧结构

WebSocket 数据以帧(Frame)的形式传输,帧结构如下:

复制代码
 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|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
|     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 bit)
  • 0:还有后续帧(消息未结束)
  • 1:这是消息的最后一个帧
RSV1, RSV2, RSV3 (各 1 bit)
  • 保留字段,必须为 0
  • 除非协商了扩展,否则不能为非零值
  • 扩展可以使用这些位
Opcode (4 bits)

定义帧的类型:

Opcode 类型 说明
0x0 Continuation 延续帧(分片消息的后续帧)
0x1 Text 文本帧(UTF-8 编码)
0x2 Binary 二进制帧
0x3-0x7 - 保留用于非控制帧
0x8 Close 连接关闭
0x9 Ping Ping 帧
0xA Pong Pong 帧
0xB-0xF - 保留用于控制帧
MASK (1 bit)
  • 0:帧未掩码(服务器到客户端)
  • 1:帧已掩码(客户端到服务器)

重要:客户端发送的所有帧必须设置 MASK=1,服务器发送的帧必须设置 MASK=0。

Payload Length (7 bits, 7+16 bits, 或 7+64 bits)

载荷长度编码:

  • 0-125:直接表示载荷长度(字节)
  • 126:后续 2 字节(16 位无符号整数)表示长度
  • 127:后续 8 字节(64 位无符号整数)表示长度

注意:必须使用最小字节数编码长度。

Masking-key (0 或 4 bytes)
  • 如果 MASK=1,包含 4 字节掩码键
  • 如果 MASK=0,不包含此字段
Payload Data (可变长度)

实际数据载荷,包括:

  • Extension data:扩展数据(如果协商了扩展)
  • Application data:应用数据

客户端到服务器的掩码

掩码的目的

防止恶意脚本构造可能被中间代理误解为 HTTP 请求的数据,从而防止缓存投毒攻击。

掩码算法

客户端必须为每个帧选择一个新的随机 32 位掩码键。

掩码/解掩码算法

复制代码
j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j

代码实现

javascript 复制代码
function mask(data, maskingKey) {
  const masked = Buffer.alloc(data.length);
  for (let i = 0; i < data.length; i++) {
    const j = i % 4;
    masked[i] = data[i] ^ maskingKey[j];
  }
  return masked;
}

// 注意:掩码和解掩码使用相同的算法
掩码键生成

掩码键必须:

  • 从强熵源生成
  • 每个帧使用新的随机值
  • 不可预测

示例(Node.js):

javascript 复制代码
const crypto = require('crypto');

function generateMaskingKey() {
  return crypto.randomBytes(4);
}

帧分片

分片的目的
  1. 发送未知大小的消息:可以在不知道总长度的情况下开始发送
  2. 多路复用:允许在输出通道上交错发送多个消息
分片规则
  1. 未分片消息:单个帧,FIN=1,opcode ≠ 0

  2. 分片消息

    • 第一帧:FIN=0,opcode ≠ 0(表示消息类型)
    • 中间帧:FIN=0,opcode=0(延续帧)
    • 最后帧:FIN=1,opcode=0(延续帧)
  3. 控制帧

    • 不能分片
    • 可以在分片消息中间插入
  4. 消息顺序

    • 同一消息的帧必须按顺序发送
    • 不同消息的帧不能交错(除非扩展支持)
分片示例

文本消息 "Hello World" 分片

复制代码
帧1: FIN=0, Opcode=0x1, Payload="Hello"
帧2: FIN=1, Opcode=0x0, Payload=" World"

二进制消息分片

复制代码
帧1: FIN=0, Opcode=0x2, Payload=[前4KB数据]
帧2: FIN=0, Opcode=0x0, Payload=[中间4KB数据]
帧3: FIN=1, Opcode=0x0, Payload=[最后2KB数据]

控制帧

控制帧用于协议级别的信令,不能包含应用数据。

控制帧特点

  • Opcode 最高位为 1:0x8-0xF
  • 载荷长度限制:≤ 125 字节
  • 不能分片

Close 帧

Opcode: 0x8

用途:关闭 WebSocket 连接

载荷格式

  • 前 2 字节:状态码(网络字节序)
  • 后续字节:UTF-8 编码的关闭原因(可选)

关闭握手流程

  1. 一端发送 Close 帧
  2. 另一端收到后,发送 Close 帧响应
  3. 发送方收到响应后,关闭 TCP 连接

状态码示例

  • 1000:正常关闭
  • 1001:端点离开(如服务器关闭)
  • 1002:协议错误
  • 1003:不支持的数据类型
  • 1006:异常关闭(未收到 Close 帧)
  • 1007:数据格式错误(非 UTF-8)
  • 1008:策略违反
  • 1009:消息过大
  • 1010:扩展协商失败
  • 1011:服务器内部错误

Ping 和 Pong 帧

Ping 帧

Opcode: 0x9

用途

  • 保持连接活跃
  • 检测远程端点是否响应

载荷:可选的应用程序数据

Pong 帧

Opcode: 0xA

用途:响应 Ping 帧

规则

  • 收到 Ping 后必须发送 Pong
  • Pong 的载荷必须与对应的 Ping 相同
  • 可以发送未请求的 Pong(作为单向心跳)

示例

javascript 复制代码
// 服务器发送 Ping
ws.send(pingFrame);

// 客户端自动响应 Pong
// 或手动响应
ws.pong(pingFrame.payload);

数据帧

数据帧用于传输应用数据。

文本帧 (Opcode 0x1)

  • 载荷:UTF-8 编码的文本数据
  • 验证:整个消息必须是有效的 UTF-8
  • 处理:接收端必须验证 UTF-8 有效性

二进制帧 (Opcode 0x2)

  • 载荷:任意二进制数据
  • 解释:由应用程序决定

数据帧示例

单帧文本消息

复制代码
FIN=1, Opcode=0x1, Payload="Hello"

单帧二进制消息

复制代码
FIN=1, Opcode=0x2, Payload=[0x48, 0x65, 0x6c, 0x6c, 0x6f]

发送和接收数据

发送数据

步骤

  1. 确保连接处于 OPEN 状态
  2. 将数据封装为 WebSocket 帧
  3. 设置适当的 opcode(文本或二进制)
  4. 设置 FIN 位(最后帧为 1)
  5. 如果是客户端,对帧进行掩码
  6. 通过底层网络连接传输

代码示例(伪代码):

javascript 复制代码
function sendMessage(data, isText) {
  if (connectionState !== OPEN) {
    throw new Error('Connection not open');
  }
  
  const opcode = isText ? 0x1 : 0x2;
  const frame = createFrame({
    fin: 1,
    opcode: opcode,
    payload: data,
    masked: isClient
  });
  
  sendToNetwork(frame);
}

接收数据

步骤

  1. 从网络接收数据
  2. 解析 WebSocket 帧
  3. 如果是控制帧,立即处理
  4. 如果是数据帧:
    • 检查 opcode 确定数据类型
    • 累积载荷数据
    • 如果 FIN=1,组装完整消息并传递给应用

代码示例(伪代码):

javascript 复制代码
function receiveData(buffer) {
  const frame = parseFrame(buffer);
  
  if (isControlFrame(frame.opcode)) {
    handleControlFrame(frame);
  } else {
    // 数据帧
    messageBuffer.append(frame.payload);
    
    if (frame.fin) {
      // 消息完整
      const message = messageBuffer.toString();
      messageBuffer.clear();
      onMessage(message, frame.opcode === 0x1);
    }
  }
}

关闭连接

正常关闭

关闭握手流程

  1. 发起关闭:发送 Close 帧
  2. 等待响应:等待对方发送 Close 帧
  3. 关闭 TCP:关闭底层 TCP 连接

服务器关闭

  • 服务器应该立即关闭 TCP 连接
  • 服务器持有 TIME_WAIT 状态

客户端关闭

  • 客户端应该等待服务器关闭 TCP 连接
  • 如果超时,客户端可以主动关闭

异常关闭

情况

  • 底层 TCP 连接意外断开
  • 协议错误导致连接失败
  • 握手失败

恢复

  • 客户端应该使用退避策略重连
  • 首次重连延迟:随机 0-5 秒
  • 后续重连:使用指数退避

状态码

定义的状态码
状态码 含义 说明
1000 Normal Closure 正常关闭
1001 Going Away 端点离开(如服务器关闭、浏览器导航)
1002 Protocol Error 协议错误
1003 Unsupported Data 不支持的数据类型
1004 Reserved 保留
1005 No Status Rcvd 未收到状态码(不能用于 Close 帧)
1006 Abnormal Closure 异常关闭(不能用于 Close 帧)
1007 Invalid frame payload data 无效的帧载荷数据
1008 Policy Violation 策略违反
1009 Message Too Big 消息过大
1010 Mandatory Extension 必需的扩展未协商
1011 Internal Server Error 服务器内部错误
1015 TLS handshake TLS 握手失败(不能用于 Close 帧)
状态码范围
  • 0-999:未使用
  • 1000-2999:协议定义(需要标准或扩展规范)
  • 3000-3999:库/框架/应用使用(可注册)
  • 4000-4999:私有使用(无需注册)

扩展机制

扩展协商

扩展在握手时通过 Sec-WebSocket-Extensions 头协商。

客户端请求

复制代码
Sec-WebSocket-Extensions: extension1; param1=value1, extension2

服务器响应

复制代码
Sec-WebSocket-Extensions: extension1; param1=value1

扩展格式

复制代码
extension = extension-token *( ";" extension-param )
extension-param = token [ "=" (token | quoted-string) ]

示例

复制代码
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits=15

已知扩展

  • permessage-deflate:压缩扩展(RFC 7692)
  • 其他扩展需要注册

子协议

子协议协商

子协议在握手时通过 Sec-WebSocket-Protocol 头协商。

客户端请求

复制代码
Sec-WebSocket-Protocol: chat, superchat

服务器响应

复制代码
Sec-WebSocket-Protocol: chat

子协议命名

建议使用域名格式避免冲突:

  • chat.example.com
  • v2.bookings.example.net

安全考虑

Origin 验证

服务器应该验证 Origin 头,拒绝来自未授权源的连接:

javascript 复制代码
const allowedOrigins = ['https://example.com', 'https://app.example.com'];

function validateOrigin(origin) {
  return allowedOrigins.includes(origin);
}

数据掩码

  • 客户端必须掩码所有发送的帧
  • 防止缓存投毒攻击
  • 掩码键必须随机且不可预测

TLS 使用

  • 生产环境应该使用 wss://(TLS)
  • 提供连接机密性和完整性
  • 使用强 TLS 算法

输入验证

  • 服务器必须验证所有输入
  • 防止注入攻击
  • 验证 UTF-8 编码

实现限制

  • 限制帧大小
  • 限制消息大小
  • 防止内存耗尽攻击

实际应用示例

JavaScript 客户端示例

javascript 复制代码
// 创建 WebSocket 连接
const ws = new WebSocket('wss://example.com/chat');

// 连接打开
ws.onopen = () => {
  console.log('WebSocket 连接已建立');
  ws.send('Hello Server!');
};

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

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

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

Node.js 服务器示例

javascript 复制代码
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws, req) => {
  // 验证 Origin
  const origin = req.headers.origin;
  if (!isAllowedOrigin(origin)) {
    ws.close(1008, 'Origin not allowed');
    return;
  }
  
  console.log('新客户端连接');
  
  // 接收消息
  ws.on('message', (message) => {
    console.log('收到消息:', message);
    
    // 广播给所有客户端
    wss.clients.forEach((client) => {
      if (client !== ws && client.readyState === WebSocket.OPEN) {
        client.send(message);
      }
    });
  });
  
  // 连接关闭
  ws.on('close', (code, reason) => {
    console.log('客户端断开:', code, reason);
  });
  
  // 发送欢迎消息
  ws.send('欢迎连接到服务器!');
});

Python 服务器示例

python 复制代码
import asyncio
import websockets

async def handle_client(websocket, path):
    # 验证 Origin
    origin = websocket.request_headers.get('Origin')
    if not is_allowed_origin(origin):
        await websocket.close(code=1008, reason='Origin not allowed')
        return
    
    print(f'新客户端连接: {websocket.remote_address}')
    
    try:
        # 发送欢迎消息
        await websocket.send('欢迎连接到服务器!')
        
        # 接收消息
        async for message in websocket:
            print(f'收到消息: {message}')
            # 回显消息
            await websocket.send(f'服务器收到: {message}')
    except websockets.exceptions.ConnectionClosed:
        print('客户端断开连接')

async def main():
    async with websockets.serve(handle_client, 'localhost', 8765):
        await asyncio.Future()  # 永远运行

asyncio.run(main())

手动实现握手(Node.js)

javascript 复制代码
const http = require('http');
const crypto = require('crypto');

function generateAccept(key) {
  const magic = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
  return crypto.createHash('sha1')
    .update(key + magic)
    .digest('base64');
}

const server = http.createServer((req, res) => {
  if (req.method === 'GET' && req.headers.upgrade === 'websocket') {
    const key = req.headers['sec-websocket-key'];
    const accept = generateAccept(key);
    
    res.writeHead(101, {
      'Upgrade': 'websocket',
      'Connection': 'Upgrade',
      'Sec-WebSocket-Accept': accept
    });
    
    // 升级到 WebSocket 连接
    // 后续需要处理 WebSocket 帧
  } else {
    res.writeHead(400);
    res.end('Not a WebSocket request');
  }
});

server.listen(8080);

总结

核心要点

  1. 握手过程

    • 基于 HTTP 升级请求
    • 使用 Sec-WebSocket-KeySec-WebSocket-Accept 验证
    • 支持子协议和扩展协商
  2. 数据帧

    • 最小化协议开销
    • 支持文本和二进制数据
    • 支持消息分片
  3. 安全机制

    • Origin 验证
    • 客户端数据掩码
    • TLS 支持
  4. 控制帧

    • Close:正常关闭连接
    • Ping/Pong:保持连接活跃

协议优势

  • 低延迟:真正的双向实时通信
  • 低开销:相比 HTTP 轮询显著减少开销
  • 简单:易于实现和使用
  • 兼容性:可以与 HTTP 共享端口

应用场景

  • 实时聊天应用
  • 在线游戏
  • 实时数据推送
  • 协作编辑
  • 股票行情
  • 监控仪表板

浏览器支持

  • Chrome 4+
  • Firefox 4+
  • Safari 5+
  • Edge 12+
  • Opera 10.6+

相关资源

相关推荐
浔川python社2 小时前
《C++ 小程序编写系列》(第六部)
java·网络·rpc
23124_802 小时前
HTTPS中间人攻击
网络·网络协议·https
小风呼呼吹儿2 小时前
Flutter 框架跨平台鸿蒙开发 - 倒计时秒表:打造多功能计时工具
网络·flutter·华为·harmonyos
河码匠2 小时前
namespace 网络命名空间、使用网络命名空间实现虚拟路由
linux·网络
开开心心就好2 小时前
打印机驱动搜索下载工具,自动识别手动搜
java·linux·开发语言·网络·stm32·物联网·电脑
海域云-罗鹏3 小时前
马来西亚工厂与内地数据中心SD-WAN组网全指南
服务器·网络
txinyu的博客3 小时前
解析muduo源码之 TimeZone.h & TimeZone.cc
linux·服务器·网络·c++
阿拉伯柠檬3 小时前
网络层协议IP(二)
linux·网络·网络协议·tcp/ip·面试
云小逸3 小时前
同网段 vs 不同网段通信:深入解析网络通信的底层原理
网络