服务端的开发离不开协议,swoole的出现对于学习通信来说,无疑是非常好的教材。非常推荐大家下载 Swoole Framework ,其中包含了多种协议的php实现,例如FTP,HTTP,Websocket等。本文大部分代码都是受这个项目的启发,当然学习的同时别忘了star一下这个项目。笔者本身计算机基础较弱,写这篇文章的同时也查了不少资料,如果有错误欢迎提出批评。
协议分为两部分:握手,数据传输
客户端发出的握手信息类似:
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 Sec-WebSocket-Version: 13
两段信息的第一行大家应该都比较熟悉,是HTTP协议中的Request-Line和Status-Line, RFC2616 。下面接着出现的是无序的头信息,这和HTTP协议相同。 一旦握手成功,一个双向连接通道就建立了。
连接用于传输 message
, message
由一个或多个 frame
组成。每个frame有一个类型,属于同一个message的frame的类型都相同。类型包括:文本,二进制,control frame(协议层信号)等。目前一共有6种类型,10种保留类型。
根据上面的客户端头信息可以看出,握手和HTTP是兼容的。WS的握手是HTTP的"升级版本"。
客户端发送的握手请求必须
1. 是一个合法的HTTP请求
2. 方法是GET
3. 头必须包含HOST字段
4. 头必须包含Upgrade字段,值为websocket,可以看作是判断请求为ws的标志。
5. 头必须包含Connection字段,值为Upgrade。
6. 头必须包含Sec-WebSocket-Key字段,用于验证。
7. 如果请求来自浏览器,头必须包含 Origin字段。
8. 头必须包含Sec-WebSocket-Version字段,值为13
取 Sec-WebSocket-Key
字段的值,连接一个GUID字符串,"258EAFA5-E914-47DA-95CA-C5AB0DC85B11", sha1 hash一下,再base64_encode,得到的值作为字段 Sec-WebSocket-Accept
的值返回给客户端。用php代码表示:
'Sec-WebSocket-Accept' => base64_encode(sha1($key . static::GUID, true))
同时,返回的状态设置为101,其他状态都表示握手没有成功。 Connection
, Upgrade
字段作为HTTP升级版必须存在。一个握手返回如下:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
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 ... | +---------------------------------------------------------------+
FIN
: 1 bit, 标记是否是最后一个message的最后一个片段 RSV1
, RSV2
, RSV3
: 各 1 bit, 保留标记,都为0 Opcode
:4 bits, 是对payload data的说明,指明这个帧的类型。 - Mask
: 1 bit, 指明Payload data是否被mask,如果为1,那么数据需要根据masking-key来unmask。客户端发送的帧都是mask的。
- Payload length
: 7 bits 或 7+16 bits 或 7+64 bits. 如果值为0-125,那么该值就是payload的长度;如果为126,那么接下来的2个byte表示payload长度(16bit, unsigned); 如果为127,那么接下来的8个bytes表示payload的长度(64bit, unsigned)。
- Masking-key
: 0 或 4 bytes, 用于unmask payload data。
- Payload data
: 长度为 Payload length, 可以分为 extension data + application data, 扩展数据的长度计算方法是是事先商议好的,剩余的就是应用数据。
masking-key
是客户端随意指定的32bit长度值。从原始数据到masked数据的方式为: 原始数据第i个字节的值 XOR masking-key的第(i%4)个字节的值
。XOR表示异或,%表示取模。
当传递一个未知长度的数据时,可以不用一下子buffer全部的数据。尤其当数据非常大时,可以分多次buffer,包装为frame来发送。
看到这里,我们已经了解了frame的结构,是否想尝试解析一个frame,官方文档提供了几段 二进制数据 ,我们可以用来练习一下。我挑选了其中两段, 代码如下:
php
<?php //A single-frame unmasked text message $data = array(0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f); //A single-frame masked text message $data2 = array(0x81, 0x85, 0x37, 0xfa, 0x21, 0x3d, 0x7f, 0x9f, 0x4d, 0x51, 0x58); handleData(toString($data)); handleData(toString($data2)); function toString(array $data) { return array_reduce($data, function($carry, $item){ return $carry .= chr($item); }); } function handleData($data){ $offset = 0; $temp = ord($data[$offset++]); $FIN = ($temp >> 7) & 0x1; $RSV1 = ($temp >> 6) & 0x1; $RSV2 = ($temp >> 5) & 0x1; $RSV3 = ($temp >> 4) & 0x1; $opcode = $temp & 0xf; echo "First byte: FIN is $FIN, RSV1-3 are $RSV1, $RSV2, $RSV3; Opcode is $opcode /n"; $temp = ord($data[$offset++]); $mask = ($temp >> 7) & 0x1; $payload_length = $temp & 0x7f; if($payload_length == 126){ $temp = substr($data, $offset, 2); $offset += 2; $temp = unpack('nl', $temp); $payload_length = $temp['l']; }elseif($payload_length == 127){ $temp = substr($data, $offset, 8); $offset += 8; $temp = unpack('nl', $temp); $payload_length = $temp['l']; } echo "mask is $mask, payload_length is $payload_length /n"; if($mask ==0){ $temp = substr($data, $offset); $content = ''; for ($i=0; $i < $payload_length; $i++) { $content .= $temp[$i]; } }else{ $masking_key = substr($data, $offset, 4); $offset += 4; $temp = substr($data, $offset); $content = ''; for ($i=0; $i < $payload_length; $i++) { $content .= chr(ord($temp[$i]) ^ ord($masking_key[$i%4])); } } echo "content is $content /n"; }
结果输出如下图:
到这里其实并不算完,ws协议还有很多很多规则,RFC文档实在是太长了。比如,如何应对每一种control frame,有详细的说明;如何关闭连接;协议扩展;错误处理;安全相关;一些基本的内容都能在swoole framework中找到对应的代码。