参考文章
websocket RFC 文档
数据包 和 数据帧表示什么??
网页实时聊天之 PHP 实现 websocket
websocket 数据帧
workerman websocket 协议实现, 太给力!!
协议组成
协议由一个开放握手组成, 其次是基本的消息成帧, 分层的 TCP.
解决的问题
基于浏览器的机制, 实现客户端与服务端的双向通信.
协议概述
来自客户端握手
- 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
来自服务端的握手
- HTTP/1.1 101 Switching Protocols
- Upgrade: websocket
- Connection: Upgrade
- Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
- // 可选的头, 表示允许的通过的客户端
- Sec-WebSocket-Protocol: chat
以上, 头顺序无所谓.
一旦客户端和服务器都发送了握手信号, 如果握手成功, 数据传输部分启动这是双方沟通的渠道, 独立于另一方, 可随意发送数据
服务器的响应, 不是随意的, 需要遵循一定的规则 请参考 RFC 文档 第 6/7 页:
获取客户端请求的 Sec-Weboscket-Key 字段值, 去除收尾空白字符
与全球唯一标识符拼接
258EAFA5 - E914 - 47DA - 95CA - C5AB0DC85B11
sha1 加密(短格式)
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
头字段值.
关闭链接
待补充.
设计理念
基于框架而不是基于流 / 文本或二进制帧.
链接要求
针对客户端要求
握手必须是一个有效的 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:
必须, 状态行
HTTP / 1.1 101 Switching Protocols
必须, 协议升级头 Upgrade: websocket
必须, 表示连接升级的头字段 Connection: Upgrade
必须,
Sec - WebSocket - Accept
头字段, 详情请查阅 协议概述 部分
可选:
Sec - WebSocket - Protocols
头部
完整的响应代码如下:
- HTTP/1.1 101 Switching Protocols
- Connection: Upgrade
- Upgrade: websocket
- Sec-WebSocket-Accept: 3nlEzv+LqVBYnTHclAqtk62uOTQ=
- // 下面这个头字段为可选字段
- Sec-WebSocket-Protocols: chat
基本框架协议
数据传输部分对 位 进行了分组!! 由于是在 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. 特殊名词含义介绍
1bit,FIN
每个 1bit, RSV1RSV2RSV3
4bit,opcode(以下定义在 ABNF 中)
%x0 连续帧
%x1 文本帧
%x2 二进制帧
%x3 - %x7 保留帧
%x8 链接关闭
%x9 ping
%xA pong
%xB-F 保留的控制帧
以上表示的都是 16 进制数值
1bit, mask
客户端发送给服务端的数据都需要设置为 1
也就是说数据都是经过掩码处理过的
7bit7 + 16bit7 + 64bit,Payload length 具体范围请参阅 RFC 文档(Page 31)
Playload length = Extended Payload length + Application Payload length
有效载荷长度 = 扩展数据长度 + 应用程序数据长度
扩展数据长度有可能为 0, 所以当 扩展数据长度 = 0 的时候, 有效载荷长度 = 应用程序长度
有效载荷数据的长度单位为 Byte
0/4 byte, masking-key
客户端发送给服务端的数据都是经过掩码处理的, 长度为 32bit
服务端发送给客户端的数据都是未经过掩码处理的, 长度为 0bit
x + y Byte, Payload Data
有效载荷数据
x Byte, Extension Data
扩展数据
y Byte, Application Data
应用数据
2. 理解
图中表示遵循 websocket 协议进行传输的数据, 由于是经过 websocket 协议处理后的数据, 所以无法直接获取有效数据如果想要获取有效数据, 就需要按照 websocket 协议规定进行解读
图中从左往右, 由低位到高位进行排列 Fin 长度 1bit, 位 0,RSV1RSV2RSV3 长度各 1bit, 分别是位 1 位 2 位 3, 以此类推
什么是低位高位??
就像是十进制数字, 如果有一个描述是这样的:
3
表示个位, 2 表示十位, 1 表示百位, 请问这个数字是?? 答案:
123
这就很好理解了,
个位十位百位
描述的是排列顺序; 同样的, 在程序领域, 低位到高位描述的也是排列顺序! 不过
个位十位百位
描述的是 10 进制的排列顺序, 而 低位高位描述的是 2 进制 的排列顺序, 具体描述是
位 0 位 1 位 2....
等
理解了低位高位, 就清楚了上图描述的数据排列顺序
众所周知, 位 (bit) 是内存中的最小存储单位, 仅能存 01 两个数值所以要想获取设置某位的值, 需要进行位操作解析数据流程:
- <?php
- // 之所以要转换为 ASCII 码值
- // 是因为 &(位运算)要求为整型数!
- // 而 ASCII 码值为数字(整型数)
- $firstByte = ord($buffer[0]);
- $secondByte = ord($buffer[1]);
- // 获取第一位值
- $fin = $firstByte >> 7;
- // 127 => 0b01111111
- // 设置该字节第 1 位为 0 后, 获取到的数据即为 payload length
- // 根据有效载荷长度值, 确定有效载荷数据的起始范围
- $payload_len = $secondByte & 127;
- // 获取 mask-key + payload data
- if ($payload_len === 127) {
- $mask_key = substr($buffer , 10 , 4);
- $encoded_data = substr($buffer , 14);
- } else if ($payload_len === 126) {
- $mask_key = substr($buffer , 4 , 4);
- $encoded_data = substr($buffer , 8);
- } else {
- $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;
- }
- // $decoded_data
- // 这个就是客户端发送的真实数据!!
相反, 如果服务器想要发送数据给 websocket 客户端, 则也要对数据进行相应处理! 处理流程:
- <?php
- $buffer = "hello boys and girls!";
- if (!is_scalar($buffer)) {
- print_r("只允许发送标量数据");
- }
- // 数据长度
- $len = strlen($buffer);
- // 第一个字节, 文本帧 1000 0001 => 129
- $first_byte = chr(129);
- if ($len <= 125) {
- // payload length = 7bit 支持的最大范围!
- $second_byte = chr($len);
- } else {
- if ($len <= 65535) {
- // payload length = 7 + 16bit 支持的最大范围 65535
- // 最后 16bit 被解释为无符号整数, 排序为: 大端字节序(网络字节序)
- $second_byte = chr(126) . pack('n' , $len);
- } else {
- // payload length = 7 + 64bit
- // 最后 64 位被解释为无符号整数, 大端字节序(网络字节序)
- $second_byte = chr(127) . pack('J' , $len);
- }
- }
- // 注意了, 发送给客户端的数据不需要处理
- // 详情查看 websocket 文档!!
- $encoded_data = $first_byte . $second_byte . $buffer;
- // $encoded_data
- // 这个就是发送给客户端的数据!
来源: https://segmentfault.com/a/1190000013298527