TCP粘包问题

转自 http://www.cnblogs.com/kex1n/p/6502002.html

TCP粘包问题分析和解决(全)

TCP通讯粘包问题分析和解决(全)html

在socket网络程序中,TCP和UDP分别是面向链接和非面向链接的。所以TCP的socket编程,收发两端(客户端和服务器端)都要有成对的socket,所以,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将屡次间隔较小、数据量小的数据,合并成一个大的数据块,而后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。算法

对于UDP,不会使用块的合并优化算法,这样,实际上目前认为,是因为UDP支持的是一对多的模式,因此接收端的skbuff(套接字缓冲区)采用了链式结构来记录每个到达的UDP包,在每一个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来讲,就容易进行区分处理了。因此UDP不会出现粘包问题。编程

====================================================================缓存

在介绍TCP以前先普及下两个相关的概念,长链接和短链接。安全

1.长链接服务器

Client方与Server方先创建通信链接,链接创建后 不断开, 而后再进行报文发送和接收。网络

2.短链接异步

Client方与Server每进行一次报文收发交易时才进行通信链接,交易完毕后当即断开链接。此种方式经常使用于一点对多点通信,好比多个Client链接一个Server.socket

 

 

TCP协议简介tcp

TCP是一个面向链接的传输层协议,虽然TCP不属于ISO制定的协议集,但因为其在商业界和工业界的成功应用,它已成为事实上的网络标准,普遍应用于各类网络主机间的通讯。

做为一个面向链接的传输层协议,TCP的目标是为用户提供可靠的端到端链接,保证信息有序无误的传输。它除了提供基本的数据传输功能外,还为保证可靠性采用了数据编号、校验和计算、数据确认等一系列措施。它对传送的每一个数据字节都进行编号,并请求接收方回传确认信息(ACK)。发送方若是在规定的时间内没有收到数据确认,就重传该数据。

(1)     数据编号使接收方可以处理数据的失序和重复问题。

(2)     数据误码问题经过在每一个传输的数据段中增长校验和予以解决,接收方在接收到数据后检查校验和,若校验和有误,则丢弃该有误码的数据段,并要求发送方重传。

(3)     流量控制也是保证可靠性的一个重要措施,若无流控,可能会因接收缓冲区溢出而丢失大量数据,致使许多重传,形成网络拥塞恶性循环。

(4)     TCP采用可变窗口进行流量控制,由接收方控制发送方发送的数据量。

TCP为用户提供了高可靠性的网络传输服务,但可靠性保障措施也影响了传输效率。所以,在实际工程应用中,只有关键数据的传输才采用TCP,而普通数据的传输通常采用高效率的UDP。

 

保护消息边界和流

那么什么是保护消息边界和流呢?

保护消息边界,就是指传输协议把数据看成一条独立的消息在网上传输,接收端只能接收独立的消息。也就是说存在保护消息边界,接收端一次只能接收发送端发出的一个数据包。而面向流则是指无保护消息保护边界的,若是发送端连续发送数据,接收端有可能在一次接收动做中,会接收两个或者更多的数据包。

例如,咱们连续发送三个数据包,大小分别是2k,4k ,8k,这三个数据包,都已经到达了接收端的网络堆栈中,若是使用UDP协议,无论咱们使用多大的接收缓冲区去接收数据,咱们必须有三次接收动做,才可以把全部的数据包接收完.而使用TCP协议,咱们只要把接收的缓冲区大小设置在14k以上,咱们就可以一次把全部的数据包接收下来,只须要有一次接收动做。

 

注意:

这就是由于UDP协议的保护消息边界使得每个消息都是独立的。而流传输却把数据看成一串数据流,他不认为数据是一个一个的消息。因此有不少人在使用tcp协议通信的时候,并不清楚tcp是基于流的传输,当连续发送数据的时候,他们时常会认识tcp会丢包。其实否则,由于当他们使用的缓冲区足够大时,他们有可能会一次接收到两个甚至更多的数据包,而不少人每每会忽视这一点,只解析检查了第一个数据包,而已经接收的其余数据包却被忽略了。因此你们若是要做这类的网络编程的时候,必需要注意这一点。

 

结论:

(1)TCP为了保证可靠传输,尽可能减小额外开销(每次发包都要验证),所以采用了流式传输,面向流的传输,相对于面向消息的传输,能够减小发送包的数量,从而减小了额外开销。可是,对于数据传输频繁的程序来说,使用TCP可能会容易粘包。固然,对接收端的程序来说,若是机器负荷很重,也会在接收缓冲里粘包。这样,就须要接收端额外拆包,增长了工做量。所以,这个特别适合的是数据要求可靠传输,可是不须要太频繁传输的场合(两次操做间隔100ms,具体是由TCP等待发送间隔决定的,取决于内核中的socket的写法)

(2)UDP,因为面向的是消息传输,它把全部接收到的消息都挂接到缓冲区的接受队列中,所以,它对于数据的提取分离就更加方便,可是,它没有粘包机制,所以,当发送数据量较小的时候,就会发生数据包有效载荷较小的状况,也会增长屡次发送的系统发送开销(系统调用,写硬件等)和接收开销。所以,应该最好设置一个比较合适的数据包的包长,来进行UDP数据的发送。(UDP最大载荷为1472,所以最好能每次传输接近这个数的数据量,这特别适合于视频,音频等大块数据的发送,同时,经过减小握手来保证流媒体的实时性

====================================================================

粘包问题分析与对策

TCP粘包是指发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾。

出现粘包现象的缘由是多方面的,它既可能由发送方形成,也可能由接收方形成。

 

何时须要考虑粘包问题

1若是利用tcp每次发送数据,就与对方创建链接,而后双方发送完一段数据后,就关闭链接,这样就不会出现粘包问题(由于只有一种包结构,相似于http协议)。

关闭链接主要是要双方都发送close链接(参考tcp关闭协议)。如:A须要发送一段字符串给B,那么A与B创建链接,而后发送双方都默认好的协议字符如"hello give me sth abour yourself",而后B收到报文后,就将缓冲区数据接收,而后关闭链接,这样粘包问题不用考虑到,由于你们都知道是发送一段字符。

2若是发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包3若是双方创建链接,须要在链接后一段时间内发送不一样结构数据,如链接后,有好几种结构:

1)"hellogive me sth abour yourself"

2)"Don'tgive me sth abour yourself"

那这样的话,若是发送方连续发送这个两个包出去,接收方一次接收可能会是"hellogive me sth abour yourselfDon't give me sth abour yourself"这样接收方就傻了,究竟是要干吗?不知道,由于协议没有规定这么诡异的字符串,因此要处理把它分包,怎么分也须要双方组织一个比较好的包结构,因此通常可能会在头加一个数据长度之类的包,以确保接收。

 

粘包出现缘由

简单得说,在流传输中出现,UDP不会出现粘包,由于它有消息边界(参考Windows网络编程)

1发送端须要等缓冲区满才发送出去,形成粘包

2接收方不及时接收缓冲区的包,形成多个包接收

具体点:

(1)发送方引发的粘包是由TCP协议自己形成的,TCP为提升传输效率,发送方每每要收集到足够多的数据后才发送一包数据。若连续几回发送的数据都不多,一般TCP会根据优化算法把这些数据合成一包后一次发送出去,这样接收方就收到了粘包数据。

(2)接收方引发的粘包是因为接收方用户进程不及时接收数据,从而致使粘包现象。这是由于接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据还没有被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据以后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。

粘包状况有两种,一种是粘在一块儿的包都是完整的数据包,另外一种状况是粘在一块儿的包有不完整的包。

不是全部的粘包现象都须要处理,若传输的数据为不带结构的连续流数据(如文件传输),则没必要把粘连的包分开(简称分包)。但在实际工程应用中,传输的数据通常为带结构的数据,这时就须要作分包处理。

在处理定长结构数据的粘包问题时,分包算法比较简单;在处理不定长结构数据的粘包问题时,分包算法就比较复杂。特别是粘在一块儿的包有不完整的包的粘包状况,因为一包数据内容被分在了两个连续的接收包中,处理起来难度较大。实际工程应用中应尽可能避免出现粘包现象。

 

为了不粘包现象,可采起如下几种措施:

(1)对于发送方引发的粘包现象,用户可经过编程设置来避免,TCP提供了强制数据当即传送的操做指令push,TCP软件收到该操做指令后,就当即将本段数据发送出去,而没必要等待发送缓冲区满;

(2)对于接收方引发的粘包,则可经过优化程序设计、精简接收进程工做量、提升接收进程优先级等措施,使其及时接收数据,从而尽可能避免出现粘包现象;

(3)由接收方控制,将一包数据按结构字段,人为控制分屡次接收,而后合并,经过这种手段来避免粘包。

 

以上提到的三种措施,都有其不足之处。

(1)第一种编程设置方法虽然能够避免发送方引发的粘包,但它关闭了优化算法,下降了网络发送效率,影响应用程序的性能,通常不建议使用。

(2)第二种方法只能减小出现粘包的可能性,但并不能彻底避免粘包,当发送频率较高时,或因为网络突发可能使某个时间段数据包到达接收方较快,接收方仍是有可能来不及接收,从而致使粘包。

(3)第三种方法虽然避免了粘包,但应用程序的效率较低,对实时应用的场合不适合。

 

一种比较周全的对策是:接收方建立一预处理线程,对接收到的数据包进行预处理,将粘连的包分开。对这种方法咱们进行了实验,证实是高效可行的。

具体能够参考:http://blog.csdn.net/soli/article/details/1297109

 

TCP无保护消息边界的解决

针对这个问题,通常有3种解决方案:

(1)发送固定长度的消息

(2)把消息的尺寸与消息一块发送

(3)使用特殊标记来区分消息间隔

其解决方法具体解决能够参考:http://blog.csdn.net/zhangxinrun/article/details/6721427

 

====================================================================

网络通信的封包和拆包

对于基于TCP开发的通信程序,有个很重要的问题须要解决,就是封包和拆包。

 

为何基于TCP的通信程序须要进行封包和拆包

TCP是个"流"协议,所谓流,就是没有界限的一串数据,你们能够想一想河里的流水,是连成一片的,其间是没有分界线的。但通常通信程序开发是须要定义一个个相互独立的数据包的,好比用于登录的数据包,用于注销的数据包。因为TCP"流"的特性以及网络情况,在进行数据传输时会出现如下几种状况。

假设咱们连续调用两次send分别发送两段数据data1和data2,在接收端有如下几种接收状况(固然不止这几种状况,这里只列出了有表明性的状况).

A.先接收到data1,而后接收到data2.

B.先接收到data1的部分数据,而后接收到data1余下的部分以及data2的所有.

C.先接收到了data1的所有数据和data2的部分数据,而后接收到了data2的余下的数据.

D.一次性接收到了data1和data2的所有数据.

对于A这种状况正是咱们须要的,再也不作讨论.对于B,C,D的状况就是你们常常说的"粘包",就须要咱们把接收到的数据进行拆包,拆成一个个独立的数据包,为了拆包就必须在发送端进行封包。

另:对于UDP来讲就不存在拆包的问题,由于UDP是个"数据包"协议,也就是两段数据间是有界限的,在接收端要么接收不到数据要么就是接收一个完整的一段数据,不会少接收也不会多接收。

 

为何会出现B.C.D的状况

1.由Nagle算法形成的发送端的粘包:Nagle算法是一种改善网络传输效率的算法.简单的说,当咱们提交一段数据给TCP发送时,TCP并不马上发送此段数据,而是等待一小段时间,看看在等待期间是否还有要发送的数据,如有则会一次把这两段数据发送出去.这是对Nagle算法一个简单的解释,详细的请看相关书籍. C和D的状况就有多是Nagle算法形成的.

2.接收端接收不及时形成的接收端粘包:TCP会把接收到的数据存在本身的缓冲区中,而后通知应用层取数据.当应用层因为某些缘由不能及时的把TCP的数据取出来,就会形成TCP缓冲区中存放了几段数据.

 

怎样封包和拆包

最初遇到"粘包"的问题时,我是经过在两次send之间调用sleep来休眠一小段时间来解决。这个解决方法的缺点是显而易见的,使传输效率大大下降,并且也并不可靠。后来就是经过应答的方式来解决,尽管在大多数时候是可行的,可是不能解决B的那种状况,并且采用应答方式增长了通信量,加剧了网络负荷. 再后来就是对数据包进行封包和拆包的操做。

 

封包

封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部份内容了(之后讲过滤非法包时封包会加入"包尾"内容)。包头其实上是个大小固定的结构体,其中有个结构体成员变量表示包体的长度,这是个很重要的变量,其余的结构体成员可根据须要本身定义。根据包头长度固定以及包头中含有包体长度的变量就能正确的拆分出一个完整的数据包。

 

拆包

对于拆包目前我最经常使用的是如下两种方式:

(1)动态缓冲区暂存方式。之因此说缓冲区是动态的是由于当须要缓冲的数据长度超出缓冲区的长度时会增大缓冲区长度。

大概过程描述以下:

A,为每个链接动态分配一个缓冲区,同时把此缓冲区和SOCKET关联,经常使用的是经过结构体关联.

B,当接收到数据时首先把此段数据存放在缓冲区中.

C,判断缓存区中的数据长度是否够一个包头的长度,如不够,则不进行拆包操做.

D,根据包头数据解析出里面表明包体长度的变量.

E,判断缓存区中除包头外的数据长度是否够一个包体的长度,如不够,则不进行拆包操做.

F,取出整个数据包.这里的"取"的意思是不光从缓冲区中拷贝出数据包,并且要把此数据包从缓存区中删除掉.删除的办法就是把此包后面的数据移动到缓冲区的起始地址.

 

这种方法有两个缺点.

1) 为每一个链接动态分配一个缓冲区增大了内存的使用.

2) 有三个地方须要拷贝数据,一个地方是把数据存放在缓冲区,一个地方是把完整的数据包从缓冲区取出来,一个地方是把数据包从缓冲区中删除.第二种拆包的方法会解决和完善这些缺点.

前面提到过这种方法的缺点.下面给出一个改进办法, 即采用环形缓冲.可是这种改进方法仍是不能解决第一个缺点以及第一个数据拷贝,只能解决第三个地方的数据拷贝(这个地方是拷贝数据最多的地方).第2种拆包方式会解决这两个问题.

环形缓冲实现方案是定义两个指针,分别指向有效数据的头和尾.在存放数据和删除数据时只是进行头尾指针的移动.

 

(2)利用底层的缓冲区来进行拆包

因为TCP也维护了一个缓冲区,因此咱们彻底能够利用TCP的缓冲区来缓存咱们的数据,这样一来就不须要为每个链接分配一个缓冲区了。另外一方面咱们知道recv或者wsarecv都有一个参数,用来表示咱们要接收多长长度的数据。利用这两个条件咱们就能够对第一种方法进行优化。

对于阻塞SOCKET来讲,咱们能够利用一个循环来接收包头长度的数据,而后解析出表明包体长度的那个变量,再用一个循环来接收包体长度的数据。

编程实现见:http://blog.csdn.net/zhangxinrun/article/details/6721495


这个问题产生于编程中遇到的几个问题:

一、使用TCP的Socket发送数据的时候,会出现发送出错,WSAEWOULDBLOCK,在TCP中不是会保证发送的数据可以安全的到达接收端的吗?也有窗口机制去防止发送速度过快,为何还会出错呢?

二、TCP协议,在使用Socket发送数据的时候,每次发送一个包,接收端是完整的接受到一个包仍是怎么样?若是是每发一个包,就接受一个包,为何还会出现粘包问题,具体是怎么运行的?

三、关于Send,是否是只有在非阻塞状态下才会出现实际发送的比指定发送的小?在阻塞状态下会不会出现实际发送的比指定发送的小,就是说只能出现要么全发送,要么不发送?在非阻塞状态下,若是之发送了一些数据,要怎么处理,调用了Send函数后,发现返回值比指定的要小,具体要怎么作?

四、最后一个问题,就是TCP/IP协议和Socket是什么关系?是指具体的实现上,Socket是TCP/IP的实现?那么为何会出现使用TCP协议的Socket会发送出错。


这个问题第1个回答:

1应该是你的缓冲区不够大,

2 tcp是流,没有界限.也就没所谓的包.

3阻塞也会出现这种现象,出现后继续发送没发送出去的.

4tcp是协议,socket是一种接口,没必然联系.错误取决于你使用接口的问题,跟tcp不要紧.


这个问题第2个回答:

一、应该不是缓冲区大小问题,我试过设置缓冲区大小,不过这里有个问题,就是就算我把缓冲区设置成几G,也返回成功,不过实际上怎么可能设置那么大

三、出现没发送完的时候要手动发送吧,有没有具体的代码实现?

四、当选择TCP的Socket发送数据的时候,TCP中的窗口机制不是能防止发送速度过快的吗?为何Socket在出现了WSAEWOULDBLOCK后没有处理?


这个问题第3个回答:

1.在使用非阻塞模式的状况下,若是系统发送缓冲区已满,并示及时发送到对端,就会产生该错误,继续重试便可。

3.若是没有发完就继续发送后续部分便可。


这个问题第4个回答:

一、使用非阻塞模式时,若是当前操做不能当即完成则会返回失败,错误码是WSAEWOULDBLOCK,这是正常的,程序能够先执行其它任务,过一段时间后再重试该操做。

二、发送与接收不是一一对应的,TCP会把各次发送的数据从新组合,可能合并也可能拆分,但发送次序是不变的。

三、在各类状况下都要根据send的返回值来肯定发送了多少数据,没有发送完就再接着发。

四、socket是Windows提供网络编程接口,TCP/IP是网络传输协议,使用socket是可使用多种协议,其中包括TCP/IP。


这个问题第5个回答:

发送的过程是:发送到缓冲区和从缓冲区发送到网络上

WSAEWOULDBLOCK和粘包都是出如今发送到缓冲区这个过程的


Socket编程 (异步通信,解决Tcp粘包)

前面提到,TCP会出现粘包问题,下面将以实例演示解决方案:

问题通常会出现的状况以下,假设咱们连续发送两条两天记录("我是liger_zql"):

模拟发送示例:

 

#region 测试消息发送,并匹配协议

 TcpClient client =new TcpClient();

 client.AsynConnect();

 Console.WriteLine("下面将连续发送2条测试消息...");

 Console.ReadKey();

 MessageProtocol msgPro;

  for (int i = 0; i<2; i++)

  {

     msgPro =newMessageProtocol("我是liger_zql");

     Console.WriteLine("第{0}条:{1}", i +1,msgPro.MessageInfo.Content);

     client.AsynSend(msgPro);

  }

  #endregion

 

接收端接受两条信息会出现以下三种状况:

1.(1)我是liger_zql(2)我是liger_zql

2.(1)我是liger_zql我是(2)liger_zql

3.(1)我是liger_zql我是liger_zql

经过以上三种状况,显然二、3都不是咱们想要的结果。那么如何处理这中状况呢?

 

解决方案:经过自定义协议...

咱们能够以将信息以xml的格式发送出去,列入<protocol>content</protocol>经过正则匹配信息是否完整,若是不完整,咱们能够先将本次接受信息缓存接受下一次信息,再次匹配获得相应的结果。

(1)将信息对象转换成必定格式的xml字符串:

(2)对接收的信息经过正则进行匹配处理:

(3)将该定义的协议换换成信息对象,经过对象获取本身想要的信息。

结果:

最后运行结果以下

 

附上源码:SocketProQuests.zip

详细可参考:http://www.cnblogs.com/zengqinglei/archive/2013/05/14/3078842.html

 

 

附:

关于Socket/TCP粘包、多包和少包, 断包:http://tsing01.blog.163.com/blog/static/2059572832012716103711125/

 

关于Tcp封包粘包问题:http://www.cnblogs.com/jiangtong/archive/2012/03/22/2411985.html

TCP通信处理粘包详解http://www.cnblogs.com/smark/p/3284756.html

 

参考:

http://hi.baidu.com/chongerfeia/blog/item/b1e572f631dd7e28bd310965.html

http://blog.csdn.net/binghuazh/archive/2009/05/28/4222516.aspx

http://blog.csdn.net/zhangxinrun/article/details/6721495