本文整理自:《Wireshark网络分析就这么简单 第1版》
作者:林沛满 著
出版时间:2014-12
假如你是一位勤劳的快递员,要送100个包裹到某公司去,怎样送货才科学?
最简单的方式是每次送1个,总共跑100趟。当然这也是最慢的方式,因为往返次数越多,消耗的时间就越长。除了需要减肥的快递员,一般人不会选择这种方式。最快的方式应该是一口气送100个,这样只要跑一趟就够了。可惜现实没有这么美好,往往存在各种制约因素:公司狭小的前台只容得下20个包裹,要等签收完了才能接着送;更令人郁闷的是,电瓶车只能装10个包裹。综合这两个因素,不难推出电瓶车的运力是效率瓶颈,而前台的空间则不构成影响。
快递送货的策略非常浅显,几乎人人可以理解,而TCP传输大块数据的策略却很少人懂。事实上这两者的原理是相似的。
TCP显然不用电瓶车送包,但它也有“往返”的需要。因为发包之后并不知道对方能否收到,要一直等到确认包到达,这样就花费了一个往返时间。假如每发一个包就停下来等确认,一个往返时间里就只能传一个包,这样的传输效率太低了。最快的方式应该是一口气把所有包发出去,然后一起确认。但现实中也存在一些限制:接收方的缓存(接收窗口)可能一下子接受不了这么多数据;网络的带宽也不一定足够大,一口气发太多会导致丢包事故。所以,发送方要知道接收方的接收窗口和网络这两个限制因素中哪一个更严格,然后在其限制范围内尽可能多发包。这个一口气能发送的数据量就是传说中的TCP发送窗口。
发送窗口对性能的影响有多大?一图胜千言,图1显示了发送窗口为1个MSS(即每个TCP包所能携带的最大数据量)和2个MSS时的差别。在相同的往返时间里,右边比左边多发了两倍的数据量。而在真实环境中,发送窗口常常可以达到数十个MSS。
图2就是在真实环境中抓的包,抓包时服务器10.32.106.73正往客户端10.32.106.103发数据。由于服务器的发送窗口很大,所以收到读请求之后,它在没有客户端确认的情况下连续发了10个包。
接着我把客户端的接收窗口强制成2920,相当于两个TCP包能携带的数据量。从图3中可以看到客户端通过“win=2920”把自己的接收窗口告诉服务器。因此服务器把发送窗口限制为2920,每发两个包就停下来等待客户端的确认。同样一个14215字节的读操作,图2只用1个往返时间就完成了,而图3则用了6个。
为了更好地说明这个过程,我把27号包到32号包用对话的形式表示出来,括号内的文字为我添加的注释。
27号包:客户端:“当前我的接收窗口是2920。”
28号包:服务器:“(好,那我的发送窗口就定为2920。)先给你1460字节。”
29号包:服务器:“再给你1460字节。(哎呀!我的发送窗口2920用完了,不能再发了。)”
30号包:客户端:“你发过来的2920字节已经处理完毕,所以现在我的接收窗口又恢复到2920。”
31号包:服务器:“(好,那我再把发送窗口定为2920。)给你一个1460字节。”
32号包:服务器:“再给你1460字节。(哎呀!我的发送窗口2920又用完了,不能再发了。)”
你也许有个疑问,本文的开头不是说有两个限制因素吗?这个例子只提到了接收窗口对发送窗口的限制,那网络的影响呢?由于网络的影响方式非常复杂,所以本文暂时跳过。下一篇文章将作详细介绍。
不知道出于何种原因,TCP发送窗口的概念被广泛误解,比如,很多人会把接收窗口误认为发送窗口。我经常想在论坛上回答相关提问,却不知道该从何答起,因为有些提问本身就基于错误的概念。下面是一些经常出现的问题。
这不是发送窗口,而是在向对方声明自己的接收窗口。在此例子中,10.32.106.103向10.32.106.73声明自己的接收窗口是64093字节。10.32.106.73收到之后,就会把自己的发送窗口限制在64093字节之内。很多教科书上提到的滑动窗口机制,说的就是这两个窗口的关系,本文就不再赘述了。
假如接收方处理数据的速度跟不上接收数据的速度,缓存就会被占满,从而导致接收窗口为0。如图5的Wireshark截屏所示,89.0.0.85持续向89.0.0.210声明自己的接收窗口是win=0,所以89.0.0.210的发送窗口就被限制为0,意味着那段时间发不出数据。
很遗憾,没有简单的方法,有时候甚至完全没有办法。因为,当发送窗口是由接收窗口决定的时候,我们还可以通过“windowsize:”的值来判断。而当它由网络因素决定的时候,事情就会变得非常复杂(下篇文章将会详细介绍)。大多数时候,我们甚至不确定哪个因素在起作用,只能大概推理。以图5为例,接收方声明它的接收窗口等于0,那接收窗口肯定起了限制作用(因为不可能再小了),因此可以大胆地判断发送窗口就是0。再回顾本文开头10.32.106.73向10.32.106.103传数据的两个例子。第一个例子中,我们只能推理出10.32.106.73的发送窗口不小于那10个包(39~48号)携带的数据总和,但具体能达到多少却不得而知,因为窗口还没用完时读操作就完成了。第二个例子比较容易分析,因为传了两个包就停下来等确认,所以发送窗口是那两个包携带的数据量2920。
发送窗口决定了一口气能发多少字节,而MSS决定了这些字节要分多少个包发完。举个例子,在发送窗口为16000字节的情况下,如果MSS是1000字节,那就需要发送16000/1000=16个包;而如果MSS等于8000,那要发送的包数就是16000/8000=2了。
不一定,确认包一般会少一些。由于TCP可以累积起来确认,所以当收到多个包的时候,只需要确认最后一个就可以了。比如本文中10.32.106.73向10.32.106.103传数据的第一个例子中,客户端用一个包(包号49)确认了它收到的10个包(39~48号包)。
在TCP刚被发明的时候,全世界的网络带宽都很小,所以最大接收窗口被定义成65535字节。随着硬件的革命性进步,65535字节已经成为性能瓶颈了,怎么样才能扩展呢?TCP头中只给接收窗口值留了16bit,肯定是无法突破65535 (2^16 - 1)的。
1992年的RFC 1323中提出了一个解决方案,就是在三次握手时,把自己的Window Scale信息告知对方。由于Window Scale放在TCP头之外的Options中,所以不需要修改TCP头的设计。Window Scale的作用是向对方声明一个Shift count,我们把它作为2的指数,再乘以TCP头中定义的接收窗口,就得到真正的TCP接收窗口了。
以图6为例,从底部可以看到10.32.106.159告诉10.32.106.103说它的Shift count是5。25等于32,这就意味着以后10.32.106.159声明的接收窗口要乘以32 才是真正的接收窗口值。
接下来我们再看图7中的3号包。10.32.106.159声明它的接收窗口为“Window size value:183”,183乘以32得到5856,所以Wireshark就显示出“Win=5856”了。要注意Wireshark是根据Shiftcount计算出这个结果的,如果抓包时没有抓到三次握手,Wireshark就不知道该如何计算,所以我们有时候会很莫名地看到一些极小的接收窗口值。还有些时候是防火墙识别不了WindowScale,因此对方无法获得Shiftcount,最终导致严重的性能问题。