php使用WebSocket详细教程之对接收数据解包及发送数据包装
Posted 笠航
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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手册,以下例子可以帮助你理解。
<?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); ?>
(四)数据解包与包装
由于下篇引用需要,对其进行两个函数进行封装。
<?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
以上是关于php使用WebSocket详细教程之对接收数据解包及发送数据包装的主要内容,如果未能解决你的问题,请参考以下文章