转载

通过Swoole Framework学习websocket协议

服务端的开发离不开协议,swoole的出现对于学习通信来说,无疑是非常好的教材。非常推荐大家下载 Swoole Framework ,其中包含了多种协议的php实现,例如FTP,HTTP,Websocket等。本文大部分代码都是受这个项目的启发,当然学习的同时别忘了star一下这个项目。笔者本身计算机基础较弱,写这篇文章的同时也查了不少资料,如果有错误欢迎提出批评。

websocket协议学习

概述

协议分为两部分:握手,数据传输

客户端发出的握手信息类似:

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= 

Frame(帧)的结构如下:

  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的说明,指明这个帧的类型。
  1. 0x0 表明为接着上一帧的连续帧
  2. 0x1 表明为text frame
  3. 0x2 表明为binary frame
  4. 0x3-7 保留
  5. 0x8 表示连接关闭
  6. 0x9 为ping
  7. 0xA 为pong
  8. 0xB-F 保留

- 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, 扩展数据的长度计算方法是是事先商议好的,剩余的就是应用数据。

Mask规则

masking-key 是客户端随意指定的32bit长度值。从原始数据到masked数据的方式为: 原始数据第i个字节的值 XOR masking-key的第(i%4)个字节的值 。XOR表示异或,%表示取模。

片段化的作用

当传递一个未知长度的数据时,可以不用一下子buffer全部的数据。尤其当数据非常大时,可以分多次buffer,包装为frame来发送。

尝试解析一个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"; }  

结果输出如下图:

通过Swoole Framework学习websocket协议

到这里其实并不算完,ws协议还有很多很多规则,RFC文档实在是太长了。比如,如何应对每一种control frame,有详细的说明;如何关闭连接;协议扩展;错误处理;安全相关;一些基本的内容都能在swoole framework中找到对应的代码。

正文到此结束
Loading...