swoole之粘包问题

swoole基础-swoole之粘包问题

什么是粘包问题,为什么我们要讲这个看起来比较奇怪的问题呢?

不着急解释,我们先看一个例子

创建一个server,server端代码如下

<?phpclassTcpBufferServer{private$_serv;/**

    * init

    */publicfunction__construct(){$this->_serv =newSwoole\Server("127.0.0.1",9501);$this->_serv->set(['worker_num'=>1,        ]);$this->_serv->on('Receive', [$this,'onReceive']);    }publicfunctiononReceive($serv, $fd, $fromId, $data){echo"Server received data:{$data}". PHP_EOL;    }/**

    * start server

    */publicfunctionstart(){$this->_serv->start();    }}$reload =newTcpBufferServer;$reload->start();

server的代码很简单,仅仅是在收到客户端代码后,标准输出一句话而已,client的代码需要注意了,我们写了一个for循环,连续向server send三条信息,代码如下

<?php$client =newswoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_SYNC);$client->connect('127.0.0.1',9501) ||exit("connect failed. Error:{$client->errCode}\n");// 向服务端发送数据for($i =0; $i <3; $i++) {    $client->send("Just a test.\n");}$client->close();

在未运行测试的情况下,我们期望server所在终端输出的结果应该是这样的

Serverreceiveddata:Justa test.Serverreceiveddata:Justa test.Serverreceiveddata:Justa test.

注意哦,我们期望的结果是server被回调了3次,才有上述期望的结果值

实际运行的结果呢,是否与我们所期望的一致?我们看下

上图左边是server输出的信息。

我们看到,左侧显示的结果是server一次性输出的结果,按理论来说,client发起了3次请求,server应该跟我们期望的结果一致,会执行3次呀,这怎么回事呢?

这个问题,便是我们今天要说的粘包问题。

为了说清楚这个问题,我们先来看下client/server之间数据传递的过程

客户端->发送数据

服务端->接收数据

通常我们直觉性的认为,客户端直接向网络中传输数据,对端从网络中读取数据,但是这是不正确的。

socket有缓冲区buffer的概念,每个TCP socket在内核中都有一个发送缓冲区和一个接收缓冲区。客户端send操作仅仅是把数据拷贝到buffer中,也就是说send完成了,数据并不代表已经发送到服务端了,之后才由TCP协议从buffer中发送到服务端。此时服务端的接收缓冲区被TCP缓存网络上来的数据,而后server才从buffer中读取数据。

所以,在onReceive中我们拿到的数据并没有办法保证数据包的完整性,swoole_server可能会同时收到多个请求包,也可能只收到一个请求包的一部分数据。

这就是一个大问题呀,如此TCP协议不行呀,这货虽然能保证我们能正确的接收到数据但是数据不对呀,这麻烦不容小觑。

既然是个问题,那我们自然也就有解决问题的方法,不然我下面说啥呢,对吧。

swoole给我们提供了两种解决方案

EOF结束协议

EOF,end of file,意思是我们在每一个数据包的结尾加一个eof标记,表示这就是一个完整的数据包,但是如果你的数据本身含有EOF标记,那就会造成收到的数据包不完整,所以开启EOF支持后,应避免数据中含有EOF标记。

在swoole_server中,我们可以配置open_eof_check为true,打开EOF检测,配置package_eof来指定EOF标记。

swoole_server收到一个数据包时,会检测数据包的结尾是否是我们设置的EOF标记,如果不是就会一直拼接数据包,直到超出buffer或者超时才会终止,一旦认定是一个完整的数据包,就会投递给Worker进程,这时候我们才可以在回调内处理数据。

这样server就能保证接收到一个完整的数据包了?不能保证,这样只能保证server能收到一个或者多个完整的数据包。

为啥是多个呢?

我们说了开启EOF检测,即open_eof_check设置为true,server只会检测数据包的末尾是否有EOF标记,如果向我们开篇的案例连发3个EOF的数据,server可能还是会一次性收到,这样我们只能在回调内对数据包进行拆分处理。

我们拿开篇的案例为例

server开启eof检测并指定eof标记是\r\n,代码如下(完整的代码都有上传到github,见文末)

$this->_serv->set(['worker_num'=>1,'open_eof_check'=>true,//打开EOF检测 'package_eof'=>"\r\n",//设置EOF ]);

客户端设置发送的数据末尾是\r\n符号,代码如下

for($i =0; $i <3; $i++) {    $client->send("Just a test.\r\n"); }

按照我们刚才的分析,server的效果可能会一次性收到多个完整的包,我们运行看看结果

因此我们还需要在onReceive回调内对收到的数据进行拆分处理

publicfunctiononReceive($serv, $fd, $fromId, $data){// echo "Server received data: {$data}" . PHP_EOL;$datas = explode("\r\n", $data);foreach($datasas$data)    {if(!$data)continue;echo"Server received data:{$data}". PHP_EOL;    }}

此时我们再看下运行结果

自行分包的效果便实现了,考虑到自行分包稍微麻烦,swoole提供了open_eof_split配置参数,启用该参数后,server会从左到右对数据进行逐字节对比,查找数据中的EOF标记进行分包,效果跟我们刚刚自行拆包是一样的,性能较差。

在案例的基础上我们看看open_eof_split配置

$this->_serv->set(['worker_num'=>1,'open_eof_check'=>true,//打开EOF检测'package_eof'=>"\r\n",//设置EOF'open_eof_split'=>true,]);

onReceive的回调,我们不需要自行拆包

publicfunctiononReceive($serv, $fd, $fromId, $data){echo"Server received data:{$data}". PHP_EOL;}

client的测试代码使用\r\n(同server端package_eof标记一致),我们看下运行效果

EOF标记解决粘包就说这么多,下面我们再看看另一种解决方案

固定包头+包体协议

下面我们要说的,对于部分同学可能有点难度,对于不理解的,建议多看多操作多问多查,不躲避不畏惧,这样才能有所提高。

固定包头是一种非常通用的协议,它的含义就是在你要发送的数据包的前面,添加一段信息,这段信息了包含了你要发送的数据包的长度,长度一般是2个或者4个字节的整数。

在这种协议下,我们的数据包的组成就是包头+包体。其中包头就是包体长度的二进制形式。比如我们本来想向服务端发送一段数据 "Just a test." 共12个字符,现在我们要发送的数据就应该是这样的

pack('N', strlen("Just a test.")) ."Just a test."

其中php的pack函数是把数据打包成二进制字符串。

为什么这样就能保证Worker进程收到的是一个完整的数据包呢?我来解释一下:

当server收到一个数据包(可能是多个完整的数据包)之后,会先解出包头指定的数据长度,然后按照这个长度取出后面的数据,如果一次性收到多个数据包,依次循环,如此就能保证Worker进程可以一次性收到一个完整的数据包。

估计好多人都看蒙了,这都是神马玩意?我们以案例来分析

server代码

<?phpclassServerPack{private$_serv;/**

    * init

    */publicfunction__construct(){$this->_serv =newSwoole\Server("127.0.0.1",9501);$this->_serv->set(['worker_num'=>1,'open_length_check'=>true,// 开启协议解析'package_length_type'=>'N',// 长度字段的类型'package_length_offset'=>0,//第几个字节是包长度的值'package_body_offset'=>4,//第几个字节开始计算长度'package_max_length'=>81920,//协议最大长度]);$this->_serv->on('Receive', [$this,'onReceive']);    }publicfunctiononReceive($serv, $fd, $fromId, $data){        $info = unpack('N', $data);        $len = $info[1];        $body = substr($data, - $len);echo"server received data:{$body}\n";    }/**

    * start server

    */publicfunctionstart(){$this->_serv->start();    }}$reload =newServerPack;$reload->start();

客户端的代码

<?php$client =newswoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_SYNC);$client->connect('127.0.0.1',9501) ||exit("connect failed. Error:{$client->errCode}\n");// 向服务端发送数据for($i =0; $i <3; $i++) {    $data ="Just a test.";    $data = pack('N', strlen($data)) . $data;    $client->send($data);}$client->close();

运行的结果

结果没错,是我们期望的结果。

我们来分析下这是为什么

1、首先,在server端我们配置了open_length_check,该参数表明我们要开启固定包头协议解析

2、package_length_type配置,表明包头长度的类型,这个类型跟客户端使用pack打包包头的类型一致,一般设置为N或者n,N表示4个字节,n表示2个字节

3、我们看下客户端的代码 pack('N', strlen($data)) . data,这句话就是包头+包体的意思,包头是pack函数打包的二进制数据,内容便是真实数据的长度strlen(data,这句话就是包头+包体的意思,包头是pack函数打包的二进制数据,内容便是真实数据的长度strlen(data)。

在内存中,整数一般占用4个字节,所以我们看到,在这段数据中0-4字节表示的是包头,剩余的就是真实的数据。但是server不知道呀,怎么告诉server这一事实呢?

看配置package_length_offset和package_body_offset,前者就是告诉server,从第几个字节开始是长度,后者就是从第几个字节开始计算长度。

4、既然如此,我们就可以在onReceive回调对数据解包,然后从包头中取出包体长度,再从接收到的数据中截取真正的包体。

$info =unpack('N', $data);$len = $info[1];$body =substr($data, - $len);echo"server received data: {$body}\n";

这便是swoole对于粘包问题的解决,你学会了吗?有任何问题下面留言哦。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,539评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,911评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,337评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,723评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,795评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,762评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,742评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,508评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,954评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,247评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,404评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,104评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,736评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,352评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,557评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,371评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,292评论 2 352

推荐阅读更多精彩内容