zoukankan      html  css  js  c++  java
  • Swoole 学习笔记 03

    粘包问题的产生和解决办法

    swoole 版本4.2.13

    产生原因

    TCP 是流式协议没有消息边界,客户端向服务器端发送一次数据,可能会被服务器端分成多次收到。客户端向服务器端发送多条数据。服务器端可能一次全部收到。

    发送方:发送方需要等缓冲区满才发送出去,造成粘包

    接收方:接收方不及时接收缓冲区的包,造成多个包接收

     

    粘包问题复现

    客户端代码

    <?php
    //粘包示例 客户端
    
    $client =  new SwooleClient(SWOOLE_SOCK_TCP,SWOOLE_SOCK_SYNC);
    
    $client->connect("127.0.0.1",9331);
    
    for ($i=1;$i<10;$i++){
        $client->send("123456".PHP_EOL);
    }
    
    echo $client->recv();
    
    $client->close();

    服务端代码

    <?php
    //粘包示例服务端
    
    $server = new SwooleServer("0.0.0.0",9331,SWOOLE_PROCESS,SWOOLE_SOCK_TCP);
    
    $server->set([
        'worker_num'=>4,
    ]);
    
    $server->on("connect",function (swoole_server $server,int $fd){
        echo "与客户端{$fd}已建立连接";
    });
    
    $server->on("receive",function (swoole_server $server,int $fd,$reactor_id,$data){
        $server->send($fd,"服务端已成功接收消息");
        echo "服务端收到客户端消息{$data}".PHP_EOL;
    });
    
    $server->on("close",function (swoole_server $server,int $fd){
        echo "Client {$fd} Close".PHP_EOL;
    });
    
    
    $server->start();

    启动客户端、启动服务端

    [root@localhost bky]# php nb_server.php
    与客户端1已建立连接服务端收到客户端消息123456
    123456
    123456
    123456
    123456
    123456
    123456
    123456
    123456
    
    Client 1 Close

    我们发现,分9次发送的消息,在服务端只接收到一次数据。

    解决办法

    方法1、EOF结束协议

    $server->set([
        'worker_num' => 4,    //worker process num
        'open_eof_check'=>true,//打开EOF检测
        'package_eof'=>PHP_EOL,//设置EOF
        'open_eof_split'=>true,//开启自动拆分
    ]);

    我们更改server端的代码,加入相关设置项,再次启动server和client

    [root@localhost bky]# php nb_server.php
    与客户端1已建立连接服务端收到客户端消息123456
    
    服务端收到客户端消息123456
    
    服务端收到客户端消息123456
    
    服务端收到客户端消息123456
    
    服务端收到客户端消息123456
    
    服务端收到客户端消息123456
    
    服务端收到客户端消息123456
    
    服务端收到客户端消息123456
    
    服务端收到客户端消息123456
    
    Client 1 Close

    可以发现粘包问题已解决

    同样的道理,如果一次请求的内容过多,也会拆分成多个数据包

    $client->send(str_repeat(123456,1024*100).PHP_EOL);

    如果服务端没有开启粘包处理,则效果如下

    # php nb_client.php
    服务端已成功接收消息服务端已成功接收消息[root@localhost bky]#

    发现数据包被拆分成了两份

    开启之后

    # php nb_client.php
    服务端已成功接收消息[root@localhost bky]#

    发现消息变成了一份

    这种方法会出现两个问题

    1、很难保证切分符不出现在数据体中

    2、EOF切割需要遍历整个数据包的内容,查找EOF,因此会消耗大量CPU资源。假设每个数据包为2M,每秒10000个请求,这可能会产生20GCPU字符匹配指令。

    所以我们一般不使用这种方法。

    方法2、固定包头和包体

    这种协议的特点是一个数据包总是由包头+包体2部分组成。包头由一个字段指定了包体或整个包的长度,长度一般是使用2字节/4字节整数来表示。服务器收到包头后,可以根据长度值来精确控制需要再接收多少数据就是完整的数据包。Swoole的配置可以很好的支持这种协议,可以灵活地设置4项参数应对所有情况。

    服务端代码

    <?php
    //tcp协议
    $server = new SwooleServer("0.0.0.0", 9330);
    $server->set(array(
        'worker_num'            => 4,    //worker process num
        //详情参考 https://wiki.swoole.com/wiki/page/287.html
        'open_length_check'     => true,//打开包长检测
        'package_length_type'   => 'N',//设置包头的类型
        'package_length_offset' => 0, //从第几个字节开始计算包长度(从0开始计数)
        'package_body_offset'   => 4,  //包体从第几个字节开始计算(即包头长度,与package_length_type相对应)
        'package_max_length'    => 1024 * 1024 * 2,//输入缓冲区大小
    ));
    
    //监听连接事件
    $server->on('connect', function ($server, $fd) {
        echo "监听到连接事件!fd==>{$fd}" . PHP_EOL;
    });
    //监听消息接收事件
    $server->on('receive', function (swoole_server $server, int $fd, int $reactor_id, $data) {
        echo "监听到消息接收事件" . PHP_EOL;
        //解包,并且截取数据包,截取的长度就是包头的长度
        $result = substr($data, 4);
        $server->send($fd, $result);
    });
    //监听关闭事件
    $server->on('close', function () {
        echo "监听到关闭事件" . PHP_EOL;
    });
    $server->start();

    客户端代码

    <?php
    $client = new swoole_client(SWOOLE_SOCK_TCP,SWOOLE_SYNC);
    $client->connect("127.0.0.1",9330) || exit("连接失败");
    $data = json_encode(str_repeat('bobobo', 1024 * 100));
    //打包  包头+包体
    $data = pack("N",strlen($data)).$data;
    $client->send($data);
    echo strlen($client->recv());
    $client->close();

    依次启动服务端和客户端

    服务端运行结果如下

    [root@localhost pack]# php server.php
    监听到连接事件!fd==>1
    监听到消息接收事件
    监听到关闭事件

    说明处理成功,只收到一次消息

    方法说明:

    客户端核心代码

    $data = pack("N",strlen($data)).$data;
    $client->send($data);

    pack打包数据,将数据包的长度转化为二进制数固定在头部,形成包头

    服务端核心代码

    $server->set(array(
        'worker_num'            => 4,    //worker process num
        //详情参考 https://wiki.swoole.com/wiki/page/287.html
        'open_length_check'     => true,//打开包长检测
        'package_length_type'   => 'N',//设置包头的类型
        'package_length_offset' => 0, //从第几个字节开始计算包长度(从0开始计数)
        'package_body_offset'   => 4,  //包体从第几个字节开始计算(即包头长度,与package_length_type相对应)
        'package_max_length'    => 1024 * 1024 * 2,//输入缓冲区大小
    ));

    .......
    //解包,并且截取数据包,截取的长度就是包头的长度
    $result = substr($data, 4);

    具体的每个配置项是和含义可以参考  

    https://wiki.swoole.com/wiki/page/287.html

    其中配置项package_length_type 要与客户端使用的打包函数 pack 的选项相对应。

    获取数据包内容时,需要去除包头,长度取 package_body_offset的值。

    其他配置项参考

    buffer_output_size

    socket_buffer_size

    https://wiki.swoole.com/wiki/page/313.html

  • 相关阅读:
    Pymsql
    MySQL基础操/下
    MySQL基础操作
    前端学习之jquery/下
    前端学习之jquery
    Python之异常处理
    Python之模块和包导入
    Python之模块
    Python之面向对象上下文管理协议
    Python之面向对象slots与迭代器协议
  • 原文地址:https://www.cnblogs.com/bobobobobo/p/13425529.html
Copyright © 2011-2022 走看看