workerman自訂的協定如何解決黏包拆包?以下這篇文章跟大家介紹一下workerman自訂協定解決黏包拆包問題的方法,希望對大家有幫助。
由於最近在使用workerman 實現Unity3D 線上遊戲的服務端,雖然也可以透過TCP 協定直接通信,但是在實際測試的過程中發現了一些小問題。 【相關推薦:《workerman教學》】
例如雙方的資料包都是字串的方式嗎,還有就因為是字串就需要切割,而有時在客戶端或服務端接收時都會出現報錯。經過列印日誌發現,兩端接收到的包都有出現不是事先約定好的格式,這也就是 TCP 的黏包拆包現象。這個的解決方法很簡單,網路上也有很多,但這裡是想用自己實作的協定解決,暫且放到後面來說。
關於網遊的通訊資料包格式的約定,我在網路上也看過一些。如果不是用弱型別語言做服務端腳本,其實別人常用的是位元組數組。但是 PHP 在接收到位元組數組時,其實就是字串,但前提時該位元組數組沒有一些特定轉換的。就拿 C# 來說,在解決黏包等問題會在位元組數組前加入位元組長度 (BitConverter.GetBytes (len))。但這個傳遞到 PHP 服務端接收時,字串前 4 個位元組就是顯示不出來,而且用過很多方法進行轉換都取不出來。後來我也想過用 Protobuf 資料方式,雖然 PHP 可以對資料可以轉換,但客戶端 C# 我還不太熟就放棄了。
另一個問題是,其實別人做網路服務端實作幀同步大部分都是 UDP 協議,同時也有 TCP 和 UDP 共用。但如果只是小型多人線上遊戲,用 PHP 做服務端,TCP 協定通訊也完全可以的。接下來就回到 workerman 的自訂協定和黏包拆包問題吧。
1. Input 方法
在這個方法裡,可以在服務端接收前對封包進行解包,檢查封包長度,過濾等。傳回 0 就將封包放入接收端的緩衝內繼續等待,傳回指定長度則表示取出緩衝區內長度。如果異常也可以回傳 false 直接關閉該客戶端連線。2. encode 方法
該方法是服務端在傳送封包到客戶端前,對封包格式的處理,也就是封包,這就要前後端約定好了。3. decode 方法
這個方法也就是解包,就是從緩衝區取出指定長度到onMessage 接收前要處理的地方,例如進行邏輯調配等等。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, "#"); } }
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自訂協定解決黏包拆包問題的方法的詳細內容。更多資訊請關注PHP中文網其他相關文章!