深入讲解swoole处理粘包问题

环境如下swoole 4.4.12PHP 7.3.5 (cli) (built: May 6 2019 11:38:17) ( NTS )一、粘包的概念官方解释:粘包,指TCP协议中,发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。
通俗解释:所谓粘包就是,一个数据在发送的时候跟上了另一个数据的信息,另一个数据的信息可能是完整的也可能是不完整的。
二、造成粘包的原因出现粘包现象的原因是多方面的,它既可能由发送方造成,也可能由接收方造成。
 发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一包数据。
若连续几次发送的数据都很少,通常TCP会根据优化算法把这些数据合成包后一次发送出去,这样接收方就收到了粘包数据;接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。
这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。
三、粘包的解决解决粘包的方法,就是分包。
所谓分包,是指在出现粘包的时候我们的接收方要进行分包处理。
在长连接中分包的时候, 数据包的边界如果发生错位, 导致读出错误的数据分包,进而曲解原始数据含义,这点需要特别注意。
 1️⃣特殊字符分割通过定义一个特殊的符号于package_eof,注意是客户端与服务端相互约定的,然后下一步就是客户端每次发送的时候都会在后面添加这个数据,服务端做数据的字符串处理(本质就是字符串切割的思路,在php中就是利用explode函数来处理)但是这种处理方式也存在问题,就是如果数据包中本身就带有package_eof,在做字符串切割处理的时候,会造成错误的分包,从而曲解原始数据的意思。
2️⃣固定包头+包体协议通过定义好发送消息的tcp数据包的格式规范,对于服务端和客户端相互之间同时遵守这个规范,这种规范就是在tcp的数据包中携带上数据的长度,接收端(可能是服务端也可能是客户端)就可以根据这个长度,对接收到的数据进行截取,从而实现分包的目的。
这里补充需要用到两个基础的函数:pack函数:https://php.golaravel.com/function.pack.htmlunpack函数:https://php.golaravel.com/function.unpack.html须知:一个字节 = 8个二进制位如果对上面对pack和unpack函数不能够很好的理解,先跳过,看下面的代码实现,可助于理解。
四、代码实现1️⃣传统方式实现代码tcp_server.php 服务端代码on('Connect', function ($serv, $fd) {    //echo "WorkerClient ".$fd.": Connect.\n";    $str = "我是服务端";    $len = pack('n', strlen($str));    $context = $len.$str;    for($i=0; $i<10; $i++){        $serv->send($fd, $context);    }});//监听数据接收事件$start = 0;$serv->on('Receive', function ($serv, $fd, $from_id, $data) use (&$start) {    // 接收客户端的信息(手动拆包)    for ($i=0; $i<10; $i++){        //因为这里,客户端/服务端的打包/解包的方式是'n',查看手册,        //是16位(占用2个字节),所以,截取0~2的字符串,再解包,就是客户端所发送数据的长度M了        $pack = unpack('n', substr($data, $start,2));        $len = $pack[1];//客户端所发送数据的长度M        $start = ($len + 2) * ($i+1);//维护$start,下一段数据包截取的起点        $context = substr($data, 2*($i+1)+$len*$i, $len);//从start~M截取数据包,获得客户端所发的完整真实的数据        echo '收到信息:' . $context . "\r\n";        sleep(1);//为了便于演示效果    }});//监听连接关闭事件$serv->on('Close', function ($serv, $fd) {    echo "WorkerClient: ".$fd."Close.\n" ;});echo "启动swoole tcp server 访问地址 127.0.0.1:9501 \n";//启动服务器$serv->start();tcp_sync_client.php 同步客户端代码connect('127.0.0.1', 9501, 0.5)) {    die("connect failed.");}//向服务器发送数据$context = '我是同步客户端';$len = pack('n', strlen($context));//在发送数据的头部拼接$context长度的二进制值。
为什么要二进制,因为只要确定了pack的打包方式,那么,这个二进制所占用的字节长度N是固定了,//这样子,只要保持客户端和服务端的打包/解包方式一致,那么服务端拿到数据,只要从0到N截取数据包,就能获得到客户端发送数据的长度M,//然后再从N开始截取长度为M的字符串,就是客户端发送的真实数据了$send = $len.$context;for ($i=0; $i<10; $i++){    $client->send($send);}if (!$client->send("hello world")) {    die("send failed.");}//从服务器接收数据$data = $client->recv();for ($i=0; $i<10; $i++){    //因为这里,客户端/服务端约定的打包/解包的方式是'n',查看手册,    //是16位(占用2个字节),所以,截取0~2的字符串,再解包,就是客户端所发送数据的长度M了    $pack = unpack('n', substr($data, $start,2));    $len = $pack[1];//客户端所发送数据的长度M    $start = ($len + 2) * ($i+1);//维护$start,下一段数据包截取的起点    $context = substr($data, 2*($i+1)+$len*$i, $len);//从start~M截取数据包,获得客户端所发的完整真实的数据    echo '收到信息:' . $context . "\r\n";    sleep(1);//为了便于演示效果}$client->close();tcp_async_client.php 异步客户端代码 on("connect", function(Client $cli) {    //$cli->send("GET / HTTP/1.1\r\n\r\n");    //向服务器发送数据    $context = '我是异步客户端';    $len = pack('n', strlen($context));    //在发送数据的头部拼接$context长度的二进制值。
为什么要二进制,因为只要确定了pack的打包方式,那么,这个二进制所占用的字节长度N是固定了,    //这样子,只要保持客户端和服务端的打包/解包方式一致,那么服务端拿到数据,只要从0到N截取数据包,就能获得到客户端发送数据的长度M,    //然后再从N开始截取长度为M的字符串,就是客户端发送的真实数据了    $send = $len.$context;    for ($i=0; $i<10; $i++){        $cli->send($send);    }});//接收服务端发送过来的数据$client->on("receive", function(Client $cli, $data){    for ($i=0; $i<10; $i++){        //因为这里,客户端/服务端约定的打包/解包的方式是'n',查看手册,        //是16位(占用2个字节),所以,截取0~2的字符串,再解包,就是客户端所发送数据的长度M了        $pack = unpack('n', substr($data, $start,2));        $len = $pack[1];//客户端所发送数据的长度M        $start = ($len + 2) * ($i+1);//维护$start,下一段数据包截取的起点        $context = substr($data, 2*($i+1)+$len*$i, $len);//从start~M截取数据包,获得客户端所发的完整真实的数据        echo '收到信息:' . $context . "\r\n";        sleep(1);//为了便于演示效果    }});$client->on("error", function(Client $cli){    echo "error\n";});$client->on("close", function(Client $cli){    echo "Connection close\n";});$client->connect('127.0.0.1', 9501);测试接着开两个命令行窗口,分别执行如下命令:[root@localhost swoole_04]#  php tcp_server.php[root@localhost swoole_04]#  php tcp_sync_client.php执行结果如下图或者开两个个命令行窗口,分别执行如下命令:[root@localhost swoole_04]#  php tcp_server.php[root@localhost swoole_04]#  php tcp_async_client.php执行结果如下图可以看出,不管是同步客户端还是异步客户端,都与服务端同样实现了粘包的处理 2️⃣swoole的方式处理粘包代码swoole_tcp_server.php 服务端代码set([    'open_length_check' => true,    'package_max_length' => 81920,    'package_length_type' => 'n',    'package_length_offset' => 0,    'package_body_offset' => 2,]);//监听连接进入事件$serv->on('Connect', function ($serv, $fd) {    //echo "WorkerClient ".$fd.": Connect.\n";    $str = "我是服务端";    $len = pack('n', strlen($str));    $context = $len.$str;    for($i=0; $i<10; $i++){        $serv->send($fd, $context);    }});//监听数据接收事件$start = 0;$serv->on('Receive', function ($serv, $fd, $from_id, $data) use (&$start) {    // 接收客户端的信息(手动拆包)    //开启了open_length_check,接收数据后直接打印出来    $info = $data."\r\n";    echo "收到消息:".$info;    sleep(1);//为了便于演示效果});//监听连接关闭事件$serv->on('Close', function ($serv, $fd) {    echo "WorkerClient: ".$fd."Close.\n" ;});echo "启动swoole tcp server 访问地址 127.0.0.1:9501 \n";//启动服务器$serv->start();swoole_tcp_sync_client.php 同步客户端代码set([    'open_length_check' => true,    'package_max_length' => 81920,    'package_length_type' => 'n',    'package_length_offset' => 0,    'package_body_offset' => 2,]);//连接到服务器if (!$client->connect('127.0.0.1', 9501, 0.5)) {    die("connect failed.");} //向服务器发送数据$context = '我是同步客户端';$len = pack('n', strlen($context));//在发送数据的头部拼接$context长度的二进制值。
为什么要二进制,因为只要确定了pack的打包方式,那么,这个二进制所占用的字节长度N是固定了,//这样子,只要保持客户端和服务端的打包/解包方式一致,那么服务端拿到数据,只要从0到N截取数据包,就能获得到客户端发送数据的长度M,//然后再从N开始截取长度为M的字符串,就是客户端发送的真实数据了$send = $len.$context;for ($i=0; $i<10; $i++){    $client->send($send);} if (!$client->send("hello world")) {    die("send failed.");} //从服务器接收数据//使用swoole的包长检测机制实现拆包for ($i=0; $i<10; $i++){    $data = $client->recv();    //开启了open_length_check,接收数据后直接打印出来    $info = $data."\r\n";    echo "收到消息:".$info;    sleep(1);//为了便于演示效果} $client->close();swoole_tcp_async_client.php 异步客户端代码 set([    'open_length_check' => true,    'package_max_length' => 81920,    'package_length_type' => 'n',    'package_length_offset' => 0,    'package_body_offset' => 2,]); $client->on("connect", function(Client $cli) {    //$cli->send("GET / HTTP/1.1\r\n\r\n");    //向服务器发送数据    $context = '我是异步客户端';    $len = pack('n', strlen($context));    //在发送数据的头部拼接$context长度的二进制值。
为什么要二进制,因为只要确定了pack的打包方式,那么,这个二进制所占用的字节长度N是固定了,    //这样子,只要保持客户端和服务端的打包/解包方式一致,那么服务端拿到数据,只要从0到N截取数据包,就能获得到客户端发送数据的长度M,    //然后再从N开始截取长度为M的字符串,就是客户端发送的真实数据了    $send = $len.$context;    for ($i=0; $i<10; $i++){        $cli->send($send);    }});//从服务端接收数据$client->on("receive", function(Client $cli, $data){    //开启了open_length_check,接收数据后直接打印出来    $info = $data."\r\n";    echo "收到消息:".$info;    sleep(1);//为了便于演示效果 });$client->on("error", function(Client $cli){    echo "error\n";});$client->on("close", function(Client $cli){    echo "Connection close\n";});$client->connect('127.0.0.1', 9501);测试接着开两个命令行窗口,分别执行如下命令:[root@localhost swoole_04]#  php swoole_tcp_server.php[root@localhost swoole_04]#  php swoole_tcp_sync_client.php执行结果如下图或者开两个命令行窗口,分别执行如下命令:[root@localhost swoole_04]#  php swoole_tcp_server.php[root@localhost swoole_04]#  php swoole_tcp_async_client.php执行结果如下图现在对pack、unpack函数应该更加能够理解了吧,喜欢记得点个赞~ 五、总结1.不管是原生的方式还是swoole的方式来实现粘包处理,原理都是一样的,都是在服务端与客户端共同约定同样对数据发送、接收规范(pack打包和unpack解包的方式),大家都遵循这套规范就行了;2.对于swoole的方式,在配置open_length_check等内容的时候,要注意:实例化完swoole的server或client(包括同步客户端和异步客户端)时,就配置好swoole的包长检测谁接收,谁来配置好swoole的包长检测。

返回列表
上一篇:
下一篇: