首页 » PHP教程 » php沾包技巧_workerman 自定义的协议若何解决粘包拆包

php沾包技巧_workerman 自定义的协议若何解决粘包拆包

访客 2024-12-10 0

扫一扫用手机浏览

文章目录 [+]

比如双方的数据包都是字符串的办法吗,还有就由于是字符串就须要切割,而有时候在客户端或做事端吸收时都会涌现报错。
经由打印日志创造,两端吸收到的包都有涌现不是事先约定好的格式,这也便是 TCP 的粘包拆包征象。
这个的办理方法很大略,网上也有很多,但是这里是想用自己实现的协议办理,暂且放到后面来说。

问题解答:

关于网游的通信数据包格式的约定,我在网上也看过一些。
如果不是用弱类型措辞做做事端脚本,实在别人常用的是字节数组。
但是 PHP 在吸收到字节数组时,实在便是字符串,但条件时该字节数组没有一些特定转换的。
就拿 C# 来说,在办理粘包等问题会在字节数组前加入字节长度 (BitConverter.GetBytes (len))。
但是这个通报到 PHP 做事端吸收时,字符串前 4 个字节便是显示不出来,用过很多方法进行转换都取不出来。
后来也想过用 Protobuf 数据办法,虽然 PHP 可以对数据可以转换,但是客户端 C# 我还不太熟就放弃了。

php沾包技巧_workerman 自定义的协议若何解决粘包拆包

还一个问题是,实在别人做网游做事端实现帧同步大部分都是 UDP 协议,同时也有 TCP 和 UDP 共用。
但是如果只是小型多人在线游戏,用 PHP 做做事端,TCP 协议通信也完备可以的。
接下来就回到 workerman 的自定义协议和粘包拆包问题吧。

php沾包技巧_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. 特定字符分割

<?phpnamespace 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 表示坐标通报,后面带的便是对应方法须要的参数了。

<?phpuse 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. 拆包测试

拆包仿照只须要将一个数据包分成两次发送,看看做事端吸收的时候能不能显示或者说能不能按约定好的格式精确显示。

相关文章