国产+高潮+在线,国产 av 仑乱内谢,www国产亚洲精品久久,51国产偷自视频区视频,成人午夜精品网站在线观看

Workerman 自定義的協(xié)議如何解決粘包拆包

北橋蘇

前言

由于最近在使用 workerman 實現(xiàn) Unity3D 聯(lián)機游戲的服務(wù)端,雖然也可以通過 TCP 協(xié)議直接通信,但是在實際測試的過程中發(fā)現(xiàn)了一些小問題。

比如雙方的數(shù)據(jù)包都是字符串的方式嗎,還有就因為是字符串就需要切割,而有時候在客戶端或服務(wù)端接收時都會出現(xiàn)報錯。

經(jīng)過打印日志發(fā)現(xiàn),兩端接收到的包都有出現(xiàn)不是事先約定好的格式,這也就是 TCP 的粘包拆包現(xiàn)象。這個的解決方法很簡單,網(wǎng)上也有很多,但是這里是想用自己實現(xiàn)的協(xié)議解決,暫且放到后面來說。

問題解答

關(guān)于網(wǎng)游的通信數(shù)據(jù)包格式的約定,我在網(wǎng)上也看過一些。如果不是用弱類型語言做服務(wù)端腳本,其實別人常用的是字節(jié)數(shù)組。

但是 PHP 在接收到字節(jié)數(shù)組時,其實就是字符串,但前提時該字節(jié)數(shù)組沒有一些特定轉(zhuǎn)換的。就拿 C# 來說,在解決粘包等問題會在字節(jié)數(shù)組前加入字節(jié)長度 (BitConverter.GetBytes (len))。

但是這個傳遞到 PHP 服務(wù)端接收時,字符串前 4 個字節(jié)就是顯示不出來,用過很多方法進行轉(zhuǎn)換都取不出來。

后來也想過用 Protobuf 數(shù)據(jù)方式,雖然 PHP 可以對數(shù)據(jù)可以轉(zhuǎn)換,但是客戶端 C# 我還不太熟就放棄了。

還一個問題是,其實別人做網(wǎng)游服務(wù)端實現(xiàn)幀同步大部分都是 UDP 協(xié)議,同時也有 TCP 和 UDP 共用。

但是如果只是小型多人在線游戲,用 PHP 做服務(wù)端,TCP 協(xié)議通信也完全可以的。接下來就回到 workerman 的自定義協(xié)議和粘包拆包問題吧。

自定義協(xié)議

workerman 對 PHP 的幾個 socket 函數(shù)進行了封裝 (關(guān)于 socket 函數(shù),如果愿意折騰,php 也可以寫一個文件傳輸?shù)男」ぞ叩?,基于 TCP 之上也自帶了幾個應(yīng)用層協(xié)議,比如 Http, Websocket, Frame 等。

也預(yù)留了用戶自行定義協(xié)議的路口,只需要實現(xiàn)他的 ProtocolInterface 接口,以下就簡單介紹以下接口需要實現(xiàn)的幾個方法。

  1. Input 方法

在這個方法里,可以在服務(wù)端接收前對數(shù)據(jù)包進行解包,檢查包長度,過濾等。返回 0 就將數(shù)據(jù)包放入接收端的緩沖內(nèi)繼續(xù)等待,返回指定長度則表示取出緩沖區(qū)內(nèi)長度。如果異常也可以返回 false 直接關(guān)閉該客戶端連接。

  1. encode 方法

該方法是服務(wù)端在發(fā)送數(shù)據(jù)包到客戶端前,對數(shù)據(jù)包格式的處理,也就是封包,這個就要前后端約定好了。

  1. decode 方法

這個方法也就是解包,就是從緩沖區(qū)里取出指定長度到 onMessage 接收前要進行處理的地方,比如進行邏輯調(diào)配等等。

粘包拆包產(chǎn)生現(xiàn)象

由于 TCP 是基于流的,且因為是傳輸層,在上層的應(yīng)用通過 socket 套接字 (理解為接口) 通信時,他不知道傳遞過來的數(shù)據(jù)包開頭結(jié)尾在哪。

只是根據(jù) TCP 的一套擁塞算法機型粘合或拆解的發(fā)送。所以從字面上看,粘包就是幾個數(shù)據(jù)包一起發(fā)送,原本應(yīng)該是兩個包,客戶端只收到了一個包。而拆包是將一個數(shù)據(jù)包拆成了幾個包,本應(yīng)該是接收一個數(shù)據(jù)包,卻只收到了一個。

所以如果不解決這個,前面提到了按約定字符串傳輸,就可能解包時報錯的情況。

粘包拆包解決方法

  1. 首部加數(shù)據(jù)包長度

<?php
/**

  • This file is part of game.
  • Licensed under The MIT License
  • For full copyright and license information, please see the MIT-LICENSE.txt
  • Redistributions of files must retain the above copyright notice.
  • @author beiqiaosu
  • @link http://www.zerofc.cn
    */
    namespace Workerman\Protocols;

use Workerman\Connection\TcpConnection;

/**

  • Frame Protocol.
    */
    class Game
    {
    /**

    • Check the integrity of the package.
    • @param string $buffer
    • @param TcpConnection $connection
    • @return int
      */
      public static function input($buffer, TcpConnection $connection)
      {
      // 數(shù)據(jù)包前4個字節(jié)
      $bodyLen = intval(substr($buffer, 0 , 4));
      $totalLen = strlen($buffer);

      if ($totalLen < 4) {
      return 0;
      }

      if ($bodyLen <= 0) {
      return 0;
      }

      if ($bodyLen > strlen(substr($buffer, 4))) {
      return 0;
      }

      return $bodyLen + 4;
      }

    /**

    • Decode.
    • @param string $buffer
    • @return string
      */
      public static function decode($buffer)
      {
      return substr($buffer, 4);
      }

    /**

    • Encode.
    • @param string $buffer
    • @return string
      */
      public static function encode($buffer)
      {
      // 對數(shù)據(jù)包長度向左補零
      $bodyLen = strlen($buffer);
      $headerStr = str_pad($bodyLen, 4, 0, STR_PAD_LEFT);

      return $headerStr . $buffer;
      }
      }

      
      2. 特定字符分割
      ```php
      <?php

namespace Workerman\Protocols;

use Workerman\Connection\ConnectionInterface;

/**

  • Text Protocol.
    */
    class Tank
    {
    /**

    • Check the integrity of the package.
    • @param string $buffer
    • @param ConnectionInterface $connection
    • @return int
      */
      public static function input($buffer, ConnectionInterface $connection)
      {

      if (isset($connection->maxPackageSize) && \strlen($buffer) >= $connection->maxPackageSize) {
      $connection->close();
      return 0;
      }

      $pos = \strpos($buffer, "#");

      if ($pos === false) {
      return 0;
      }

      // 返回當前包長
      return $pos + 1;
      }

    /**

    • Encode.
    • @param string $buffer
    • @return string
      */
      public static function encode($buffer)
      {
      return $buffer . "#";
      }

    /**

    • Decode.
    • @param string $buffer
    • @return string
      */
      public static function decode($buffer)
      {
      return \rtrim($buffer, "#");
      }
      }

粘包拆包測試

這里就只演示特定字符串分割的解決方法,因為上面首頁 4 字節(jié)加包長的還是存在問題。就是第一次發(fā)送不帶包長,后面模擬粘包還是拆包都會停留在緩沖區(qū),下面演示可以參照上面代碼查看。

  1. 服務(wù)開啟和客戶端連接

截圖
截圖

  1. 服務(wù)業(yè)務(wù)端代碼

數(shù)據(jù)包格式說明一下,字符串以逗號分割,數(shù)據(jù)包以 #分割,逗號分割第一組是業(yè)務(wù)方法,如 Login 表示登陸傳遞,Pos 表示坐標傳遞,后面帶的就是對應(yīng)方法需要的參數(shù)了。

<?php

use Workerman\Worker;

require_once __DIR__ . '/vendor/autoload.php';

// #### create socket and listen 1234 port ####
$worker = new Worker('tank://0.0.0.0:1234');

// 4 processes
//$worker->count = 4;

$worker->onWorkerStart = function ($connection) {
    echo "游戲協(xié)議服務(wù)啟動……";
};

// Emitted when new connection come
$worker->onConnect = function ($connection) {
    echo "New Connection\n";
    $connection->send("address: " . $connection->getRemoteIp() . " " . $connection->getRemotePort());
};

// Emitted when data received
$worker->onMessage = function ($connection, $data) use ($worker, $stream) {

    echo "接收的數(shù)據(jù):" . $data . "\n";

    // 簡單實現(xiàn)接口分發(fā)
    $arr = explode(",", $data);

    if (!is_array($arr) || !count($arr)) {
        $connection->close("數(shù)據(jù)格式錯誤", true);
    }

    $func = strtoupper($arr[0]);
    $client = $connection->getRemoteAddress();

    switch($func) {
        case "LOGIN":
            $sendData = "Login1";
            break;
        case "POS":
            $positionX = $arr[1] ?? 0;
            $positionY = $arr[2] ?? 0;
            $positionZ = $arr[3] ?? 0;

            $sendData = "POS,$client,$positionX,$positionY,$positionZ";
            break;
    }

    $connection->send($sendData);
};

// Emitted when connection is closed
$worker->onClose = function ($connection) {
    echo "Connection closed\n";
};

// 接收緩沖區(qū)溢出回調(diào)
$worker->onBufferFull = function ($connection) {
    echo "清理緩沖區(qū)吧";
};

Worker::runAll();

?>
  1. 粘包測試

只需要在客戶端模擬兩個數(shù)據(jù)包連在一起,但是要以 #分隔,看看服務(wù)端接收的時候是一幾個包進行處理的。

截圖

  1. 拆包測試

拆包模擬只需要將一個數(shù)據(jù)包分成兩次發(fā)送,看看服務(wù)端接收的時候能不能顯示或者說能不能按約定好的格式正確顯示。

截圖
截圖

918 2 1
2個評論

JackDx

好文~~

  • 暫無評論
ak47f16200

mark

  • 暫無評論

北橋蘇

346
積分
0
獲贊數(shù)
0
粉絲數(shù)
2019-08-03 加入
??