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手冊,以下例子可以幫助你理解。

<?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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM