php使用WebSocket详细教程之对接收数据解包及发送数据包装(二)
接上篇介绍如何建立连接等基础了解,接下来介绍的是服务器接收到数据的转化,获得真实数据。
本篇需要理解的内容:
- WebSocket数据的收发协议?
- 什么是masking-key?
- php的两个函数pack()与unpack()?
- 理解数据包装与数据解包
(一)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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 开发者必知的日志记录最佳实践
· 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工具