WebSocket协议详解及应用(五)-WebSocket协议帧结构详解

本篇主要介绍WebSocket协议的帧结构,详细讲解WebSocket帧的构成。

一、帧结构图及含义

 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1
 0 1 2 3 4 5 6 7 8 9 A B C D E F 0 1 2 3 4 5 6 7 8 9 A B C D E F
+-+-+-+-+-------+-+-------------+-------------------------------+
|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 ...                  |
+---------------------------------------------------------------+

0Bit:
FIN 结束标识位,如果FIN为1,代表该帧为结束帧(如果一条消息过长可以将其拆分为多个帧,这时候FIN可以置为0,表示后面还有数据帧,服务器需要将该帧内容缓存起来,待所有帧都接收后再拼接到一起。控制帧不可拆分为多帧)。

1~3Bit:
RSV1~RSV3 保留标识位,以后做协议扩展时才会用到,目前该3位都为0

4~7Bit:
opcode 操作码,用于标识该帧负载的类型,如果收到了未知的操作码,则根据协议,需要断开WebSocket连接。操作码含义如下:
0x00 连续帧,表示这个帧需要与前面收到的FIN标识不为1的帧拼接成完整的帧后才能使用,如果该帧FIN标识仍为0,则表示后面还有帧。
0x01 文本帧,最常用到的数据帧类别之一,表示该帧的负载是一段文本(UTF-8字符流)
0x02 二进制帧,较常用到的数据帧类别之一,表示该帧的负载是二进制数据,blob、ArrayBuffer类型的数据都是二进制帧。
0x03-0x07 保留帧,留作未来非控制帧扩展使用
0x08 关闭连接控制帧,表示要断开WebSocket连接,浏览器端调用close方法会发送0x08控制帧
0x09 ping帧,用于检测端点是否可用,浏览器不允许主动发送该帧(浏览器不提供API)。
0x0A pong帧,用于回复ping帧,浏览器不允许主动发送该帧(浏览器不提供API)。
0x0B-0x0F 保留帧,留作未来控制帧扩展使用
注(翻译自WHATWG):WebSocket协议定义了Ping和Pong帧用于keep-alive、心跳包以及网络状态探测、延迟测量等。这些目前不暴露在API中。
用户代理可以发送ping和主动的pong帧,例如尝试保持本地网络NAT映射、检测失败连接或向用户显示延迟测量。用户代理不能使用ping或主动pong来帮助服务器,服务器在需要的时候会发起pong帧。

8Bit:
MASK 掩码标识位,用来表明负载是否经过掩码处理,浏览器发送的数据都是经过掩码处理(浏览器自动处理,无需开发者编码),服务器发送的帧必须不经过掩码处理。所以此处浏览器发送的帧必为1,服务器发送的帧必为0,否则应断开WebSocket连接。

9~15Bit:
payload length 负载长度,如果负载长度0~125字节,则此处的一个字节就是负载长度的字节数;如果负载长度在126~65535之间,则此处第一个字节的值为126,后面第16~32Bit(2字节)表示负载的真实长度。如果负载长度在65536~2的64次方-1时,此处第一个字节为127,后面第16~80Bit(8字节)表示负载的真实长度。其中负载长度包括应用数据长度和扩展数据的长度。

payload length 后面4个字节可能是掩码的key(如果掩码位是1则有这4个字节的key,否则没有),掩码计算方法将在后面给出。

接下来就是负载的数据了,它们可能需要根据掩码的key进行编码(仅浏览器需要掩码),如果存在扩展数据,需要放在应用数据之前。

二、掩码计算
如果你只需要做浏览器端的编程,可以忽略以下内容,浏览器会自动计算掩码。如果你需要做服务端编程,则需要详细阅读下面内容,你必须根据掩码计算的方法将浏览器发送的数据帧进行解码操作。
掩码的key值是一个由客户端随机选择的32比特值。首先,浏览器必须从掩码可用的key值中选择一个值,用于封装数据帧。掩码的key必须是一个不可预测的值(例如从一个健壮的熵值中获取),并且必须使服务器或代理不能简单的预测出接下来的key值,以防止有人使用恶意的应用通过监听网络选择key值。
掩码操作不会影响负载的长度,通过下面的算法可以进行编码和解码,编码和解码的步骤完全一样:

  1. 将负载和掩码分成8位位组(分割成字节)
  2. 将负载每一个字节与掩码的每个字节做循环位异或操作
  3. 将结果得到的每一个字节拼接到一起即为掩码计算后的数据

使用PHP计算代码如下:

function parseRawFrame($payload, $mask) {
    $payloadLen = strlen($payload);
    $dest = '';
    $maskArr = array();
    for ($i = 0; $i < 4; $i++) {
        $maskArr[$i] = ord($mask[$i]);
    }
    for ($i = 0; $i < $payloadLen; $i++) {
        $dest .= chr(ord($payload[$i]) ^ $maskArr[$i % 4]);
    }
    return $dest;
}