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