自定义协议如何解决粘包拆包问题? 下面的文章将向您介绍如何自定义协议来解决粘包和解包问题。 希望对您有所帮助。
前言:
由于最近一直在使用服务器来实现网络游戏,虽然也可以通过TCP协议实现直接通信,但是在实际测试过程中发现了一些小问题。 【相关推荐:《教程》】
比如两边的数据包都是字符串的形式吗? 另外,因为它们是绳子,所以需要剪断。 有时,客户端或服务器接收时会出现错误。 打印日志后发现两端收到的数据包格式不是事先约定好的。 这就是TCP数据包粘包和拆包的现象。 这个问题的解决办法很简单,网上也有很多,不过这里我想用我实现的协议来解决,留到以后再说。
问题与解答:
我也在网上看到了一些关于网络游戏通信数据包格式的约定。 如果您不使用弱类型语言来执行服务器端脚本,其他人通常会使用字节数组。 但是当PHP接收到字节数组时,它实际上是一个字符串,但前提是字节数组没有进行一些特定的转换。 以 C# 为例。 在解决粘包等问题时,会在字节数组之前添加字节长度(.(len))。 但是当这个传给PHP服务器接收时,字符串的前4个字节无法显示,而且经过多种方法转换也无法检索出来。 后来我也想过用数据的方式。 虽然PHP可以转换数据,但我因为对客户端C#不熟悉而放弃了。
还有一个问题是,其实别人在网络游戏服务器中使用的帧同步大部分都是使用UDP协议,TCP和UDP也是共享的。 但如果只是小型多人网络游戏,完全可以使用PHP作为服务器,TCP协议通信。 接下来,让我们回到自定义协议和粘包解包问题。
自定义协议:
封装了PHP的几个功能(关于功能,如果你愿意乱搞的话,PHP也可以写一个文件传输的小工具),而且还自带了几个基于TCP的应用层协议,比如Http、Frame、 ETC。 。 它还为用户定义自己的协议预留了交集。 他们只需要实现它的接口。 下面简单介绍一下以下接口需要实现的几个方法。
1. 输入法
在该方法中,数据包在被服务器接收之前可以进行解包、检查、过滤等操作。 返回0表示将数据包放入接收端的缓冲区中并继续等待。 返回指定长度意味着取出缓冲区中的长度。 如果出现异常,也可以返回false直接关闭客户端连接。
2. 方法
该方法是服务器在向客户端发送数据包之前对数据包格式的处理,即数据包封装。 这个必须前后端都同意。
3. 方法
这种方法也叫拆包,就是从缓冲区中取出指定的长度到接收前需要处理的地方,比如逻辑部署等。
粘包拆包造成的现象:
因为TCP是基于流的,并且因为是传输层,所以当上层应用程序通过(理解为接口)进行通信时,它并不知道所传递的数据包的开头和结尾在哪里。 只需根据 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. 开箱测试
解包模拟只需将一个数据包分成两次发送,看服务器收到时能否显示,或者能否按照约定的格式正确显示。