workerman自定義的協議如何解決粘包拆包?下面本篇文章給大家介紹一下workerman自定義協議解決粘包拆包問題的方法,希望對大家有所幫助。
前言:
? ? ? ? 由于最近在使用 workerman 實現 Unity3D 聯機游戲的服務端,雖然也可以通過 TCP 協議直接通信,但是在實際測試的過程中發現了一些小問題。【相關推薦:《workerman》】
? ? ? ? 比如雙方的數據包都是字符串的方式嗎,還有就因為是字符串就需要切割,而有時候在客戶端或服務端接收時都會出現報錯。經過打印日志發現,兩端接收到的包都有出現不是事先約定好的格式,這也就是 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 WorkermanProtocols; use WorkermanConnectionTcpConnection; /** * 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 WorkermanProtocols; use WorkermanConnectionConnectionInterface; /** * 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 WorkermanWorker; 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?Connectionn"; ????$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?closedn"; }; //?接收緩沖區溢出回調 $worker->onBufferFull?=?function?($connection)?{ ????echo?"清理緩沖區吧"; }; Worker::runAll(); ?>
3. 粘包測試
? ? 只需要在客戶端模擬兩個數據包連在一起,但是要以 #分隔,看看服務端接收的時候是一幾個包進行處理的。
4. 拆包測試
? ? ? ? 拆包模擬只需要將一個數據包分成兩次發送,看看服務端接收的時候能不能顯示或者說能不能按約定好的格式正確顯示。
更多編程相關知識,請訪問:workerman!!