WebSocket协议详解及应用(六)-WebSocket协议控制帧结构详解

WebSocket控制帧有3种:Close(关闭帧)、Ping以及Pong。

一、控制帧

控制帧是由操作码上的位值置为1来定义的。目前,控制帧的操作码定义了0x08(关闭帧)、0x09(Ping帧)、0x0A(Pong帧)。0x0B-0x0F是为那些将来可能定义而目前尚未定义的控制帧预留的。
控制帧用于WebSocket协议交换状态信息,控制帧可以插在消息片段之间。
注意:所有的控制帧的负载长度务必不大于125字节,并且禁止对控制帧进行分片处理。

二、关闭帧

关闭帧的操作码是0x08。
关闭帧可能包含数据部分(应用数据帧),该部分表明了关闭的原因,例如端点关闭、端点接收帧过大或端点收到的帧不符合预期。如果有数据部分,则数据的前两个字节必须是一个无符号整数(网络字节序),该无符号整数表示了一个状态码,具体定义哪些关闭码将在后面的文章中介绍。在无符号整数后面,可能还有一个UTF-8编码的数据,表示关闭原因,关闭原因由开发者自行定义(可选),并无规范。关闭原因并不一定是对人可读的,但会对调试或传递相关信息起到一定的作用。由于数据不能保证可读,所以客户端不应将其显示给用户(会在关闭事件onclose中)。
客户端发送给服务器的关闭帧必须掩码处理。
应用程序在发送了一个关闭帧后,禁止再发送任何数据(此时处于CLOSING状态)。
如果端点(客户端或服务器)收到了一个关闭帧,并且之前没有发送过关闭帧,则端点必须发送一个关闭帧作为响应。(当端点发送一个关闭帧回应时,通常会显示它收到的状态码。)当端点可以发送关闭响应时应尽快发送关闭响应。一个端点可以延迟发送响应直到它的当前消息发送完毕(例如,已经发送了大多数的消息片段,则端点可能会在发送关闭响应帧前先将剩下的消息帧发送出去)。但不能保证对方在已经发送了关闭帧后还能够继续处理这些数据。
在双方都以发送并接收了关闭帧后,端点需要断掉WebSocket连接并且必须关闭底层的TCP连接。服务器必须立即切断底层TCP连接,客户端最好等待服务器断开连接,但也可以在发送并接收了关闭帧后任何时候断开连接,例如在一段时间内服务器仍没有断开TCP连接。
如果服务器和客户端同时发送了关闭帧,两端都会接收关闭帧,并且都需要断开TCP连接。

三、PING帧

Ping帧的操作码为0x09。
Ping帧可以包含应用数据。
一旦接到了一个Ping帧,端点必须返回一个Pong帧作为响应,除非它收到了一个关闭帧。它应在可以发送时尽快发送Pong帧响应。
端点可以在连接建立后一直到连接关闭前任何时候发送Ping帧。
提示:Ping帧既可用于保持活动状态也可用于验证远端仍可响应数据。

四、PONG帧
Pong帧的操作码为0x0A。
Pong帧必须与Ping帧拥有相同的应用数据部分。
如果端点收到了多个Ping帧,但还没来的及全部回应,可以只回应最后一个Ping帧。
Pong帧可以在未收到Ping帧时就被发送,用作单向心跳包。
不需要对未被请求的Pong帧(对方主动发送的Pong帧)进行回应。

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;
}