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
个请求,这可能会产生20G
条CPU
字符匹配指令。
所以我们一般不使用这种方法。
方法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