Swoole 学习笔记 03 - 粘包问题的产生和解决办法

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

 

swoole 版本4.2.13

 

产生原因

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

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

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

 

粘包问题复现

客户端代码

<?php
//粘包示例 客户端

$client =  new Swoole\Client(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 Swoole\Server("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 Swoole\Server("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

 

posted @ 2020-08-03 12:00  波波波波波波  阅读(269)  评论(0编辑  收藏  举报