网络粘包及Nagle算法(粘包,断包)

今天学到一个很有趣的算法,而且对windows和linux网络编程有了一个新的认识。

首先考虑一个问题,如果我们的应用是发送一些小字节的数据给服务器,而且数据量很大,小字节,小到1个字节,这个时候,我们需要IP层20个字节,TCP20个字节(或者UDP8个字节),这样导致的一个很严重的问题,网络非常低效。如果你是网络协议设计者,你应该怎么做?

一个自然而言的想法就是将数据拼接在一起,多次变为一次发多个包。这就是有名的Nagle算法(Nagle algorithm)。(以下是转过来的,具体见http://bbs.chinaunix.net/thread-3767363-1-1.html这是使用它的发明人John Nagle的名字来命名的,John Nagle在1984年首次用这个算法来尝试解决福特汽车公司的网络拥塞问题(RFC 896),该问题的具体描述是:如果我们的应用程序一次产生1个字节的数据,而这个1个字节数据又以网络数据包的形式发送到远端服务器,那么就很容易导致网络由于太多的数据包而过载。比如,当用户使用Telnet连接到远程服务器时,每一次击键操作就会产生1个字节数据,进而发送出去一个数据包,所以,在典型情况下,传送一个只拥有1个字节有效数据的数据包,却要发费40个字节长包头(即ip头20字节+tcp头20字节)的额外开销,这种有效载荷(payload)利用率极其低下的情况被统称之为愚蠢窗口症候群(Silly Window Syndrome)。可以看到,这种情况对于轻负载的网络来说,可能还可以接受,但是对于重负载的网络而言,就极有可能承载不了而轻易的发生拥塞瘫痪。
针对上面提到的这个状况,Nagle算法的改进在于:如果发送端欲多次发送包含少量字符的数据包(一般情况下,后面统一称长度小于MSS的数据包为小包,与此相对,称长度等于MSS的数据包为大包,为了某些对比说明,还有中包,即长度比小包长,但又不足一个MSS的包),则发送端会先将第一个小包发送出去,而将后面到达的少量字符数据都缓存起来而不立即发送,直到收到接收端对前一个数据包报文段的ACK确认、或当前字符属于紧急数据,或者积攒到了一定数量的数据(比如缓存的字符数据已经达到数据包报文段的最大长度)等多种情况才将其组成一个较大的数据包发送出去。

对WINDOWS来说,当TCP收到一个数据包以后,会启动一个200毫秒的计时器(这个很有趣,当时我没有解决粘包问题的时候,或者说没有发现粘包,使用200毫秒作为发送一个包的时间间隔)。当ACK确认数据包发出之后,计时器会复位,接收到下一个数据包时,会再次启动200毫秒的计时器。为了提升应用程序在内部网和Internet上的传输性能,Microsoft TCP栈使用了下面的策略来决定在接收到数据包后什么时候发送ACK确认数据包:

1、如果在200毫秒的计时器超时之前,接收到下一个数据包,则立即发送ACK确认数据包。
2、如果当前恰好有数据包需要发给ACK确认信息的接收端,则把ACK确认信息附带在数据包上立即发送。
3、当计时器超时,ACK确认信息立即发送。

第二种情况很有趣,因为这个和一个TCP传输中的一个技术有相似的地方,叫piggybacking,这个原理相似都是将ACK消息避免单独发送,而是直接附在正常数据包中。一个典型的例子是ACK一般很小,而往往我们发送数据包的死后,会有一些,几个字节空出来,这部分数据就可以填充进去ACK。

对于这三个准则来说,有些例外的地方是,如果协议栈拼接的数据包大于MTU,因特网规定的数据的大小,则将该数据立刻发送,不等待ACK确认。WINDOWS是默认开启Nagle算法的,也就是说默认他是进行拼接的。我在做客户端发送数据给服务器的时候,服务器是IOCP模型,Nagle让我吃了很大苦头,数据发过去,往往接受错误,我没想到的是客户端这边进行了拼接处理,所以服务器收到的是一个很大的数据包,解析出错。所以我们在有些情况下,需要将Nagle算法关闭,通过设置socket的模式为TCP_NODELAY,字面意思是无延迟立刻发送。这个可以保证数据不会被拼接在一起。

下面这个是我看文章看到的,但是自己没遇到过,作为一个知识吧。windows内核对网络的支持在于,他会将应用程序要发送的数据复制到内核缓冲区内,有8k这么大,可以通过SO_SNDBUF选项进行设置,绝大多数情况下,我们send出去以后其实没有进入网络而是进入了内核,由内核推送出去。SO_SNDBUF设置为0可以避免这个问题,金庸内核缓冲区。

有些情况下,我们的程序因为默认的设置,往往是这样的,没法一个数据包,需要等待前一个数据包的ACK的回复才会继续发送下一个数据包,而如前所述,由于200毫秒的定时器的存在,1秒钟往往只能发送5个包,所以这是很没有效率的,我们可以通过直接设置TCP_NODELAY(客户端和服务端都需要设置)。

看一下MSDN的解释(转)

Winsock使用下面的规则来向应用程序表明一个Send调用的完成:
1、如果socket仍然在SO_SNDBUF限额内,Winsock复制应用程序要发送的数据到内核缓冲区,完成Send调用。
2、如果Socket超过了SO_SNDBUF限额并且先前只有一个被缓冲的发送数据在内核缓冲区,Winsock复制要发送
的数据到内核缓冲区,完成Send调用。
3、如果Socket超过了SO_SNDBUF限额并且内核缓冲区有不只一个被缓冲的发送数据,Winsock复制要发送的数据
到内核缓冲区,然后投递数据到网络,直到Socket降到SO_SNDBUF限额内或者只剩余一个要发送的数据,才
完成Send调用。

所以说在windows下,大部分情况下还是不要禁用内核缓冲区,8K足够用。

加增内容 2014年1月12日

这一部分,只是说一下我在编写服务器的时候遇到的问题,粘包现象。粘包没有我之前想的那么简单,至少不只是禁用掉nagle算法那么简单。

在写IOCP的服务器的时候,遇到这么一个问题,收到的数据老是会丢包。先说一下我的服务器的处理,我自己定义了一个包结构,服务器对这个包结构进行解析。包结构=包头+数据。包头=包长度+包校验+帧号+包标示符(这个包如何处理)。但是真正的处理的时候发现少了很多包,比如我发一个文件,发文件的操作时打开文件,每1024个数据组成一个文件包发过去,然后服务器接收,然后写入文件。但是实际操作中发现,往往会少包,服务器收到的是1,2,3,6,7,8...中间的一些包就不见了,我天真的以为是丢包了。但是,很明显,TCP的传输怎么可能会丢包呢(应该是TCP丢包的概率要比我服务器收包有问题的概率小很多)。所以我就写了一个简单的小测试程序,进行观察。发现如果我连续发送100个包,每个包内容都是“abcdefg”的字符串,服务器收到包以后,打印出来。但是服务器莫名其妙的是会打印出

“abcdefgabcdefgabcdefg”,也就是连续的三个包(我已经确认我已经禁用了nagle算法)。

甚至于是这样子的

“abcdefgabcdefgabcdefgabcdefgabcd”,也就是不仅粘包了,而且断包了!!

这里我查阅了一些资料,自己总结一下所有这些问题,希望遇到问题的童鞋可以好受一些,如果有什么错误希望指正哈。

首先:说一下TCP的特性,TCP在维基百科上的定义是这样的

传输控制协议英语:Transmission Control ProtocolTCP)是一种面向连接的可靠的基于字节流的传输层通信协议。

面向连接,是指他和UDP的区别,TCP负责维护两端之间的链接状态,不会让你掉线。

可靠,是指他一定会让数据传输到数据接收端。

基于字节流,指TCP是一个流式协议。

所以根据第二条和第三条,可以总结出,我目前是可能会出现断包和粘包的可能性的,这个已经和nagle算法无关了,这是接收方的问题,接收方收到数据可能是断的,也可能是放在缓冲区里面很多条数据,被你全部取出来了。

这个解决方案很多的,包括定义包头,定义包尾,或者定义包校验,然后自己进行相关的处理等等,我不在这篇文章里面继续说了,我把解决方案放在我的IOCP服务器的编写的那个文章里面(如对您有用,移步http://blog.csdn.net/wangqing008/article/details/17371483)。有内容会持续更新。



你可能感兴趣的:(系统)