Swoole下TCP粘包问题的产生及解决办法
TCP在数据传输中,如果数据过大则会影响传输速度,则需要拆包。若数据过小,当我们连续而又快速的写入数据,数据会先保存到套接字缓冲区中,网卡会将多次写入的数据一并发送到服务器,此时就会发生粘包现象。即多个数据混合在一起发送。无法区分。
在这个过程中可能会出现3种情况:
1 、正常:两个数据包逐一分开发送
2 、粘包:两个包一同发送,
3 、拆包:Server接收到不完整的或多出一部分的数据包
如下图展示(来自网络):
一:粘包现象展示
创建server.php
<?php // swoole_tcp_server.php //创建Server对象,监听 127.0.0.1:9501端口 $serv = new Swoole\Server("127.0.0.1", 9501); $serv->set([ //心跳检测,每三秒检测一次,10秒没活动就断开 'heartbeat_idle_time'=>6,//连接最大的空闲时间 'heartbeat_check_interval'=>3 //服务器定时检查 ]); //监听连接进入事件 $serv->on('Connect', function ($serv, $fd) { echo "Client ".$fd.": Connect.\n"; }); //监听数据接收事件 $serv->on('Receive', function ($serv, $fd, $from_id, $data) { // 接收客户端的想你想 echo "接收到".$fd."信息的".$data."\n"; $serv->send($fd, "Server: "); }); //监听连接关闭事件 $serv->on('Close', function ($serv, $fd) { echo "Client: ".$fd."Close.\n" ; }); echo "启动swoole tcp server 访问地址 127.0.0.1:9501 \n"; //启动服务器 $serv->start();
创建client.php
<?php /* * @Author: your name * @Date: 2021-03-24 10:56:50 * @LastEditTime: 2021-04-20 15:13:24 * @LastEditors: Please set LastEditors * @Description: In User Settings Edit * @FilePath: \www\swoole\tcp\client.php */ $client = new swoole_client(SWOOLE_SOCK_TCP); //连接到服务器 if (!$client->connect('127.0.0.1', 9501, 0.5)) { die("connect failed."); } //向服务器发送数据 for ($i=0; $i < 10; $i++) { sleep(1);//注意,这里我先暂停1秒发送 $client->send($i); } //从服务器接收数据 $data = $client->recv(); if (!$data) { die("recv failed."); } echo $data."\n"; //关闭连接 $client->close(); ?>
第一次包含暂停:结果展示正确:
第二次此时去掉暂停:再次测试:
第二次出现的结果明显不是我期望打印的结果,此时发送的问题 就叫粘包。
二:解决粘包问题
1,EOF结束符协议
即在每个数据包的结尾加上特殊的字符表示包已经结束。缺点:一定要确保该数据内不包含设定的字符。性能差。因为要遍历每个字节。
// 服务端代码加入配置 $serv->set([ 'open_eof_split' => true, 'package_eof' => "\r\n", ]); // 客户端代码加入配置 $client->set([ 'open_eof_split' => true, 'package_eof' => "\r\n", ]); //客户端数据发送修改 //向服务器发送数据 for ($i=0; $i < 10; $i++) { $client->send($i."\r\n"); }
测试结果:
1.1,换一种配置性能优于上面,但是只能解决分包问题,不能解决合包,具体设置如下
// 服务端配置 $serv-->set(array( 'open_eof_check' => true, //主要是这个参数变化 'package_eof' => "\r\n", )); // 客户端配置 $client->set(array( 'open_eof_check' => true, 'package_eof' => "\r\n", ));
2,固定包头+包体协议(建议采用)
采用php的pack和unpack函数计算出传输的字符串长度,通过substr获取数据。
// 服务端配置 $serv->set([ // 'open_eof_check' => true, // 'package_eof' => "\r\n", 'open_length_check' => true, 'package_max_length' => 81920, 'package_length_type' => 'n', //see php pack() 'package_length_offset' => 0, 'package_body_offset' => 2, ]); // 客户端发送数据调整 //向服务器发送数据 for ($i=0; $i < 10; $i++) { $client->send(pack("n", strlen($i)) . $i); }
测试结果:
此处类似于:参考pack,unpack函数解释。
// 发送数据 $message = 'hello world'; $sendMessage = pack("n", strlen($message)) . $message; $client->send($sendMessage); // 接受数据 $dataLength = unpack("n", $data)[1]; $message = substr($data, -$length);
参考链接:https://wiki.swoole.com/#/learn?id=tcp%e7%b2%98%e5%8c%85%e9%97%ae%e9%a2%98