WebSocket 协议

参考文章

websocket RFC github 中文翻译php

Websocket RFC 文档git

workerman websocket 协议实现github

协议组成

协议由一个开放握手组成,其次是基本的消息成帧,分层的TCP.web

解决的问题

基于浏览器的机制,实现客户端与服务端的双向通讯.浏览器

协议概述

  1. 来自客户端握手
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
  1. 来自服务端的握手
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
// 可选的头,表示容许的经过的客户端
Sec-WebSocket-Protocol: chat

以上,头顺序无所谓.服务器

一旦客户端和服务器都发送了握手信号,若是握手成功,数据传输部分启动。这是双方沟通的渠道,独立于另外一方,可随意发送数据。websocket

服务器的响应,不是随意的,须要遵循必定的规则 请参考RFC 文档 第 6/7页:cookie

  1. 获取客户端请求的 Sec-Weboscket-Key 字段值,去除收尾空白字符
  2. 与全球惟一标识符拼接 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
  3. sha1 加密(短格式)
  4. base64 加密

PHP 程序描述:网络

$client_key = 'dGhlIHNhbXBsZSBub25jZQ==';
$client_key = trim($client_key);
$guid       = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
$key        = $client_key . $guid;
$key        = sha1($key , true);
$key        = base64_encode($key);

上述结果得出的值便是服务端返回给客户端握手的 Sec-Websocket-Accept 头字段值.框架

关闭连接

接收到一个 0x8 控制帧后,连接也许当即断开,也许在接收完剩下的数据后断开。

  • 能够有消息体,指明消息缘由,可做为日志进行记录。
  • 应用发送关闭帧后必须不在发送更多数据帧。
  • 若是一个端点接受到一个关闭帧且先前没有发送关闭帧,则必须发送一个关闭帧。
  • 端点在接受到关闭帧后,能够延迟响应关闭帧,继续发送或接受数据帧,但不保证一个已经发送关闭帧的端点继续处理数据。
  • 发送并接收了关闭帧的端点,被认为是关闭了 websocket 链接,其必须关闭底层的 TCP 链接。

设计理念

基于框架而不是基于流/文本或二进制帧.

连接要求

针对客户端要求

  • 握手必须是一个有效的 HTTP 请求
  • 请求的方法必须为 GET,且 HTTP 版本必须是 1.1
  • 请求的 REQUEST-URI 必须符合文档规定的要求(详情查看 Page 13)
  • 请求必须包含 Host
  • 请求必须包含 Upgrade: websocket 头,值必须为 websocket
  • 请求必须包含 Connection: Upgrade 头,值必须为 Upgrade
  • 请求必须包含 Sec-WebSocket-Key
  • 请求必须包含 Sec-WebSocket-Version: 13 头,值必须为 13
  • 请求必须包含 Origin
  • 请求可能包含 Sec-WebSocket-Protocol 头,规定子协议
  • 请求可能包含 Sec-WebSocket-Extensions ,规定协议扩展
  • 请求可能包含其余字段,如 cookie

不符合上述要求的服务器响应,客户端都会断开连接.

  • 若是响应不包含 Sec-WebSocket-Protocol 中指定的子协议,客户端断开
  • 若是响应 HTTP/1.1 101 Switching Protocols 状态码不是 101,客户端断开

针对服务端要求

  • 若是请求是 HTTP/1.1 或更高的 GET 请求,包含 REQUEST-URI 则应正确地按照文档要求进行解析.
  • 必须验证 Host 字段
  • Upgrade 头字段值必须是大小写不敏感的 websocket
  • Sec-WebSocket-keyd 解码时长度为 16Byte
  • Sec-WebSocket-Version 值必须是 13
  • Host 若是没有被包含,则连接不该该被解释为浏览器发起的行为
  • Sec-WebSocket-Protocol 中列出的客户端请求的子协议,服务端应按照优先顺序排列,响应
  • 任选的其余字段

响应要求:

  • 验证 Origin 字段,若是不符合要求的请求则返回适当的错误代码(例如:403)
  • Sec-WebSocket-Key 值是一个 base64 加密后的值,服务端不须要对其进行解码,而仅是用来建立服务器的握手.
  • 验证 Sec-WebSocket-Version 值,若是不是 13,则返回一个适当的错误代码(例如:HTTP/1.1 426 Upgrade Required)
  • 资源名验证
  • 子协议验证
  • extensions 验证

若是经过了上述验证,则服务器表示接受该连接.那么起响应必须符合如下要求详情查看 Page 23:

  1. 必须,状态行 HTTP/1.1 101 Switching Protocols
  2. 必须,协议升级头 Upgrade: websocket
  3. 必须,表示链接升级的头字段 Connection: Upgrade
  4. 必须,Sec-WebSocket-Accept 头字段,详情请查阅 协议概述 部分
  5. 可选:Sec-WebSocket-Protocols 头部

完整的响应代码以下(严格按照以下格式响应!!头部顺序无所谓!关键是后面的换行符注意了!严格控制数量!):

HTTP/1.1 101 Switching Protocols\r\n
Connection: Upgrade\r\n
Upgrade: websocket\r\n
Sec-WebSocket-Accept: 3nlEzv+LqVBYnTHclAqtk62uOTQ=\r\n
// 下面这个头字段为可选字段
Sec-WebSocket-Protocols: chat\r\n\r\n

基本框架协议

数据传输部分对 进行了分组!!因为是在bit层面上进行的数据封装,因此若是直接取出的话,获取到的将是处理后的数据,须要解密。下图是传输数据格式

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 ...                |
 +---------------------------------------------------------------+

1. 特殊名词含义介绍

  1. 1bit,FIN
  2. 每一个 1bit, RSV一、RSV二、RSV3
  3. 4bit,opcode(如下定义在ABNF中)

    • %x0 连续帧
    • %x1 文本帧
    • %x2 二进制帧
    • %x3 - %x7 保留帧
    • %x8 连接关闭
    • %x9 ping
    • %xA pong
    • %xB-F 保留的控制帧
    • 以上表示的都是 16 进制数值
  4. 1bit, mask

    • 客户端发送给服务端的数据都须要设置为 1
    • 也就是说数据都是通过掩码处理过的
  5. 7bit、7 + 16bit、7 + 64bit,Payload length 具体范围请参阅 RFC 文档(Page 31)

    • Playload length = Extended Payload length + Application Payload length
    • 有效载荷长度 = 扩展数据长度 + 应用程序数据长度
    • 扩展数据长度有可能为 0,因此当 扩展数据长度 = 0 的时候,有效载荷长度 = 应用程序长度
    • 有效载荷数据的长度单位为 Byte
  6. 0/4 byte, masking-key

    • 客户端发送给服务端的数据都是通过掩码处理的,长度为 32bit
    • 服务端发送给客户端的数据都是未通过掩码处理的,长度为 0bit
  7. x + y Byte, Payload Data

    • 有效载荷数据
  8. x Byte, Extension Data

    • 扩展数据
  9. y Byte, Application Data

    • 应用数据

2. 理解

图中表示遵循 websocket 协议进行传输的数据,因为是通过 websocket 协议处理后的数据,因此没法直接获取有效数据。若是想要获取有效数据,就须要按照 websocket 协议规定进行解读。

图中从左往右,按高位到低位进行排列。

什么是低位、高位??

就像是十进制数字,若是有一个描述是这样的:3表示个位,2 表示十位,1表示百位,请问这个数字是??答案:123

这就很好理解了,个位、十位、百位 描述了排列顺序;一样的,在程序领域,低位到高位描述的也是排列顺序!不过 个位、十位、百位描述的是10进制的排列顺序,而 低位、高位描述的是 2进制 的排列顺序,具体描述是 位0、位一、位2.... 等(当前举例中的的排列顺序为低位到高位),如下是图片描述:

描述

理解了低位、高位,就清楚了上图描述的数据排列顺序。

众所周知,位(bit)是内存中的最小存储单位,仅能存 0、1两个数值。因此要想获取、设置某位的值,须要进行位操做。因为是在上进行操做者,因此,图中描述的内容是在补码的基础上进行的。

客户端发送给服务端的数据是通过掩码处理的! 须要进行解析,解析数据流程:

// 按照 websocket 规范解析客户端加密数据
function decode(string $buffer){
    // buffer[0] 获取第一个字节,8bit
    // 对照那张图,表示的是 fin + rsv1 + rsv2 + rsv 3 + opcode
    // 之因此要转换为 ASCII 码值
    // 是为了确保位运算结果正确!
    // php 位运算详情参考:https://note.youdao.com/share/?id=927bfc2f40a8d62f4c9165de30a41e75&type=note#/
    // 这边作一点简单解释
    // 后面的代码会有 $first_byte >> 7 这样的代码
    // php 中 << >> 都会将操做数当成是整型数(int) 
    // 因此若是不转换成 ascii 值的话,过程将会是
    // (int) $buffer[0] >> 7
    // 这样的结果将是错误的!!
    // ord((int) $buffer[0]) !== ord($buffer[0]) 就是最好的证实
    // 由于 ascii 值不同,则二进制值(严格一点,我认为应该说成是:补码)也不同
    // 这违反了 websocket 规定的协议
    // 会致使解析错误
    $first_byte  = ord($buffer[0]);
    // buffer[1] 获取第二个字节,8bit
    // 对照那张图,表示的是 mask + payload len
    $second_byte = ord($buffer[1]);
    
    // 获取左边第一位值
    $fin = $first_byte >> 7;
    // 对照那张图,要想获取 payload len 表示的值
    // 须要设置 位 7 为 0
    // 由于位 7 表示的是掩码,位 0 - 6 表示的是 paylaod len 的补码
    // 因此要想获取 payload len 的值
    // 0111 1111 => 127
    $payload_len = $second_byte & 127;
    
    // 客户端发送给服务端的数据是通过掩码处理的
    // 因此要获取 掩码键 + 掩码处理事后的客户端数据
    // 获取 mask-key + payload data
    if ($payload_len === 127) {
        // 若是 payload len = 127 byte
        // payload len 自己占据 7bit
        // extended payload lenght 占据 64bit
        $mask_key       = substr($buffer , 10 , 4);
        $encoded_data   = substr($buffer , 14);
    } else if ($payload_len === 126) {
        // 若是 payload len = 126 byte
        // payload length 自己占据 7bit
        // extended payload lenght 占据 16bit
        $mask_key       = substr($buffer , 4 , 4);
        $encoded_data   = substr($buffer , 8);
    } else {
        // 若是 payload len = 126 byte
        // payload length 自己占据 7bit
        // extended payload lenght 占据 0bit
        $mask_key       = substr($buffer , 2 , 4);
        $encoded_data   = substr($buffer , 6);
    }
    
    // 对 payload data 进行解码
    $decoded_data = "";
    
    // 对每个有效载荷数据进行解码操做
    // 解码规则在 RFC 文档中有详细描述
    for ($index = 0; $index < count($encoded_data); ++$index)
    {
        $k              = $index % 4;
        $valid_data     = $encoded_data[$index] ^ $mask_data[$k];
        $decoded_data  .= $valid_data;
    }
    
    // 这个就是客户端发送的真实数据!!
    return $decoded_data;
}

相反,若是服务器想要发送数据给 websocket 客户端,则也要对数据进行相应处理!处理流程:

// 按照 websocket 规范封装发送给客户端的消息
function encode($msg){
    if (!is_scalar($msg)) {
        print_r("只容许发送标量数据");
    }
    
    // 数据长度
    $len = strlen($msg);
    
    // 这边仅实现传输文本帧!第一个字节,文本帧 1000 0001 => 129
    // 若是须要例如二进制帧,用于传输大文件,请另行实现
    $first_byte = chr(129);
    
    if ($len <= 125) {
        // payload length = 7bit 支持的最大范围!
        $second_byte = chr($len);
    } else {
        if ($len <= 65535) {
            // payload length = 7 , extended payload length = 16bit,支持的最大范围 65535
            // 最后16bit 被解释为无符号整数,排序为:大端字节序(网络字节序)
            $second_byte = chr(126) . pack('n' , $len);
        } else {
            // payload length = 7,extended payload length = 64bit
            // 最后 64 位被解释为无符号整数,大端字节序(网络字节序)
            $second_byte = chr(127) . pack('J' , $len);
        }
    }
    
    // 注意了,发送给客户端的数据不须要处理
    // 详情查看 websocket 文档!!
    $encoded_data = $first_byte . $second_byte . $buffer;
    
    // 这个就是发送给客户端的数据!   
    return $encoded_data;
}

消息分片

分片目的

消息分片的主要目的是容许消息开始但没必要缓冲整个消息时,发送一个未知大小的消息;未分片的消息须要缓冲整个消息,以便获取消息大小;

分片要求:

  • 首个分片 Fin = 0,opcode != 0x0,其后跟随多个 Fin = 0,opcode = 0x0的分片,终止于 Fin = 1,opcode = 0x0的片断
  • 扩展数据可能发生在分片中的任意一个分片中
  • 控制帧可能被注入到分片消息的中间,控制帧自己必须不被分割
  • 消息分片必须按照发送者发送顺序交付给收件人
  • 片断中的一个消息必须不能与片断中的另外一个消息交替,除非已协商了一个能解释交替的扩展。
  • websocket服务器应可以处理分片消息中间的控制帧
  • 一个发送者能够为非控制消息(非控制帧)建立任何大小的片断
  • 不能处理控制帧
  • 若是使用了任何保留的位值且这些值的意思对中间件是未知的,一个中间件必须不改变一个消息的分片。
  • 在一个链接上下文中,已经协商了扩展且中间件不知道协商的扩展的语义,一个中间件必须不改变任何消息的分片。一样,没有看见WebSocket握手(且没被通知有关它的内容)、致使一个WebSocket链接的一个中间件,必须不改变这个连接的任何消息的分片。
  • 因为这些规则,一个消息的全部分片是相同类型,以第一个片断的操做码设置。由于控制帧不能被分片,用于一个消息中的全部分片的类型必须或者是文本、或者二进制、或者一个保留的操做码。

ping

接受到一个 ping(0x9) 控制帧,必须返回一个 pong(0xa) 控制帧,表示进程还在!!实际就是心跳检查

pong

  1. 能够在接收到 ping(0x9) 控制帧后,做为响应消息返回。
  2. 也能够单向发送 pong 帧,表示发送方进程还在,做为单向心跳

状态码

  1. 1000,正常关闭
  2. 1001,正在离开
  3. 1003,正在关闭链接
  4. 1004,保留
  5. 1005,保留
  6. 1006,保留
  7. 1007,端点正在终止链接,由于它收到的消息中没有与消息类型一致。
  8. 1008,端点正在终止连接,由于接收到了违反其规则的消息。
  9. 1009,端点正在终止连接,由于接受到的消息太大
  10. 1010,端点正在终止连接,由于扩展问题
  11. 1011,端点正在终止连接,发生了之外错误
  12. 1015,保留
  13. .....省略了部分,详情参考 rfc 文档

尾部

以上我的理解,仅供参考,有错欢迎纠正,未完待续 ....

相关文章
相关标签/搜索