workerman websocket 协议实现github
协议由一个开放握手组成,其次是基本的消息成帧,分层的TCP.web
基于浏览器的机制,实现客户端与服务端的双向通讯.浏览器
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
以上,头顺序无所谓.服务器
一旦客户端和服务器都发送了握手信号,若是握手成功,数据传输部分启动。这是双方沟通的渠道,独立于另外一方,可随意发送数据。websocket
服务器的响应,不是随意的,须要遵循必定的规则 请参考RFC 文档 第 6/7页:cookie
Sec-Weboscket-Key
字段值,去除收尾空白字符258EAFA5-E914-47DA-95CA-C5AB0DC85B11
sha1
加密(短格式)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
链接。基于框架而不是基于流/文本或二进制帧.
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
则应正确地按照文档要求进行解析.Upgrade
头字段值必须是大小写不敏感的 websocket
Sec-WebSocket-key
d 解码时长度为 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
)若是经过了上述验证,则服务器表示接受该连接.那么起响应必须符合如下要求详情查看 Page 23:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept
头字段,详情请查阅 协议概述 部分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 ... | +---------------------------------------------------------------+
4bit,opcode(如下定义在ABNF中)
1bit, mask
7bit、7 + 16bit、7 + 64bit,Payload length 具体范围请参阅 RFC 文档(Page 31)
Byte
0/4 byte, masking-key
x + y Byte, Payload Data
x Byte, Extension Data
y Byte, Application Data
图中表示遵循 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; }
消息分片的主要目的是容许消息开始但没必要缓冲整个消息时,发送一个未知大小的消息;未分片的消息须要缓冲整个消息,以便获取消息大小;
接受到一个 ping(0x9)
控制帧,必须返回一个 pong(0xa)
控制帧,表示进程还在!!实际就是心跳检查
ping(0x9)
控制帧后,做为响应消息返回。pong
帧,表示发送方进程还在,做为单向心跳以上我的理解,仅供参考,有错欢迎纠正,未完待续 ....