摘要
鉴于TCP传输控制过程比较复杂,单单看书就已经看了3、4遍,为加深理解,本文主要对相关机制作一个通览式的复习总结说明,如TCP是如何对成块式数据流进行流控的,慢启动、拥塞避免的机制是什么;不涉及太深的细节,文中大多内容出自于《TCP/IP协议详解 卷1:协议》。学识有限,如有错漏,请不吝指正。
前置知识
阅读本文需要具备的前置知识:
TCP/IP报文分组概念
ACK确认概念
滑动窗口滑动过程
TCP应用分类
如果从传输的应用数据量级大小来看,TCP应用可以分为两大类:
交互式数据流。该类应用每次要传输的数据通常都比较小,如telnet/secureCRT/terminal在客户端连接上远程主机敲入Linux命令时,通常每敲入一个键都会传输一个数据分组到远程主机,远程主机收到该请求报文并且经过业务逻辑处理后再回复回显给客户端,客户端此时才将按键的字符显示在屏幕上,那么在这个过程中,客户端发送的数据分组除去TCP/IP报文头后,实际传输的数据(键盘字母的字节)是非常小的。另一个例子就是远程桌面软件,滑动鼠标时产生的众多小报文分组。
成块数据流。该类应用每次传输的数据通常都较大,如ftp文件传输、email等。
在实际中要判断一个应用是属于交互式数据流,还是属于成块数据流,还是需要根据具体的实际情况进行判断。
TCP基础特性
TCP经受时延确认(ACK)
TCP在接收到数据时并不立即发送ACK,相反,TCP推迟发送,以便将ACK与需要沿该方向发送的数据一起发送(如果有新数据需要发送,则立即和ACK一起发送,否则等待,最大等待200ms)
重复确认ACK
接收方每当收到1个失序的报文,就重复一次失序前的那个报文的ACK。
交互式数据流的传输控制
从上一节可知,交互式数据流应用的特点是,每次应用要发送的数据量都比较小(产生的报文分组很小,但每个分组的TCP/IP报文头大小比应用数据本身的大小要大很多,如20字节IP首部+20字节TCP首部+1字节应用数据),当网络中出现了大量的小分组时(网络中有很多用户同时使用应用),这在局域网中通常不会有问题(客户端服务端都部署在局域网中),但在广域网上就很可能会出现问题,中间要经过好几个路由,有可能会造成拥塞(总体TCP/IP报文头大小占了大头)。因此,这时可以考虑采用Nagle算法,通过该算法可以减少分组数目,缓解网络拥塞。
Nagle算法
Nagle算法描述如下:
要求一个TCP连接上最多只能有一个未被确认的分组,在该分组的ACK确认达到之前不能发送其他的小分组。相反,Nagle算法会收集少量的正在等待的小分组,在ACK到来时以一个分组的方式发送出去。(可以理解为打包操作)
该算法是自适应的:ACK达到的越快,数据也就发送得越快;反之ACK到达越慢,数据发送得也就越慢。从这里可以看出,nagle算法可以起到减少分组数量的作用,发送的分组少了,对网络负载的冲击也会相应的降低,从而避免了进一步的拥塞。
但如果在低速的广域网上,对于要求数据实时、低时延传输的应用,就会很可能出现更高的时延(因为本该一次全部发送出去的数据,现在要分批发送,时延就更高了,本来从发送一次报文到相应的ACK回来的这个过程时延就不低。如果接收端触发了经受时延确认算法,则会更恶劣)。具体示例、阐述详见《TCP/IP协议详解 卷1:协议》-“关闭nagle算法”章节。
对于高速的局域网,Nagle算法是否适用?
在以太网上一个字节被发送、确认和回显的平均往返时间约为16ms,按这个速度算,1s可以传输1000/16=62.5个字节。那么如果在局域网中,诸如telnet/terminal这类的程序,就会出现ACK确认都回来了,新的按键数据还没有出现的尴尬情况(手速太慢),因此也就无需nagle算法了,但对于其他应用来说就另说了。
成块数据流的传输控制
对于成块数据流应用,在发送数据时有两种方式:
发送方在发送下一个数据块之前需要等待接收方对接收数据的确认(ACK),即发送-停止-等待确认-发送;
发送方在停止等待确认前可以连续发送多个分组
第一种方式比较少采用,一般是采用第二种方式,那么对于第二种方式,在一次停止等待确认前是不是可以发送任意个分组? 在TCP中,发送端有有限的发送缓存、接收端有有限的接收缓存,发送端与接收端之间可能会经过数个路由,而每个路由又有自身的缓存容量限制,如果发送端发送过多的分组突破了中间路由的缓存上限,那么会出现分组丢弃、网络拥塞,又或者接收端本地已没有足够的空闲缓存空间,这些情况也会造成分组丢弃、继而发送端不得不进行超时重传。因此,需要有合适的方法控制分组进入网络的速率以适应当前网络情况,以及适应接收端的接收能力。从发送端侧实施的流控算法有慢启动、拥塞避免,从接收端侧实施的则有滑动窗口。
滑动窗口WND
滑动窗口:
由接收方控制,接收方根据本地TCP缓存剩余空闲空间多少进行控制 通告 发送方。
可以看出,滑动窗口主要是接收端用于“告诉”发送端,“你下次最多能发送XXX字节数据过来,不能再大啦!”,这是接收段端的流控。
在局域网下,发送方一般可以一直发送多个报文段,直到达到接收方的窗口大小为止。但在广域网下,如果两者之间存在较慢的链路,中间的路由需要缓存分组,则发送过多的分组(虽然没达到滑动窗口大小)有可能出现耗尽路由的缓存、丢弃分组,从而拥塞网络。分组丢失给发送方最直观的指示(表现)是:
超时,发出去分组后很久都没有对应的ACK;
收到重复的确认,接收端接收到失序的报文,中间有报文没达到;
TCP根据不同的分组丢失指示执行不同流控策略,如果是“超时”,则接下来发送端执行“慢启动”策略,如果是“收到重复的确认”,则执行“快速重传-快速恢复-拥塞避免”策略;无论执行何种策略,TCP对一个TCP连接维护两个关键变量:
拥塞窗口CWND,描述了发送方感受到的网络拥塞的估计
慢启动阀门sstresh
在建立连接后,对CWND初始化为1个报文段,sstresh为65535个字节。当拥塞发生时,设置慢启动阀门sstresh值为当前拥塞窗口cwnd的一半。
慢启动
慢启动算法简要描述如下:
发送方初始化cwnd为1个报文段
每收到一个ACK就增加一个报文段(可以认为是一种指数增长)
cwnd上限取cwnd与wnd最小值,也就是最多不超过对方通告的滑动窗口
一直持续到当拥塞发生时所处位置一半的时候才停止(cwnd==sstresh),然后转而执行拥塞避免
拥塞避免
拥塞避免算法如下:
每次收到一个ACK时将cwnd增加1/cwnd(可以认为是加性增长)
直到发送方与接收方之间的管道被填满(cwnd==wnd),此时分组进入网络的速率与回复确认的速率基本相同
快速重传
快速重传是在发送方接收到3个或以上的重复ACK时执行的策略,这种情况下没有执行慢启动的原因是 由于收到重复的ACK不仅仅是告诉我们有一个分组已经丢失,还意味着接收端还接收到了该ACK标记的序号以后的报文组,因为接收端只有在接收到另一个报文(断序后面的)才会发送一个重复的ACK,也就是说,在收发两端仍然有流动的数据,此时不希望执行慢启动来突然减少数据流。快速重传和快速恢复算法描述如下:
当收到第3个重复的ACK时,马上重传丢失的报文,不用等到定时器溢出,将sstresh设置为当前窗口cwnd的一半,设置cwnd为sstresh+3倍报文段大小();
每收到另一个重复的ACK,cwnd增加一个报文段大小并发送一个新的分组;
当下一个确认新数据的ACK到达时,设置cwnd=sstresh(快速重传中设置的值),这个ACK应该是进行重传后的一个往返时间内对重传步骤的确认,也是对丢失的分组和收到的第1个重复的ACK之间所有的中间报文段的确认,此时可以认为快速重传、恢复已结束,接下来要执行拥塞避免,降低分组进入网络的速率(减半)。
小结
以上简要总结了TCP交互式数据流、成块数据流的流控机制,其中成块数据流流控的一个核心思想是发送报文组的数量先从1开始缓慢的指数增长(慢启动),然后再加性增长, 直至分组进入网络的速率与回复的速率基本相当。如果拥塞,则根据分组丢失的指示来执行慢启动或快速重传,无论执行何种策略,目的只有一个,那就是将分组进入网络的速率减半以缓解当前的网络拥塞状况。整体梳理一遍,如下图所示:
引用参考
《TCP/IP协议详解 卷1:协议》.W.Richard Stevens