问题的表述
问题的背景是这样的:有一个系统,那有后台服务器,也有移动端的客户端。当客户端上线时,服务器会将指定的数据库的数据发送给客户端,客户端解析后呈现。
但是,有一天客户端开发跟我反映一个奇怪的问题:之前客户端一上线就收到服务器发来的数据,现在客户端收到了数据,但是解析出问题了。但是前后端的代码都没有改啊,只是要发送的数据是新添加的,并且比之前的测试数据要大。
原因的剖析
根据他的描述,很明显问题出在了要发送的数据上。由于之前是测试数据,并不会放过大的数据到后台数据库,现在放上去的是真实数据,要比测试数据大好多。
于是我就重新拾起了尘封已久的tcp/ip协议的书,也试着在网上寻找答案。最终我知道了我们这个问题叫做tcp协议的组包问题。
什么叫tcp的组包问题呢?简单的说就是tcp协议把过大的数据包分成了几个小的包传输,客户端要把同一组的数据包重新组合成一个完整的数据包。
具体点的解释:
首先我们要知道MTU(最大传输单元)。IP分片在以太网上,由于电气限制,一帧不能超过1518字节,除去以太网帧头14字节(mac地址等)和帧尾4字节校验,不考虑PPPoE协议(读者自行了解这是什么鬼)的8个字节,还剩1500字节,这个大小称为MTU(最大传输单元)。
这1500还包含20字节IP头,8字节UDP头或者20字节TCP头。所以真正的一次发送的数据包,UDP为1500-28=1472字节,TCP为1500-40=1460字节。
当然我们要知道tcp是可靠传输协议,以字节流的形式传输数据的,什么意思呢?就是他可以允许超过1452字节的数据进行传输,因为他会把它切分成多个包,然后用序号进行排序,从而表明了这几个包是同一组数据。
但是UDP是不可靠传输,它一次性就只能发那么多了,超过的他就不允许发了,所以他效率高,发送一次就是一个完整的数据,接收方直接拿去解析就可以了。(UDP和TCP还有很多不同的优缺点,读者感兴趣可以自行了解)
所以了解了这些之后对于上面描述的问题现象也就不难解释了,由于我们后台服务器发送的数据包过大,比如有5000字节,那么tcp协议就会把它分成1460+1460+1460+620字节,分四个包发送给客户端,客户端单单解析一个数据包得到的数据肯定是不完整的,要是传输的数据是带一定的结构的话就会解析出错了。
发送端处理方法
那么知道了产生这个问题的原因,又该怎么解决这个问题呢。我在一个网站上看到有如下回答:链接
一 可以每次发送同样大小的包,过大的包不予发送,过小的包,后面部分用固定的字符’\0’进行填充。
二 将流按字符处理,抽出一个字符做转义字符(通常Java用’\’来做转义字符,比如”\n”表示换行)。
三 在发送方发送一个包的时候,先将这个包的长度发送给对方(一般是4个字节表示包长),然后再将包的内容发送过去.接收方先接收4个字节,看看包的长度,然后按照长度来接收包。
于是我采用了最后一种解决办法,服务器其发送的数据中,前4个字节为需要发送的数据的字节数,如上所说,这样的做法其实就是在TCP协议之上又在封装了一层,只不过很简单明了。
接收端处理方法
因为TCP会对收到的报文段进行重新排序,然后再交给应用层处理,所以可以用以下的逻辑作为接收端的处理方法。
算法(装逼专用词):
1.设置全局标量flag最为标识位,初始值为0;设置全局变量L,初始值为0。
2.取缓存区数据包,判断标识位,若为0,进入步骤3;若为1,传递参数L,进入步骤4。
3.解析数据前4个字节,得到字节数L后与当前数据块剩下的字节数M比较。若相等,执行步骤5。若L>M,则设置标识位为1,暂时存储这M字节数据,传参数L=L-M,执行步骤2。
4.比较L与当前数据包的大小M,若相等,执行步骤5;若L>M,暂时存储着M字节数据,传参数L=L-M,执行步骤2。
5.调用解析函数正常解析,设置标识位为0。
流程图如下: