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

WebSocket协议详解及应用(四)-WebSocket握手协议之通信建立与错误处理

本篇主要介绍浏览器在服务器返回的握手协议有问题时如何进行处理。注:本文介绍的内容已在浏览器内部实现,不需要做任何编码工作,以下内容只介绍原理和协议本身。

一、连接异常的情况

以下情况,客户端必须停止握手并断开连接:

  1. 服务器域名不能解析
  2. 数据包不能成功的传递到服务器
  3. 服务器指定端口禁止连接
  4. 服务器TLS传输失败,例如服务器证书无法验证
  5. 服务器无法完成握手协议,例如目标服务器不是WebSocket服务器(返回非101状态码)
  6. 服务器握手成功,但一些选项引起客户端放弃连接(如服务器提供了客户端无法识别子协议)
  7. 服务器在完成握手协议后意外关闭了连接

以上所有的情况都会使WebSocket以1006(连接意外断开)的退出码断开连接。

二、握手成功后浏览器的工作

当整个握手阶段无任何异常时,服务器与浏览器已经建立连接,此时浏览器会完成一些初始化工作。
首先将readyState属性设置为OPEN(WebSocket的常量,值为1),readyState是WebSocket的一个属性,代表连接的状态,包含CONNECTING、OPEN、CLOSING、CLOSED等4个状态。其中OPEN即连接已建立,可以进行数据通信了。
如果extensions非空,则设置此属性的值。
如果protocol的值非空,则设置此属性的值。
如果返回了cookie设置的响应头,则需要将指定的cookie设置到WebSocket构造函数的第二个参数URL下。
最后触发WebSocket的onopen事件处理函数。

三、WebSocket对象的属性及方法介绍

调用WebSocket构造方法后,会返回一个WebSocket对象,类似如下:

URL: "ws://localhost/"
binaryType: "blob"
bufferedAmount: 0
extensions: ""
onclose: null
onerror: null
onmessage: null
onopen: null
protocol: ""
readyState: 3
url: "ws://localhost/"
CLOSED: 3
CLOSING: 2
CONNECTING: 0
OPEN: 1
close: function close() { [native code] }
send: function send() { [native code] }

binaryType为二进制使用哪种类型的数据结构,取值可以是blob或arraybuffer.
bufferedAmount为缓存中剩余的字节数,有时数据并不是准备好才传输到网络上,而是一边生成一边传递,这时需要将数据缓存到一定大小再发送,并且需要定时将缓存中剩余的内容发送出去。bufferedAmount就记录了send函数中的应用数据(UTF-8的字符串或二进制的数据)队列中还未被发送到网络的字节数。如果连接关闭,该属性值会在send方法被调用时增长。
onclose在连接关闭时触发
onerror在发生错误时触发
onmessage在浏览器接到服务器发来的数据时触发
onopen在连接建立时触发
readyState连接状态
CLOSED连接已经关闭
CLOSING连接关闭中,此时处于半关闭状态,浏览器已经发送了关闭指令,但此时服务器还未响应关闭指令。浏览器不能再收发数据了。
CONNECTING正在连接中,浏览器已经发送了握手请求,等待服务器返回握手响应。
OPEN已建立连接,可以传输数据。
close方法用于浏览器主动关闭连接,它会发送操作码8到服务器,它接受2个可选参数,关闭码和关闭原因。
send方法用于浏览器向服务器发送数据,它支持多种数据类型的传输。