国产三级农村妇女在线,国产精品毛片a∨一区二区三区,国产乱子伦视频大全,国产精品色拉拉,国产欧美日韩一区二区三区,

首頁 > 技術(shù) > IOS開發(fā)

詳解Swoole TCP流數(shù)據(jù)邊界問題解決方案

IOS開發(fā) 2023-02-16 22:19:12
目錄
  • 1. 數(shù)據(jù)發(fā)送過程
  • 2. 什么是數(shù)據(jù)邊界
    • 2.1 代碼演示
  • 3.EOF 解決方案
    • 3.1 open_eof_check
    • 3.2 open_eof_split
    • 3.3 open_eof_check 和 open_eof_split 差異
  • 4. 固定包頭 + 包體解決方案
    • 5. 總結(jié)
      • 6. 擴展知識
        • 6.1 字節(jié)序

      1. 數(shù)據(jù)發(fā)送過程

      首先由客戶端將數(shù)據(jù)發(fā)往緩沖區(qū) (服務端并不是直接收到的), 對于客戶端來說,這次的數(shù)據(jù)即是發(fā)送成功了, 對于服務端是否真正的收到他是不知道的, 然后再由服務端從緩沖區(qū)中讀取數(shù)據(jù)。圖解:

      詳解Swoole TCP流數(shù)據(jù)邊界問題解決方案(圖1)

      2. 什么是數(shù)據(jù)邊界

      因為 TCP 是流式傳輸,對于服務端來說并不知道此時在緩沖區(qū)內(nèi)的數(shù)據(jù)是一次請求還是兩次請求的,所以在服務端接收數(shù)據(jù)時需要根據(jù)指定字符或約定長度來對數(shù)據(jù)進行分包,這個分包的標志即是數(shù)據(jù)邊界。否則可能會出現(xiàn)一次讀取兩條或多條數(shù)據(jù),造成讀取、解析數(shù)據(jù)出錯。

      詳解Swoole TCP流數(shù)據(jù)邊界問題解決方案(圖2)

      2.1 代碼演示

      可以用代碼實現(xiàn)一下,假設客戶端死循環(huán)往緩沖區(qū)不停輸入 “1”,即相當于每次的報文內(nèi)容都是 1, 那么在服務端讀取時收到的數(shù)據(jù)就是隨機長度的。

      客戶端代碼:

      $client = new SwooleClient(SWOOLE_SOCK_TCP);
      if ($client->connect('127.0.0.1', 9501, -1)) {
          while(true) {
              $client->send(1);        
          }
      }
      $client->close();

      服務端代碼:

      $server = new SwooleServer('127.0.0.1', 9501);
      $server->on('connect', function($server, $fd){
          echo "client : ".$fd." connect";
      });
      
      $server->on('receive', function($server, $fd, $from_id, $data){
          echo "receive:". $data.PHP_EOL;
      });
      
      $server->on('close', function($server){
      
      });

      運行結(jié)果

      詳解Swoole TCP流數(shù)據(jù)邊界問題解決方案(圖3)

      可以看到運行結(jié)果,服務端獲取到的數(shù)據(jù)完全是隨機的,有長有短,那么接下來我們說下如何解決這個問題。

      3.EOF 解決方案

      第一種解決方案類似于我們 http 請求頭的分隔符,在每次發(fā)送的數(shù)據(jù)包結(jié)尾處使用 rn (可以配置) 來結(jié)尾, 當服務端從緩沖區(qū)中讀取數(shù)據(jù), 根據(jù)指定字符來分割數(shù)據(jù)包,EOF 有兩種配置方案:

      3.1 open_eof_check

      首先放出配置方式:

      $server->set([
          'open_eof_check' => true,
          'package_eof' => "rn"
      ]);

      這種配置方式會對客戶端發(fā)來的數(shù)據(jù)包進行檢測, 當發(fā)現(xiàn)結(jié)尾是 rn 時,才會投遞給 worker 進程, 也就是我們的 onReceive 回調(diào),否則會一直拼接數(shù)據(jù)包,直到超出緩沖區(qū)或者超時才終止。 但此方法有一個問題是可能會一次性收到多個數(shù)據(jù)包,因為他是從數(shù)據(jù)包的結(jié)尾處來進行檢查的,在數(shù)據(jù)內(nèi)容中存在 rn 時程序并不會發(fā)現(xiàn),需要我們自己在應用代碼中再次使用 rn 來拆分數(shù)據(jù)包。

      客戶端運行代碼

      $client = new SwooleClient(SWOOLE_SOCK_TCP);
      
      if ($client->connect('127.0.0.1', 9501, -1)) {
      
          while(true) {
              $send2 = "Hello World rn";
              $client->send($send2);        
          }
      }
      
      $client->close();

      服務端代碼

      $server = new SwooleServer('127.0.0.1', 9501);
      $server->set([
          'open_eof_check' => true,
          'package_eof' => "rn"
      ]);
      
      $server->on('connect', function($server, $fd){
          echo "client : ".$fd." connect";
      });
      
      $server->on('receive', function($server, $fd, $from_id, $data){
          echo "receive:". $data;
      });
      
      $server->on('close', function($server){
      
      });
      
      $server->start();

      運行結(jié)果

      詳解Swoole TCP流數(shù)據(jù)邊界問題解決方案(圖4)

      3.2 open_eof_split

      配置方式:

      $server->set([
          'open_eof_split' => true,
          'package_eof' => "rn"
      ]);

      這種配置方式,服務端會對客戶端發(fā)來的數(shù)據(jù)逐個字符進行檢查,遇到 rn 就發(fā)送給 worker 進程,可以有效實現(xiàn)分包,但缺點是性能比較差。

      運行結(jié)果:可以看到每次接收到一個 Hello World(代碼我就不貼了, 只把服務端 set 配置改一下, 其他都一樣)

      詳解Swoole TCP流數(shù)據(jù)邊界問題解決方案(圖5)

      3.3 open_eof_check 和 open_eof_split 差異

      open_eof_check 只檢查接收數(shù)據(jù)的末尾是否為 EOF,因此它的性能最好,幾乎沒有消耗

      open_eof_check 無法解決多個數(shù)據(jù)包合并的問題,比如同時發(fā)送兩條帶有 EOF 的數(shù)據(jù),底層可能會一次全部返回

      open_eof_split 會從左到右對數(shù)據(jù)進行逐字節(jié)對比,查找數(shù)據(jù)中的 EOF 進行分包,性能較差。但是每次只會返回一個數(shù)據(jù)包

      4. 固定包頭 + 包體解決方案

      引用一段官方文檔的描述:

      包長檢測提供了固定包頭 + 包體這種格式協(xié)議的解析。啟用后,可以保證 Worker 進程 onReceive 每次都會收到一個完整的數(shù)據(jù)包。

      長度檢測協(xié)議,只需要計算一次長度,數(shù)據(jù)處理僅進行指針偏移,性能非常高,推薦使用。

      可見官方是推薦使用這種方式的,就是配置比其他方案要復雜一些, 首先貼一下配置:

      $server->set([
      // 打開包長檢測特性
      'package_length_check' => true,
      // 包頭中某個字段作為包長度的值,底層支持了 10 種長度類型??蓞⒖?pack() 方法
      'package_length_type' => 'N',
      // length 長度值在包頭的第幾個字節(jié)。
      'package_length_offset' => 8,
      // 從第幾個字節(jié)開始計算長度,一般有 2 種情況:
      //length 的值包含了整個包(包頭 + 包體),package_body_offset 為 0
      //包頭長度為 N 字節(jié),length 的值不包含包頭,僅包含包體,package_body_offset 設置為 N
      'package_body_offset' => 16,
      // 設置最大數(shù)據(jù)包尺寸,單位為字節(jié)
      'package_max_length' => 81920
      ]);

      下面是一個數(shù)據(jù)包結(jié)構(gòu)例子,可以很好的體現(xiàn)了字段含義。

      詳解Swoole TCP流數(shù)據(jù)邊界問題解決方案(圖6)

      以上通信協(xié)議的設計中,包頭長度為 4 個整型,16 字節(jié),length 長度值在第 3 個整型處。因此 package_length_offset 設置為 8,0-3 字節(jié)為 type,4-7 字節(jié)為 uid,8-11 字節(jié)為 length,12-15 字節(jié)為 serid。

      下面來說一下代碼實現(xiàn):

      客戶端代碼:

      $client = new SwooleClient(SWOOLE_SOCK_TCP);
      
      $data = "123456789012345678901234567890";
      $type = 0x30;
      $uid = 0x123;
      $length = strlen($data);
      $serid = 0x15;
      $head = pack("N4", $type, $uid, $length, $serid);
      $body = pack("a{$length}", $data);
      $message = $head.$body;
      
      
      if ($client->connect('127.0.0.1', 9502, -1)) {
          $client->send($message);
          echo $client->recv();
      }
      
      $client->close();

      服務端代碼:

      $serv = new SwooleServer('127.0.0.1', 9502);
      $serv->set([
          'open_length_check'     => true,
            'package_max_length'    => 81920,
            'package_length_type'   => 'N',
            'package_length_offset' => 8,
            'package_body_offset'   => 16,    
      ]);
      
      $serv->on('connect', function($server, $fd){
          echo $fd. " Connect !".PHP_EOL;
      });
      
      $serv->on('receive', function($server, $fd, $from_id, $data){
          var_dump($data);            // 源數(shù)據(jù)
          $tmp = unpack("Ntype/Nuid/Nlength", $data);
          $unpacking = unpack("Ntype/Nuid/Nlength/Nserid/a{$tmp['length']}body", $data);
          var_dump($unpacking);        // 解包后數(shù)據(jù)
          $server->send($fd, " Server Receive Data: ". $unpacking['body']);
      });
      
      
      $serv->on('close', function($server){
      
      });
      
      $serv->start();

      客戶端運行結(jié)果

      詳解Swoole TCP流數(shù)據(jù)邊界問題解決方案(圖7)

      服務端運行結(jié)果

      詳解Swoole TCP流數(shù)據(jù)邊界問題解決方案(圖8)

      可以看到 客戶端成功的把發(fā)送的數(shù)據(jù)回顯, 服務端也打印出了接收到的所有數(shù)據(jù), 其中有些字段在發(fā)送時是 16 進制的, 所以服務端在接收到之后需要進行進制轉(zhuǎn)換, 我這里沒有進行轉(zhuǎn)換, 所以顯示的數(shù)據(jù)是 10 進制的。

      5. 總結(jié)

      通過對比可以看出使用固定包頭 + 包體的方式是效率最高的一種, 因為他是按照固定長度去讀取的。期間專門去了解了 pack 函數(shù)的使用方法,但也不確定這么寫到底對不對,如果有其他了解的仁兄可以慷慨解答一下,網(wǎng)上相關資料有點少,官方文檔上也只給出了幾個字段的釋義。

      6. 擴展知識

      6.1 字節(jié)序

      計算機硬件有兩種儲存數(shù)據(jù)的方式:大端字節(jié)序(big endian)和小端字節(jié)序(little endian)。

      舉例來說,數(shù)值 0x2211 使用兩個字節(jié)儲存:高位字節(jié)是 0x22,低位字節(jié)是 0x11。

      • 大端字節(jié)序:高位字節(jié)在前,低位字節(jié)在后,這是人類讀寫數(shù)值的方法。
      • 小端字節(jié)序:低位字節(jié)在前,高位字節(jié)在后,即以 0x1122 形式儲存。

      這個前和后指的是內(nèi)存地址,計算機處理字節(jié)時是不知道高低字節(jié)之分的,它只知道按順序讀取字節(jié),先讀第一個字節(jié),再讀第二個字節(jié)。

      例如: 0x1234567 的讀取順序:

      詳解Swoole TCP流數(shù)據(jù)邊界問題解決方案(圖9)

      以上就是詳解Swoole TCP流數(shù)據(jù)邊界問題解決方案的詳細內(nèi)容,更多關于Swoole TCP流數(shù)據(jù)邊界問題解決方案的資料請關注腳本之家其它相關文章!

      TAg

      加載中~

      本網(wǎng)站LOGO受版權(quán)及商標保護,版權(quán)登記號:國作登字-2022-F-10126915,未經(jīng)湖南木星科技官方許可,嚴禁使用。
      Copyright ? 2012-2022 湖南木星科技有限公司(木星網(wǎng))版權(quán)所有
      轉(zhuǎn)載內(nèi)容版權(quán)歸作者及來源網(wǎng)站所有,本站原創(chuàng)內(nèi)容轉(zhuǎn)載請注明來源,商業(yè)媒體及紙媒請先聯(lián)系:aishangyiwan@126.com

      工信部備案號:湘ICP備19012813號-5