TCP通讯中的粘包问题 编程
尹德位 2015 西安 缓存
关键词 : TCP 网络通讯 粘包 Linux C/S 网络
一 粘包问题概述 并发
二 粘包回避设计 socket
第一章 粘包问题概述 性能
1.1 描述背景 大数据
采用TCP协议进行网络数据传送的软件设计中,广泛存在粘包问题。这主要是因为现代操做系统的网络传输机制所产生的。咱们知道,网络通讯采用的套接字(socket)技术,其实现实际是由系统内核提供一片连续缓存(流缓冲)来实现应用层程序与网卡接口之间的中转功能。多个数据包被连续存储于连续的缓存中,在对数据包进行读取时因为没法肯定发生方的发送边界,而采用某一估测值大小来进行数据读出,若双方的size不一致时就会使数据包的边界发生错位,致使读出错误的数据分包,进而曲解原始数据含义。 spa
1.2 粘包的概念 操作系统
粘包问题的本质就是数据读取边界错误所致,经过下图能够形象地理解其现象。 设计
如图1所示,当前的socket缓存中已经有6个数据分组到达,其大小如图中数字。而应用程序在对数据进行收取时(如图2),采用了300字节的要求去读取,则会误将pkg1和pkg2一块儿收走当作一个包来处理。而实际上,极可能pkg1是一个文本文件的内容,而pkg2则多是一个音频内容,这风马牛不相及的两个数据包却被揉进一个包进行处理,显然有失稳当。严重时可能由于丢了pkg2而致使软件陷入异常分支产生乌龙事件。
所以,粘包问题必须引发全部软件设计者(项目经理)的高度重视!
那么,或许会有读者发问,为什么不让接收程序按照100字节来读取呢?我想若是您了解一些TCP编程的话就不会有这样的问题。网络通讯程序中,数据包一般是不能肯定大小的,尤为在软件设计阶段没法真的作到肯定为一个固定值。好比聊天软件客户端若采用TCP传输一个用户名和密码到服务端进行验证登录,我想这个数据包不过是几十字节,至多几百字节便可发送完毕,而有时候要传输一个很大的视频文件,即便分包发送也应该一个包在几千字节吧。(听说,某国电信平台的MW中见到过一次发送1.5万字节的电话数据)这种状况下,发送数据的分包大小没法固定,接收端也就没法固定。因此通常采用一个较为合理的预估值进行轮询接收。(网卡的MTU都是1500字节,所以这个预估值通常为MTU的1~3倍)。
相信读者对粘包问题应该有了初步认识了。
第二章 粘包回避设计
2.0 闲扯
做者在此提出三种可解之法,这都是从软件设计的角度去考虑的,固然代码实现也是能够验证没问题的。下面一一为读者解开其谜底。
读者在别的文献中还能看到一种叫作【短链接】的方法,根据经验不建议采用此法,开销太大得不偿失。故而本文对该方案不作解释。
2.1 设计方案一:定长发送
在进行数据发送时采用固定长度的设计,也就是不管多大数据发送都分包为固定长度(为便于描述,此处定长为记为LEN),也就是发送端在发送数据时都以LEN为长度进行分包。这样接收方都以固定的LEN进行接收,如此一来发送和接收就能一一对应了。分包的时候不必定能完整的刚好分红多个完整的LEN的包,最后一个包通常都会小于LEN,这时候最后一个包能够在不足的部分填充空白字节。
固然,这种方法会有缺陷。1.最后一个包的不足长度被填充为空白部分,也即无效字节序。那么接收方可能难以辨别这无效的部分,它自己就是为了补位的,并没有实际含义。这就为接收端处理其含义带来了麻烦。固然也有解决办法,能够经过增添标志位的方法来弥补,即在每个数据包的最前面增长一个定长的报头,而后将该数据包的末尾标记一并发送。接收方根据这个标记确认无效字节序列,从而实现数据的完整接收。2.在发送包长度随机分布的状况下,会形成带宽浪费。好比发送长度可能为 1,100,1000,4000字节等等,则都须要按照定长最大值即4000来发送,数据包小于4000字节的其余包也会被填充至4000,形成网络负载的无效浪费。
综上,此方案适在发送数据包长度较为稳定(趋于某一固定值)的状况下有较好的效果。
2.2 设计方案二:尾部标记序列
在每一个要发送的数据包的尾部设置一个特殊的字节序列,此序列带有特殊含义,跟字符串的结束符标识”\0”同样的含义,用来标示这个数据包的末尾,接收方可对接收的数据进行分析,经过尾部序列确认数据包的边界。
这种方法的缺陷较为明显:1.接收方须要对数据进行分析,甄别尾部序列。2.尾部序列的肯定自己是一个问题。什么样的序列能够向”\0”同样来作一个结束符呢?这个序列必须是不具有一般任何人类或者程序可识别的带含义的数据序列,就像“\0”是一个无效字符串内容,于是能够做为字符串的结束标记。那普通的网络通讯中,这个序列是什么呢?我想一时间很难找到恰当的答案。
2.3 设计方案三:头部标记分步接收
这个方法是做者有限学识里最好的办法了。它既不损失效率,还完美解决了任何大小的数据包的边界问题。
这个方法的实现是这样的,定义一个用户报头,在报头中注明每次发送的数据包大小。接收方每次接收时先以报头的size进行数据读取,这必然只能读到一个报头的数据,从报头中获得该数据包的数据大小,而后再按照此大小进行再次读取,就能读到数据的内容了。这样一来,每一个数据包发送时都封装一个报头,而后接收方分两次接收一个包,第一次接收报头,根据报头大小第二次才接收数据内容。(此处的data[0]的本质是一个指针,指向数据的正文部分,也能够是一篇连续数据区的起始位置。所以能够设计成data[user_size],这样的话。)
下面经过一个图来展示设计思想。
由图看出,数据发送多了封装报头的动做;接收方将每一个包的接收拆分红了两次。
这方案看似精妙,实则也有缺陷:1.报头虽小,但每一个包都须要多封装sizeof(_data_head)的数据,积累效应也不可彻底忽略。2.接收方的接收动做分红了两次,也就是进行数据读取的操做被增长了一倍,而数据读取操做的recv或者read都是系统调用,这对内核而言的开销是一个不能彻底忽略的影响,对程序而言性能影响可忽略(系统调用的速度很是快)。
优势:避免了程序设计的复杂性,其有效性便于验证,对软件设计的稳定性要求来讲更容易达标。综上,方案三乃上上策!
致 热爱技术和努力的人们 ! 2015/11/29 凌晨
<<END>>