目前大多数互联网数据通信都是通过TCP协议进行的,了解其通信方式对提高通信效率,排查通信效率问题有很重要的意义。
一. TCP的滑动窗口机制
1. 概述
TCP协议是可靠的通信协议,数据发送方发送给数据接收方的每一个包必须需要数据接收方返回对应的ACK,否则数据发送方就需要重传这个包。这个模式就有点像我和你面对面聊天,你一句我一句。但这种方式的缺点是效率比较低的。如果你说完一句话,我在处理其他事情,没有及时回复你,那你不是要干等着我做完其他事情后,我回复你,你才能说下一句话,很显然这不现实。这样的传输方式有一个缺点:数据包的往返时间越长,通信的效率就越低。
为解决这个问题,TCP 引入了滑动窗口这个概念。即使在往返时间较长的情况下,它也不会降低网络通信的效率。那么有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。假设窗口大小为 3 个 TCP 段,那么发送方就可以「连续发送」 3 个 TCP 段,并且中途若有 ACK 丢失,可以通过「下一个确认应答进行确认」。如下图:
图中的 ACK 600 确认应答报文丢失,也没关系,因为可以通过下一个确认应答进行确认,只要发送方收到了 ACK 700 确认应答,就意味着 700 之前的所有数据「接收方」都收到了。这个模式就叫累计确认或者累计应答。
2. 流量控制
数据发送方发送数据和处理ACK的方式需要一个机制来保证数据传输的可靠性的同时确保数据发送方发送速度和数据接收方的处理数据速度保持一致,避免数据接收方处理不了了,数据发送方还在持续发送,也就是说需要这个机制来做流量控制。
TCP协议使用滑动窗口方式来实现流量控制。首先明确滑动窗口的范畴:TCP是双工的协议,会话的双方都可以同时接收和发送数据。TCP会话的双方都各自维护一个发送窗口和一个接收窗口。各自的接收窗口大小取决于应用、系统、硬件的限制(TCP传输速率不能大于应用的数据处理速率)。各自的发送窗口需要和对端进行协商来决定窗口的大小的。
3. 窗口大小的协商
TCP 头里有一个字段叫 Window,也就是窗口大小。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
3.1 参数说明:
建立连接:
-
Flag中的Window size value:窗口的大小
-
Flag中的Calculated Window size:放大后窗口的大小,也就是实际可用的大小
-
Options中的Window scale:窗口可以放大的倍数,也就是Calculated Window size最大等于Window size value 乘以 2 的 Window scale 次方
比如Window size value如果等于1024,Window scale等于7,那么Calculated Window size最大可以等于1024*128=131072
传输过程中,可以在抓包的数据中看到Win字段的大小就是目前TCP接收方可用窗口的大小。
3.2 建立连接时告知对方自己接收窗口大小
发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。TCP是双工通信的,TCP连接在建立时,双方互相告知自己接收窗口的大小,避免对方发送的数据大于自己接收窗口的尺寸。下面是一个实例:
A机器发SYN包中告诉对端自己接收窗口大小为7300并且不可放大:
B机器回的ACK包告诉A机器自己接收窗口大小为28960,并且可以扩大128倍:
在这个示例中,B机器向A机器发送数据的时候,因为A机器的窗口比较小且不可扩展,可能会因为窗口过小导致传输效率过低的问题。
3.3 通信过程中协商窗口大小
在数据传输过程中数据接收方会根据自身缓存的处理速度和余量来和发送方协商窗口大小,下面抓包是一个较好的通讯过程中协商窗口大小的例子:
-
包175,接收方发送ACK携带WIN = 384,告知发送方,现在只能接收384个字节
-
包176,发送方果真只发送了384个字节,Wireshark也比较智能,也宣告TCP Window Full
-
包177,接收方回复一个ACK,并通告窗口为0,说明接收方已经收到所有数据,并保存到缓冲区,但是这个时候应用程序并没有接收这些数据,导致缓冲区没有更多的空间,故通告窗口为0, 这也就是所谓的零窗口,零窗口期间,发送方停止发送数据
-
发送方察觉到窗口为0,则不再发送数据给接收方
-
包178,接收方发送一个窗口通告,告知发送方已经有接收数据的能力了,可以发送数据包了
-
包179,接收方收到窗口通告之后,就发送缓冲区内的数据了.
4. 窗口滑动
4.1 发送方窗口滑动
发送方的发送数据如下图所示被分为四类:
1.已经成功发送,并且收到确认
2.已经发送成功尚未确认
3.未发送,准备发送
4.未发送,不允许发送
其中第二类数据和第三类数据处于TCP发送端的滑动窗口中,发送窗口只有收到发送窗口内字节的ACK确认,才会移动发送窗口的左边界;
发送方只能发送第三类数据,如果窗口很小,那么每一批发送的数据也就很少,如果ACK返回很慢,那么数据传输速度会非常慢
4.2 接收方窗口滑动
接收方的缓存数据分为三类:
1. 已接收并已确认
2. 未接受但准备好接收(接收窗口)
3. 尚未接收并尚未准备好接收
接收方窗口只有在前面所有的段都确认的情况下才会移动左边界。当在前面还有字节未接收但收到后面字节的情况下,窗口不会移动,并不对后续字节确认。以此确保发送方会对这些数据重传。
三. Window Scale过小对传输速率的影响
TCP传输数据时,发送方窗口过小或者接收方窗口过小,都会严重影响数据传输的效率,特别在异地跨机房传输数据的情形,由于ACK返回的时间较长,窗口过小对数据传输速率的影响会被放大。
下面我们来做一个实验来验证发送方窗口过小会严重影响TCP传输速率,实验的前置条件是:
-
异地机房,一个包从发送方发送给接收方时间开销15ms左右;
-
发送方TCP窗口大小为8092字节,不可扩张;
我们来看下传输过程的截图:
从这个截图我们可以看到发送方每一批数据分四个包发,四个包的总尺寸加起来正好是8092个字节,包发送出去后等30毫秒后才收到ACK,然后开始发送下一批8092个字节的数据。也就是说每发送8KB的数据需要30MS左右,按照这个速度发送10Mbit(1028KB)的数据大约,1028/8*30/1000=4.8秒,这个速度比较慢了。
我们做个调优,将发送方的窗口设置为64KB,并且设置Window scale为7,再来传输大文件,下面是抓包截图:
由于发送方的窗口足够大,发送请求的时候不需要等待ACK,可以持续在连接上写TCP数据包,在发送方看来,发送33KB才花费0.6MS,加上路上的时间,发送10Mb的数据一共花费了880MS,还不到一秒,效率提升了5倍多。
四.JAVA的apache httpclient设置发送和接收窗口大小
Java的apache httpclient是使用较广的HTTP连接池开源库,在使用的时候允许用户设置rcvBufSize和sndBufSize,如果设置了这两个值,也就设置了TCP连接中本端的发送方和接收方窗口的大小,最要命的是代码设置了之后就关闭了linux内核的动态调整功能,会严重影响通信效率,建议不设置,由linux系统根据自身资源来动态调整TCP连接的发送方和接收方窗口的大小。
五.总结
本文科普了TCP连接中窗口滑动机制的原理,详述了窗口大小动态调整的机制,在第三节详细描述了发送窗口过小可能导致数据传输过慢的根本原因:
发送方在发送完窗口内的数据后,需要等待ACK回来后才能继续发送下一批数据,在窗口很小的时候,发送方大部分时间都在等待ACK,所以会严重影响传输速率,特别在发送方和接收方距离较远RT较高时更加明显。
通过程序设置rcvBufSize和sndBufSize会导致窗口不可动态变化,建议在通常情况下不要设置,除非有特别的考虑。