WebSocket 协定

参考文章

websocket RFC github 中文翻译

Websocket RFC 文档

workerman websocket 协定完成

协定构成

协定由一个开放握手构成,其次是基础的音讯成帧,分层的TCP.

处置惩罚的题目

基于浏览器的机制,完成客户端与服务端的双向通信.

协定概述

  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

以上,头递次无所谓.

一旦客户端和服务器都发送了握手信号,假如握手胜利,数据传输部份启动。这是两边沟通的渠道,独立于另一方,可随便发送数据。

服务器的响应,不是随便的,须要遵照肯定的划定规矩 请参考RFC 文档 第 6/7页:

  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, RSV1、RSV2、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、位1、位2.... 等(当前举例中的的分列递次为低位到高位),以下是图片形貌:

《WebSocket 协定》

明白了低位、高位,就清晰了上图形貌的数据分列递次。

尽人皆知,位(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 文档

尾部

以上个人明白,仅供参考,有错迎接改正,未完待续 ….

    原文作者:灰色v碰触
    原文地址: https://segmentfault.com/a/1190000013298527
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞