分享workerman自定義協定解決粘包拆包問題的方法

2022-12-12 22:00:51
自定義的協定如何解決粘包拆包?下面本篇文章給大家介紹一下workerman自定義協定解決粘包拆包問題的方法,希望對大家有所幫助。

php入門到就業線上直播課:進入學習
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API偵錯工具:

前言:

由於最近在使用 workerman 實現 Unity3D 聯機遊戲的伺服器端,雖然也可以通過 TCP 協定直接通訊,但是在實際測試的過程中發現了一些小問題。【相關推薦:《》】

比如雙方的封包都是字串的方式嗎,還有就因為是字串就需要切割,而有時候在使用者端或伺服器端接收時都會出現報錯。經過列印紀錄檔發現,兩端接收到的包都有出現不是事先約定好的格式,這也就是 TCP 的粘包拆包現象。這個的解決方法很簡單,網上也有很多,但是這裡是想用自己實現的協定解決,暫且放到後面來說。

問題解答:

關於網遊的通訊封包格式的約定,我在網上也看過一些。如果不是用弱型別語言做伺服器端指令碼,其實別人常用的是位元組陣列。但是 PHP 在接收到位元組陣列時,其實就是字串,但前提時該位元組陣列沒有一些特定轉換的。就拿 C# 來說,在解決粘包等問題會在位元組陣列前加入位元組長度 (BitConverter.GetBytes (len))。但是這個傳遞到 PHP 伺服器端接收時,字串前 4 個位元組就是顯示不出來,用過很多方法進行轉換都取不出來。 後來也想過用 Protobuf 資料方式,雖然 PHP 可以對資料可以轉換,但是使用者端 C# 我還不太熟就放棄了。

還一個問題是,其實別人做網遊伺服器端實現影格同步化大部分都是 UDP 協定,同時也有 TCP 和 UDP 共用。但是如果只是小型多人線上遊戲,用 PHP 做伺服器端,TCP 協定通訊也完全可以的。接下來就回到 workerman 的自定義協定和粘包拆包問題吧。

自定義協定:

workerman 對 PHP 的幾個 socket 函數進行了封裝 (關於 socket 函數,如果願意折騰,php 也可以寫一個檔案傳輸的小工具的),基於 TCP 之上也自帶了幾個應用層協定,比如 Http, Websocket, Frame 等。也預留了使用者自行定義協定的路口,只需要實現他的 ProtocolInterface 介面,以下就簡單介紹以下介面需要實現的幾個方法。

1. Input 方法

在這個方法裡,可以在伺服器端接收前對封包進行解包,檢查包長度,過濾等。返回 0 就將封包放入接收端的緩衝內繼續等待,返回指定長度則表示取出緩衝區內長度。如果異常也可以返回 false 直接關閉該使用者端連線。

2. encode 方法

該方法是伺服器端在傳送封包到使用者端前,對封包格式的處理,也就是封包,這個就要前後端約定好了。

3. decode 方法

這個方法也就是解包,就是從緩衝區裡取出指定長度到 onMessage 接收前要進行處理的地方,比如進行邏輯調配等等。

粘包拆包產生現象:

由於 TCP 是基於流的,且因為是傳輸層,在上層的應用通過 socket 通訊端 (理解為介面) 通訊時,他不知道傳遞過來的封包開頭結尾在哪。只是根據 TCP 的一套擁塞演演算法機型粘合或拆解的傳送。所以從字面上看,粘包就是幾個封包一起傳送,原本應該是兩個包,使用者端只收到了一個包。而拆包是將一個封包拆成了幾個包,本應該是接收一個封包,卻只收到了一個。所以如果不解決這個,前面提到了按約定字串傳輸,就可能解包時報錯的情況。

粘包拆包解決方法:

1. 首部加封包長度

<?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)
    {
        // 封包前4個位元組
        $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)
    {
        // 對封包長度向左補零
        $bodyLen = strlen($buffer);
        $headerStr = str_pad($bodyLen, 4, 0, STR_PAD_LEFT);

        return $headerStr . $buffer;
    }
}
登入後複製

2. 特定字元分割

<?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 位元組加包長的還是存在問題。就是第一次傳送不帶包長,後面模擬粘包還是拆包都會停留在緩衝區,下面演示可以參照上面程式碼檢視。

1. 服務開啟和使用者端連線

2. 服務業務端程式碼

封包格式說明一下,字串以逗號分割,封包以 #分割,逗號分割第一組是業務方法,如 Login 表示登陸傳遞,Pos 表示座標傳遞,後面帶的就是對應方法需要的引數了。

<?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 "遊戲協定服務啟動……";
};

// 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 "接收的資料:" . $data . "\n";

    // 簡單實現介面分發
    $arr = explode(",", $data);

    if (!is_array($arr) || !count($arr)) {
        $connection->close("資料格式錯誤", 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";
};


// 接收緩衝區溢位回撥
$worker->onBufferFull = function ($connection) {
    echo "清理緩衝區吧";
};

Worker::runAll();

?>
登入後複製

3. 粘包測試

只需要在使用者端模擬兩個封包連在一起,但是要以 #分隔,看看伺服器端接收的時候是一幾個包進行處理的。

4. 拆包測試

拆包模擬只需要將一個封包分成兩次傳送,看看伺服器端接收的時候能不能顯示或者說能不能按約定好的格式正確顯示。

更多程式設計相關知識,請存取:!!

以上就是分享workerman自定義協定解決粘包拆包問題的方法的詳細內容,更多請關注TW511.COM其它相關文章!