接上篇介紹如何建立連接等基礎了解,接下來介紹的是服務器接收到數據的轉化,獲得真實數據。
本篇需要理解的內容:
- 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