TCP Data Flow and Window Management

在前面的内容,我们主要关注TCP如何建立连接的,以及TCP是如何保证数据的可靠传输的,如何来处理报文的丢失的。在本章中,一开始先介绍下在交互性较强的应用中TCP是怎么做的。比如说在在线游戏,按下一个键,按下一下鼠标就要发送一个TCP报文,这种情况下报文中的TCP header占据了更多的内容,有效字节(数据)非常少,游戏的操作往往是十分频繁的,那么就会导致在短时间内往网络中塞很多数据,可能会造成拥塞,甚至接收方来不及处理。如果说需要更好的性能,将多个小报文装在一起在发送,那么就会引入延迟。所以如何取舍,是一个难题。

在本章中,我们主要讨论TCP的窗口管理来对流量进行控制,和交互性应用的一些话题。

Interactive Communication

在本节中,我们会使用ssh来查看delayed acknowledgement是如何工作的和Nagle algorithm是如何来降低网络中的小报文的(small packet)。当我们在ssh程序中,按下一个键位,然后通过网络将信息传给服务器,服务器收到之后再将这个键位的信息返回给客户端。注意,双方通信之间的数据都是经过加密的。

在ssh的时候,很多人会对键盘的按键都会产生一个TCP报文这个事情感到意外(确实,我就是这样)。这也就是说:

That is, the keystrokes are sent from the client to the server individually (one character at a time rather than one line at a time)

此外,发送一个keystrokes之后,服务器还会返回一个字符。总的来说,在ssh中,按下一个键,就会涉及到4条报文:client按下键位,服务器返回的ack,服务器echo回来的字符,客户端返回的ACK(如下图的图(a))。不过,第二个和第三个通常会合并为一个,如下图(b)的。

SSH中的报文交互

下面是文中使用ssh来连接上一个服务器,并且使用date来输出时间的wireshark抓包图,我自己抓出来的很不一样,就不放了。

ssh程序的wireshark抓包

第1个报文是,按下d之后发送给服务器的报文,第2个报文是echo的数据以及ACK的报文,第3个报文是客户端对于服务器返回的echo和ACK的ACK。就是上面图(b)的情况。接下来的都与之类似,每3个报文看作一组。两组相邻的报文之间的时间间隔就是人按下的键位之间的时间间隔了。

注意到,这些报文的长度都是48字节,这里的长度不包括报文头,接下来会讲到,这些报文的长度应该是88字节。在最后的16-19报文长度变成了64字节,这是服务器返回的日期的信息。另外,这些包含着数据的报文都有PSH位,这就是说赶紧交给上面的应用程序。

this flag is conventionally used to indicate that the buffer at the side sending the packet has been emptied in conjunction with sending the packet

Delayed Acknowledgements

在很多情况中,TCP并不会为每一个接收到的分组返回ACK。因为累计确认的缘故,我们只要返回最大的一个ACK,也能使得发送方窗口滑动。使用了累计ACK使得TCP能够延迟一会发送ACK。同时还可以在ACK中附带一些数据,这叫做捎带(piggyback)。

This is a form of piggybacking that is used most often in conjunction with bulk data transfers

delay发送了更少的ACK,所以能够降低网络中的流量。通常来说,2个分组返回一个ACK是很常见的。

Delaying ACKs causes less traffic to be carried over the network than when ACKs are not delayed because fewer ACKs are used. A ratio of 2 to 1 is fairly com- mon for bulk transfers.

在返回ACK之前delay以及是否使用delay ACK都是可以在操作系统中配置的。比如说,在Mac OS x中,

net.inet. tcp.delayed_ack,0:disable delay,1:总是delay 2: ACK every other packet(没隔一个分组确认一次)3: 在2的基础上,自动检测。

但是,我们不能delay太久了,不然就会对方被误认为超时了,所以会引入一个Delayed Timer,超时了就发送ACK。此外,对于交互性很强的应用(ssh)如果和nagle(接下来就要介绍)组合在一起,那么性能也很差。

Nagle Algorithm

就之前的SSH例子当中,按下一个键位,使得从客户端发送到浏览器的报文有88字节,20字节IP头,20字节TCP头,48字节的数据。在这些报文中,有效数据和协议头之间差距并不大。这种小报文会使得网络的负载较大。

However, these tinygrams can add to congestion and lead to inefficient use of capacity on wide area networks

使用Nagle算法就可以解决这个问题,它是由John Nagle提出的。

Nagle算法的核心非常简单:当一个TCP连接此时有发送但是未被确认的报文的时候,小报文不能被发送,除非之前的数据被确认了,换句话说Nagle算法只能在一个RTT之内发送一个客户端的报文。

The Nagle algorithm says that when a TCP connection has outstanding data that has not yet been acknowledged, small segments (those smaller than the SMSS) cannot be sent until all outstanding data is acknowledged.

这样一来,Nagle算法使得TCP成了停等(stop and wait)的状态。Nagle算法这种停等的行为,降低了发送方发送报文的速度,也就间接地缓解了网络的压力。

下面以ssh作为例子来讲述Nagle算法,下图首先是未启用Nagle算法的SSH例子:

未使用nage算法的ssh

这里总共有5个ssh请求报文,7个响应报文,7个pure ACK。pure ACK都是来自客户端发送给服务器的,表明你echo回来的东西我已经收到了。总共花了0.58s。

下面是启用Nagle算法的SSH抓包结果:

使用了nagle算法的ssh

总共只有11个分组,第1,2和第3,4是两次不同的keystroke,可以看到每次的时间大概是190ms,和此时的RTT差不多。可以看到Nage算法降低了报文的数量。因为pure ack不存在了,client对于echo回来的ack的报文,也捎带了此时的keystroke。

比较两者时间,第一个0.58s,第二个0.80s。启用Nagle算法后分组更少,但是时间更长。下图帮助理解:

原文对于Nagle算法的打包发送说的不多,只是说了:

The Nagle algorithm says that when a TCP connection has outstanding data that has not yet been acknowledged, small segments (those smaller than the SMSS) cannot be sent until all outstanding data is acknowledged. Instead, small amounts of data are collected by TCP and sent in a single segment when an acknowledgment arrives.

下面再补充一段wiki的解释:

Nagle's algorithm works by combining a number of small outgoing messages and sending them all at once. Specifically, as long as there is a sent packet for which the sender has received no acknowledgment, the sender should keep buffering its output until it has a full packet's worth of output, thus allowing output to be sent all at once.

Delayed ACK and Nagle Algorithm Interaction

当Delay ACK和Nagle算法一起使用的时候,就会出现一些问题。在ssh的例子中,如果我们在客户端启用delay ACK,然后在服务端启用Nagle算法。如下图:

nage with delayed ack

通常来说,TCP(delayed ACK的缘故)会收到两个full size的分组之后才返回一个ACK。但是这里并不是,在这里的两个TCP报文中,因为一个是full size的,一个是small。所以在上图中,client会一直不发送ACK。而在Server中,因为 Nagle算法的缘故,不收到ACK就不发数据了。就陷入了死锁。不过幸运的是,这种死锁并不会持续太久,只要delayed ACK timer超时以后,ACK就发过去了。但是死锁的这段时间就被浪费了,因此在SSH程序中,就需要关闭nagle 算法。各个系统中都有方法来关闭Nagle算法。

Disable the Nagle algorithm

在有些场景中,Nagle算法应该被关闭。比如在在线游戏当中,按下键盘或者鼠标点击要立刻发送过去。在Linux中,使用TCP_NODELAY来关闭Nagle算法。此外,还可以通过设置配置文件的方式来关闭Nagle算法,比如说Windows中使用设置注册表的方式来关闭。

Flow Control and Window management

在之前,说过TCP的双方都有一个窗口,可以用来进行流量控制,不至于发送到网络中的数据太多了。在之前的SSH例子中,我们看到所有的窗口值都是未发生变化的,4220字节。如下图(我选取了其中一张):

窗口都没有发生变化

如果TCP-based应用不是那么的繁忙,处理数据的速度很快,那么就会出现这种,窗口长度没有发生变化的情况。

leading to no change of the Window Size field as the connection progresses.

在另外一些程序中,它可能忙于做其他事情,所以窗口长度会逐渐减小。最后,如果应用程序一直不读取数据,那么接收方必须通知接收方,停下来别再发数据给我了。这个过程叫作advertisement of zero window。

This is accomplished by sending a window advertisement of zero (no space)

所接收的报文中的Window size字段表示对方的缓冲区还有多少字节可以用于接收数据。Window size的最大字节数是65535字节,如果启动了Window scale字段,那就可以表示更大, 最大可以到1GB。

Sliding Window

因为TCP是一个全双工的协议,因此双方都可以维持一个发送窗口和接收窗口。接下来,我们对这两个窗口做更详细的解释,接下来首先解释的是发送窗口的示意图:

发送窗口

窗口的度量单位是字节,而不是分组的数目。接下来先介绍这些名词:

  • offered window:用于给对方提供数据的窗口,会逐渐滑动,上图中4-9字节就是offered window的长度。offered window的大小,是由接收方在它的window size字段中指定的。

The size of the offered window is controlled by the Window Size field sent by the receiver in each ACK.

  • SND.UNA:窗口的左边界,小于SND.UNA即表示已经被确认的数据。
  • usable window:窗口中可用的字节数,即offered window中的减去已经被发送但是还未被确认的字节数。上图中的SND.WND是offered window的大小,SND.NXT是下一个可发送的字节offset。usable window = SND.UNA+SND.WND-SND.NXT。上述例子中的带进去就可以了。

随着时间流逝,窗口的大小由边界变量的移动来改变。

  1. window close表示的是左边界(SND.UNA)的右移。这会发生在ACK返回的时候和window size变得更小的时候。
  2. window open表示的是右边界(SND.UNA+SND.WND)的右移,当接收方的应用程序读取数据的时候,会使得更多的缓存空间被释放。所以offered window也会增大。
  3. window shrink发生在窗口右边界左移的时候,虽然RFC 1122不建议这一点, 但是TCP必须能够处理这个。

TCP可以根据对方返回的报文中的ACK和window size字段来调节自己的窗口大小。但是左边界但是不能左移,左边界是由ACK来控制的,因为ACK是不能回退的。当window size并未改变,但是左边界和右边界都移动了,就叫作窗口滑动。如果返回的ACK增加了,但是window size字段减小了,那么左边界右移使得左右边界更加靠近,当两者完全靠在一起的时候,这时就叫作zero window,说明接收方缓冲区没有地方可以存新来的数据了。所以发送方开始探测(probe)对方的窗口,来知道什么时候可以发送。

接收方窗口比较简单,如下图:

接收窗口

左边界表示的是接下来到达的数据的offset,小于左边界都是已经被acked 的数据。对于任何小于总边界的数据都会被认为是重复的直接丢弃,对于任何接收到的序列号大于右边界都会被直接丢弃。只有恰好填充在左边界的数据才会增加ACK。对于接收到的乱序数据,只要在窗口范围内都是可以接收的,中间的gap可以使用SACK来提示对方发送缺少的数据

Zero Window and the TCP Persist Timer

TCP的接收方通过它的advertisement window来告知发送方我还有多少数据可以接收。

We have seen that TCP implements flow control by having the receiver specify the amount of data it is willing to accept from the sender: the receiver’ s adver- tised window

当advertisement window变为0的时候,发送方必须停止发送数据知道advertisement window不为0,直到接收方发送了一条window update报文来通知它可以继续发送数据。但是 因为window update不包含数据,因此TCP并不会保证他的可靠传输,因此发送方必须有方法来知晓这一情况(即可以给对方发送数据了),即使在window update丢失的情况下。

Because such updates do not generally contain data (they are a form of “pure ACK”), they are not reliably delivered by TCP

如果接收方收到一条zero window的报文,但是发送方返回来的window update报文丢失了。那么双方都会停止操作,等待来自己对方的报文,如同死锁。所以,在收到zero window之后,发送方会使用一个persist timer来定期的检查发送方那边的是否用空闲缓冲区了。在persist timer超时以后,发送方就给接收方发送一条window probe的报文。强制让对方返回一条ACK报文,且必须包含着window size。

The persist timer triggers the transmission of window probes. Window probes are segments that force the receiver to provide an ACK, which also necessarily contains a Win-dow Size field.

Window probe包含着一个字节的数据,所以TCP可以保证他的可靠传输,这就避免了前面所说的死锁情况。

The probes are sent whenever the TCP persist timer expires, and the byte included may or may not be accepted by the receiver, depending on how much buffer space it has available.

接收方如果没有空闲空间,那么就不会返回ACK。有的话就返回ACK并且带有window size字段。

PS: persist timer也会执行exponential back off,和之前多次提到的一样。

Example

下面是一个zero window的例子,在接收方中,程序运行之后暂停20秒再去读取接收到的数据。如下图(部分):

运行一段时间以后,发送方返回来的window size字段一直在变大,看起来十分违反直觉,这是因为在接收方中有缓冲区的自动扩容功能。

This is because of an automatic window adjustment algorithm (see Section 15.5.4) that allocates memory to the receiving TCP even if not requested by the application

PS:回想一下,前面ssh的例子中的窗口不变是因为应用程序很快就将发送过来的数据都处理了。与这里的原因不同

没过多久以后,接收方的缓冲区就被塞满了。此时发送方就会接到zero window的报文:

zero window

然后发送方多次发送window probe来查看对方缓冲区是否可以接收数据。到20s之后,应用程序读取数据之后,就可以发送数据了。

Silly Window Syndrome

在window-based的协议中,尤其是这些segment-size不固定的协议,会受到silly window syndrome的影响。

Window-based flow control schemes, especially those that do not use fixed-size segments (such as TCP), can fall victim to a condition known as the silly window syndrome (SWS)

这种情况发生的时候,会使得双方之间交互的报文都是小报文,而不是full size的报文。这会导致效率低下。因为所携带的数据,很少,可能只比报文头多一点点。

This leads to undesirable inefficiency because each segment has relatively high overhead—a small number ofd ata bytes relative to the number of bytes in the headers.

SWS在发送方和接收方中都会发生:接收方返回了很小的window size,或者发送方发送了很小的数据报。

the receiver can advertise small windows (instead of waiting until a larger window can be advertised), and the sender can transmit small data segments (instead of waiting for additional data to send a larger segment)

其实,SWS最直观的问题就是,在接收方比较繁忙的时候,如果窗口一有点空闲就通告,对方又会发送一个小报文过来,此消彼长,可能在一段时间内,交互的报文都是小报文,所以引入了SWS avoidance的策略

为了避免这个问题,TCP实现的时候需要遵循以下规则:

  1. 当作为接收者的时候,小的window advertisement不能发送。RFC 1122中规定了:发送方不能advertise比当前窗口(可能为0)更大窗口值,直到当前窗口值超过了一个full size segment(receive MSS)或者缓冲区大小的一半,取决于这两个哪个更小。这两种例子可能在如下情形发生:1)应用程序读取了数据,释放了缓冲区。2)发送方发送了window probe。
  2. 当作为发送者的时候,不能发送小的报文段,由Nagle算法来决定什么时候发送小的报文段。发送方不传送报文来避免SWS,除非几种情况例外:
  3. 一个full size报文可以发送
  4. 所发送的数据大小超过了对方所advertise的窗口长度的一半。
  5. 满足以下两个条件的时候,想发就发:1)一个ACK不是目前所期望的(也就是说,我们没有outstanding data but unacknowledged)。2)Nagle算法被关闭了

上述的三个条件,同样能够帮助我们回答以下的问题:a)如果Nagle算法阻止发送方发送小报文,那么多小的报文才算小呢?根据条件1提到,小于full size的报文就是small packet。

SWS Example

下面介绍了一个SWS避免的例子。

接收方:有3000字节大小的缓冲区,暂停15秒之后开始读取数据,每次读取之间间隔2秒,每次读取256字节。

下面是wireshark的抓包图。经过握手阶段以后,发送方在发送了1460字节和588字节的分组。

返回来的ACK的window size=952字节,小于一个MSS(在握手阶段可以看到,TCP的MSS为1460字节)的大小。所以接收方的Nagle算法禁止它发送报文(条件1)。经过5秒后,定时器超时后,它就会如同window probe一样,发送数据过去。于是就导致了对方返回zero window advertisement。

PS:不知道这个5秒代表着什么。我猜可能是,TCP本次发送的数据会塞满缓冲区,所以停止一会,期望对方先释放点缓冲区出来,省的开始window probe的环节,这应该也是SWS的策略之一。

在6.970,差不多发送zero window 2秒之后,发送方发送了window probe来看看是否有空闲空间。但是因为前面说了程序要到15秒才开始读取数据,所以此时返回的window probe ACK中的window size字段为0。接着后面的persist timer会发生在4秒后,接着8秒后,window probe的persist timer也是exponential backoff的

showing the characteristic exponential timeout back off.

在25.061,此时应用程序已经进行了6次读取。所以此时缓冲区有空闲了。根据接收方的SWS避免,window size超过了一个full size,所以可以发送一条window update报文。

window update报文

接着,接收到1460字节的数据。于是缓冲区就剩下75字节大小了。这似乎违反了,接收方的SWS avoidance策略,75字节小于一个MSS也小于缓冲区的四分之一(在Free BSD中,不是二分之一)。之所以会这样是因为,为了避免窗口的收缩(shrinkage)。

在前面说过:如果窗口的右边界往左移动就是窗口的收缩,但是应该避免这种情况。

在packet 15中,可以知道此时窗口的右边界是(3002+1535)=4537。如果packet 17 advertise的窗口小于75字节,那么将会造成窗口的收缩。TCP是不允许这一点的(至于为什么会小于75我也不知道)。因此避免窗口收缩比避免SWS的优先级更高。这也就是说,如果通告的窗口可以避免窗口收缩,那么SWS避免就直接忽视。这里就是这样的情况,虽然我认为他说的差点说服力。

在packet 17和packet 18之间又间隔了5秒,因为此时发送数据也会充满缓冲区。所以出现了和最开始那个5秒一样的行为。接着开始了window probe的环节。在packet 21通告了767字节的window size,此时如果一发送数据,又会塞满缓冲区,因此又进入了SWS避免的环节。

原文的表格太长就不复制了。下面来讲述接收方如何实现SWS的避免。在15秒之后,接收方应用程序开始读取数据,到18.408之前只读取了512字节的数据。SWS 避免策略发现它小于MSS(1460字节)也小于缓冲区的一半(1500字节)。所以不能发送window update给接收方。但是,window probe仍然被接收了,因为此时有缓冲区是可用的,可以观察到packet 14的 ACK增长了一个字节,是因为window probe的1个字节数据被接受了。

虽然511字节可用,但是接收方的SWS避免策略使得它不发送window update。在FreeBSD中,当缓冲区可用字节超过四分之一或者超过一个MSS才可发送,511 < 750,所以这里再packet 14返回的还是zero window。

接下来的过程都是与之类似,略。

Large Buffers and Auto-Tuning

前面说过了小的接收缓存会导致SWS问题,因而性能变得低下。为了避免这些问题,我们要避免应用程序来分配TCP缓冲区。在大多数情况中,应用程序分配的TCP缓存都会被忽视。

In most cases, the size specified by the application is effectively ignored, and the operating system instead uses either a large fixed value or a dynamically calculated value

在新的版本中的windows和Linux中,window auto-tuning都是支持的。就可以避免之前的问题,动态地增长缓冲区大小。下面的是Linux2.6.7中的接受和发送缓存的最大值和默认值,r=read,w=write:

缓存的大小

下面是自动调节中缓冲区大小,这里的自动缓冲区大小 的意思应该是(我认为的),在上面提到的缓冲区大小之上,可以在伸缩的大小

这三个数值从左到右分别表示,最小值,默认值,最大值。

Example

这是书上的例子,应用程序启动之后,暂停了20秒再去读取数据。使用wireshark的抓包结果如下:

window auto-tunning

上图中,我们可以看到,虽然应用程序一直没有读取数据,但是返回的window size仍然在增长。这就是window auto-tunning的缘故。经过一段时间后,就没有空闲的缓存空间了,20s以后,应用程序读取了数据,接收方返回了window update报文来通知对方,缓存有空闲位置了

zero window

Summary

原文中涉及到的攻击的内容以及紧急指针的内容就略过了,因为紧急指针现在使用的很少。

交互性应用较强的应用通常会返回小于SMSS的报文,比如说在SSH中echo back。使用delayed ACK可以稍微晚一些时候返回ACK,这样一来可以在ACK中附带一些数据返回。这叫捎带--piggyback。使用delayed ACK可以降低网络中报文的数量,但是这就引入一些delay。

此外,如果在一些RTT较长的网络中,塞入大量的数据可能会进一步恶化网络的环境。所以使用Nagle算法,来降低短时间内可以塞入到网络中的报文数量,不过它也会引入delay。然而delay ACK和Nagle算法如果一起使用可能会导致死锁的情况,因此程序员可以选择关闭或者开启Nagle算法。

接收方返回的报文中包含着window size字段,它表明了接收方还有多大的空闲缓冲区可以接受数据。当接收方没有空闲的缓冲区的时候,它就返回zero window来告知对方,有空闲的缓冲区的时候返回window update报文。但是window update报文会丢失,所以在接收方中定期来询问接收方可以发数据了吗,使用window probe报文来完成这一点。

基于窗口的协议,可能会导致SWS(silly window syndrome)问题,这是因为接收方缓冲区太小或者发送方发送的报文太小造成的。为了避免这个情况,缓冲区大小不能由应用程序来指定,而是内核指定一个较大的TCP缓冲区,此外在大多数系统中,这个缓冲区大小还是可以自动调节的,保证了性能

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇