分享Workerman自定义协议解决粘包和拆包问题

 2024-01-21 02:04:49  阅读 0

自定义协议如何解决粘包拆包问题? 下面的文章将向您介绍如何自定义协议来解决粘包和解包问题。 希望对您有所帮助。

封装的代码_封包代码对应的是点_代码封版是什么意思

前言:

由于最近一直在使用服务器来实现网络游戏,虽然也可以通过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. 开箱测试

解包模拟只需将一个数据包分成两次发送,看服务器收到时能否显示,或者能否按照约定的格式正确显示。

标签: 接收 字符 字节

如本站内容信息有侵犯到您的权益请联系我们删除,谢谢!!


Copyright © 2020 All Rights Reserved 京ICP5741267-1号 统计代码