php使用WebSocket详细教程之对接收数据解包及发送数据包装(二)

接上篇介绍如何建立连接等基础了解,接下来介绍的是服务器接收到数据的转化,获得真实数据。
本篇需要理解的内容:

  1. WebSocket数据的收发协议?
  2. 什么是masking-key?
  3. php的两个函数pack()与unpack()?
  4. 理解数据包装与数据解包

(一)WebSocket数据的收发协议
首先,对于客户端向服务器发送数据,都是以数据帧形式传输,下面给出数据帧格式

复制代码
 1 0                   1                   2                   3
 2   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
 3  +-+-+-+-+-------+-+-------------+-------------------------------+
 4  |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
 5  |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
 6  |N|V|V|V|       |S|             |   (if payload len==126/127)   |
 7  | |1|2|3|       |K|             |                               |
 8  +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
 9  |     Extended payload length continued, if payload len == 127  |
10  + - - - - - - - - - - - - - - - +-------------------------------+
11  |                               |Masking-key, if MASK set to 1  |
12  +-------------------------------+-------------------------------+
13  | Masking-key (continued)       |          Payload Data         |
14  +-------------------------------- - - - - - - - - - - - - - - - +
15  :                     Payload Data continued ...                :
16  + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
17  |                     Payload Data continued ...                |
18  +---------------------------------------------------------------+
19  
20  具体每一bit的意思
21 FIN      1bit 表示信息的最后一帧
22 RSV 1-3  1bit each 以后备用的 默认都为 0
23 Opcode   4bit 帧类型,稍后细说
24 Mask     1bit 掩码,是否加密数据,默认必须置为1
25 Payload len  7bit 数据的长度
26 Masking-key      1 or 4 bit 掩码
27 Payload data     (x + y) bytes 数据
28 Extension data   x bytes  扩展数据
29 Application data y bytes  程序数据
复制代码

 

 在这里,首先我们需要理解的是1byte(1字节)=8bit(8位)=2位16进制数,在接下来代码中会涉及到。

另外数据的实际长度,会存在三种情况,这里先解释一下,在代码中也会有详细的解释。第一种情况当payload len的长度小于126时,payload len及时实际的数据的长度,第二种情况payload len的值等于126时,payload len其后2byte代表数据的真实长度,第三种情况,也就是等于127时,payload len其后8byte代表数据的真实长度。另外,masking-key其后会紧跟真实数据。如下图。

(二)什么是masking-key
WebSocket协议规范:为了避免迷惑网络中介(如代理服务器),以及涉及到安全问题,客户端必须mask所有送给服务器的frame。

为了避免面这种针对中间设备的攻击,以非HTTP标准的frame作为用户数据的前缀是没有说服力的,因为不太可能彻底发现并检测每个非标准的frame是否能够被非HTTP标准的中间设施识别并略过,也不清楚这些frame数据是否对中间设施的行为产生错误的影响。

对此,WebSocket的防御措施是mask所有从客户端发往服务器的数据,这样恶意脚本(攻击者)就没法获知网络链路上传输的数据是以何种形式呈现的,所以他没法构造可以被中间设施误解为HTTP请求的frame。

(三)pack与unpack

pack(format,args+) 函数把数据装入一个二进制字符串。

unpack(format,data) 函数从二进制字符串对数据进行解包。

format这里在其后可跟一个数值或者*,详见php手册,以下例子可以帮助你理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
    $string=pack('a6',"china");
    var_dump($string);
    echo ord($string[5])."\n";
    echo ord($string[4])."\n";
    $string=pack('a*',"china");
    var_dump($string);
    // echo ord($string[5])."\n";
    //echo bin2hex($string);
    echo ord($string[4])."\n";
    $string1=pack('A6',"china");
    var_dump($string1);
    echo ord($string1[5])."\n";
    echo ord($string1[4])."\n";
    echo substr("abcdefghi",4,4);
?>

(四)数据解包与包装

由于下篇引用需要,对其进行两个函数进行封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
<?php
   
//下面是sock类
  
class Sock{
  
  
  private $sda=array();  //已接收的数据
  
  private $slen=array(); //数据总长度
  
  private $sjen=array(); //接收数据的长度
  
  private $keys=array();  //加密key
  
  private $n=array();
  
     
  
  public function __construct($address, $port){
  
  }
  
  
  //解码函数,mask-key 为了避免迷惑网络中介(如代理服务器),以及涉及到安全问题,客户端必须mask所有送给服务器的frame。”
  //所以真实数据需要通过mask,但服务器的发送不需要mask key
  
  function uncode($str,$key){
  
    $mask = array();
  
    $data = '';
  
    $msg = unpack('H*',$str);//由于socket传输的数据都为二进制数据,进行数据解包,对应pack数据打包,用什么打包用什么解包,详细的用法会在接下来另写一个专题
  
    var_dump($msg);
  
    //一个字节为8位(1byte=8bit)(8个2进制位),由于unpack("H*",$str)将数据转换为16进制,一个16进制为4个2进制位
    //因此1字节为两个16进制位,接下来就好理解多了,8bit为两个16进制位
    /**
     * websocket数据收发协议
     * 具体每1bit(位)的意思
     * FIN      1bit 表示信息的最后一帧
     * RSV 1-3  1bit each 以后备用的 默认都为 0
     * Opcode   4bit 帧类型,稍后细说
     * Mask     1bit 掩码,是否加密数据,默认必须置为1
     * Payload len  7bit 数据的长度
     * Masking-key      1 or 4 bit 掩码
     * Payload data     (x + y) bytes 数据
     * Extension data   x bytes  扩展数据
     * Application data y bytes  程序数据
     */
    /**
     * Payload len占据七位用来描述消息长度,
     * 由于7位最多只能描述127所以这个值会代表三种情况,
     * 一种是消息内容少于126存储消息长度,此时payload就是实际数据的长度
     * 如果消息长度等于UINT16(8位无符号整型,1111111)的情况,此值为126,
     * 当消息长度大于UINT16的情况下,此值为127;
     * 这两种情况的消息长度存储到紧随后面的byte[],
     * 分别是UINT16(2位byte)和UINT64(8位byte)。
     * 其中127网上很多都说是4byte,其实8byte才是正确的
     *
     */
    //这里获取两位16进制,也就是8bit,也就是FIN(1bit)+RSV(1bit)*3+Opcode(4bit)=8bit=2个16进制位
     $head = substr($msg[1],0,2);
  
    print_r("msg:".$msg[1]."\n");
    print_r("msg[1]:".$head."\n");
  
    if ($head == '81' && !isset($this->slen[$key])) {
      //获得第二字节,也就是再后两位16进制,第二个字节包含掩码(1bit)+数据长度(7bit)=8bit,也是为2位16进制数
      $len=substr($msg[1],2,2);
      $len=hexdec($len);//把十六进制的转换为十进制
      //这里我们把‘fe’转化为二进制,也就是11111110,第一位为掩码值为1,也就是证明掩码加密
      //而其后的1111110,其实也就是126,此时我们就要看向上面介绍的
      if(substr($msg[1],2,2)=='fe'){
        //此时再获取其后的两位得到数据的16进制长度
        $len=substr($msg[1],4,4);
        $len=hexdec($len);//转化为10进制
        print_r("beforemsg:".$msg[1]."\n");
        //从数组的第5位字符开始截取新的字符串,即真实长度之后的字符串,由于紧跟其后的为4byte的umask和真实数据
        //方便之后统一获得umask,截取该字符串,往下看就知道
        $msg[1]=substr($msg[1],4);
        print_r("aftermsg:".$msg[1]."\n");
      }
      //接下来就是payload len为127的情况‘ff’二进制为11111111,与‘fe’同理的理解方式
      else if(substr($msg[1],2,2)=='ff'){
        //很显然,实际数据长度为16进制的8byte*2=16位
        //得到16进制的实际数据长度并转化为十进制
        $len=substr($msg[1],4,16);
        $len=hexdec($len);
        print_r("beforemsg:".$msg[1]."\n");
        //同理,从数组的第16位字符开始截取新的字符串,之前的为控制位,之后的为数据位的内容
        //由于以下设计从第四开始截取umask,所以不能从第20个字符截取,留四个
        $msg[1]=substr($msg[1],16);
        print_r("aftermsg:".$msg[1]."\n");
  
      }
      //这里获取4byte的umask,umask其后紧跟真实的数据
      //这里根据不同情况统一处理,可能没仔细看会有点乱,理解不了的可以评论告诉我
      //我另写一个不统一获取的
      $mask[] = hexdec(substr($msg[1],4,2));
  
      $mask[] = hexdec(substr($msg[1],6,2));
  
      $mask[] = hexdec(substr($msg[1],8,2));
  
      $mask[] = hexdec(substr($msg[1],10,2));
  
      $s = 12;//真实数据在$msg的起始位置
  
      $n=0;//初始n为0
  
    }
    //如果到这里就是判断是分片消息的处理了(不懂自己可以去了解下,涉及太多不讲解),需要综合上一个接收的数据处理,
    else if($this->slen[$key] > 0){
  
      $len=$this->slen[$key];
  
      $mask=$this->keys[$key];
  
      $n=$this->n[$key];
  
      $s = 0;
  
    }
  
       
    //这个也顺便说,每次自加2,所以最多到$msg[1]长度减2,强迫详细解释
    $e = strlen($msg[1])-2;
  
    for ($i=$s; $i<= $e; $i+= 2) {
      //从指定 ASCII 值返回字符
      //这里是将实际数据解码成对应的ASCII值,也就是实际你能看懂的消息,2个16进制位为一个字节,一个字节读取
      //根据获取的字节为第几个字节与4取余然后mask与字节的十进制数作异运算,得到真实数据hexdec将16进制转化为十进制
      $data .= chr($mask[$n%4]^hexdec(substr($msg[1],$i,2)));
  
      //echo $data."\n";
  
      $n++;
  
    }
    $dlen=strlen($data);//转化后数据的长度
  
    //假如通过消息分片,数据还不完整,需要更新上次的数据参数。
    if($len > 255 && $len > $dlen+intval($this->sjen[$key])){
  
      $this->keys[$key]=$mask;//mask掩码
  
      $this->slen[$key]=$len;//数据总长度
  
      $this->sjen[$key]=$dlen+intval($this->sjen[$key]);//接收数据长度
  
      $this->sda[$key]=$this->sda[$key].$data;//已接收数据
  
      $this->n[$key]=$n;//更新n
  
      //返回false,例如想把接收的数据发给谁,由于数据不完整,并不能发送,所以要跳过接收消息要处理的程序,等待数据完整再发送。
      return false;
  
    }
    //在这里就意味着消息已经完整
    else{
      //销毁、释放辅助变量
      unset($this->keys[$key],$this->slen[$key],$this->sjen[$key],$this->n[$key]);
      //取出完整的数据
      $data=$this->sda[$key].$data;
      //然后在释放辅助记录完整数据的变量。
      unset($this->sda[$key]);
      //返回完整的数据
      return $data;
  
    }
  
       
  
  }
  
     
  
  //与uncode相对,理解解码之后,code就会容易理解多了,
  
  function code($msg){
  
    $frame = array();
  
    //81开头固定
    $frame[0] = '81';
  
    $len = strlen($msg);
    //frame[1]构造数据长度信息
    //长度小于126时,就构造一个16进制作为字符串长度即payload len
    if($len < 126){
      //如果长度小于16,则需在其前面补充0
      $frame[1] = $len<16?'0'.dechex($len):dechex($len);
  
    }
    //长度在126<len<65025之间时,也就是解码的等于payload len=7e(126)
    else if($len < 65025){
  
      $s=dechex($len);
      //则构造payload len为‘7e’,也就是等于126是,同样根据其之前少于多少位用0补充,满足解码的占2byte(4位16进制数)
      $frame[1]='7e'.str_repeat('0',4-strlen($s)).$s;
    }
    //剩下的就为大于65025 也就是解码的等于payload len=7f(127)
    else{
  
      $s=dechex($len);
      //同理占位不足补充0
      $frame[1]='7f'.str_repeat('0',16-strlen($s)).$s;
  
    }
    //构造真实数据转16进制
    $frame[2] = $this->ord_hex($msg);
    //将frame数组连接成字符串
    $data = implode('',$frame);
    //pack()函数把数据装入一个二进制字符串,"H*"将数据按大端字节序的16进制格式包装。
    return pack("H*", $data);
  
  }
  
  function ord_hex($data) {
    $msg = '';
    $l = strlen($data);
    for ($i= 0; $i<$l; $i++) {
      $msg .= dechex(ord($data{$i}));
    }
    return $msg;
  }
     
}
  
?>

https://blog.csdn.net/Vae_sun/article/details/90347802

posted @   笠航  阅读(1536)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示