TCP在计算机网络中是一个非常重要的概念,不论是考试还是面试出现的频率都非常的高,本篇我们就来聊聊TCP连接,它涉及到基本TCP的连接过程,以及TCP中拥塞窗口的讲解,其中对拥塞窗口的问题讲解是本篇中价值最大的。
参考资料:《Wireshark 网络分析就这么简单》 作者:林沛满
首先我们来看一个HTTP请求的过程:
这里是前面的5个数据包,下面是最后几个数据包:
下面是数据流图:
我们可以看到在其中有很多参数,首先我们先来来了解一下这些参数的意义。
seq:表示该数据段的序号,如图中第一个数据包中显示seq=951057939
TCP提供的是有序连接,所以每一个数据包都需要标上一个序号,这个序号是由上一个数据包的Seq+数据长度而来的,当然,有一个起始的序列号,在这里就是我们看到的seq=951057939。因为TCP是双向的,所以双方都需要维护这么一个值。
len:该数据段的长度
这个数据段长度不包括TCP头的长度,比如上面的那个数据包长度就是1448
ack: 确认号
接收方用来向发送方告知收到了哪些数据。不需要每收到一个数据包就告知发送方收到了这个数据包,而是可以累积来告诉,比如对于上面的数据包,接受方只需要发送ack=2897,就表示数据段1和数据段2都已经收到。
除了这些参数,TCP头还有很多标志:
现在我们可以来看看TCP是如何管理连接的:
首先在建立连接的过程中会发送三个包,就是所谓的“三次握手”。就是上图中的前3个包
翻译成文字就是:
为什么需要三个包来确认连接呢?如果使用两个的话,我可以看看一个场景:
首先客户端发送的第一个数据包跑到一个延迟严重的路径上,服务器迟迟没有收到这个请求,客户端由于没有收到来自服务器的响应,自然认为第一个数据包丢失了,又发送了一遍,这次成功建立了连接。然后,跑到一个延迟严重的路径上数据包终于到达了服务器,服务器会认为这是一个新的连接,发送确认包给客户端,如果只有两个包来确认连接,这就结束了,但是服务器白白建立了一个多余的连接,但是如果有三个包,当服务器发送确认包给客户端时,客户端知道这是一个无效的连接,就会发送拒绝包给服务器,服务器收到后,就会放弃这个连接。
在断开连接的过程中会发送四个包,就是所谓的“四次握手”。就是上图中的最后4个包
翻译成文字就是:
这样双方都断开了连接,实际上四次挥手的断开也并完全不可靠。有兴趣的读者可以阅读其他资料。
在上面的讲解过程中,实际上我们还会发现一个参数Win。这个参数就是接收方用来声明自己的接收窗口的。什么是接收窗口?我们先来看看一个例子:
如果一个快递员需要送100个包裹去公司,如何发送比较合理?如果一口气送100个,公司狭小的前台只够容纳20个包裹的位置,需要等待签收完了再发送,同时快递员受限于电瓶车的运力,一次只能送10包裹,这个时候电瓶车的运力又是效率的瓶颈了。其实这个和TCP中传输大块的数据原理很相似。
在实际发送数据的过程中,如果一口气把数据全部发送过去,接收方的接收缓存(接收窗口)可能无法接收这么多数据。同时网络带宽也是一个需要考虑的问题,一口气发太多数据可能会导致丢包。这两个因素共同限制了发送方的发送窗口。
发送窗口代表的含义是,在没有的得到接收端确认的情况下发送端最多可以发送的字节数。这里要注意,我说的是发送窗口,它的大小是接收窗口和网络通信质量两个方面共同决定的。举个例子,如果发送方的发送窗口比较大,可能直接可以一下子发送几十个数据包而不用考虑接收方的应答,如果发送窗口比较小,可能需要发送两三个就停下来等待接收端的应答,如果接收到应答,发送窗口又可以恢复了。显然,如果发送两三个就停下来等待应答,就会导致总的通信时间的上升。
我们知道接收窗口的大小可以在包中的win参数的值看出,发送窗口的大小怎么看呢?答案是没有方法,在通信的过程中,我们没有办法通过数据包来判断发送窗口的大小,我们只能大致的推断出发送窗口的大小。因为发送窗口如果只是由接收窗口限制的话,还比较简单,这时候接收窗口和发送窗口一样大。而如果还有网络其他因素影响的话,就会使事情变得复杂起来。但是我们知道发送窗口不可能大于接收窗口,如果接收窗口为0,发送窗口一定为0。再者,如果接收窗口非常大,而发送端发送一两个包就立刻停下来等待确认,我们也可以判断发送窗口就是发送的那一两个包的大小。
在建立连接请求的数据包中还有一个MSS参数,这个MSS的含义是每个TCP数据包所能携带的最大数据量。这样一来我们也就知道了,发送窗口决定了一口气可以发送多少个字节,MSS决定了要分多少个包发送完成。
我们知道ack信号的发送可以积累,但是实际上接收端发送ACK信号非常有讲究,具体可以参考这里
在TCP刚被发明的时候,全世界的网络带宽都很小,所以最大接收窗口被定义为65535个字节。后来随着硬件革命,显然65536已经无法表示了,但是TCP头中给接收窗口大小值只留了6bit。无法超过65536。但是TCP头有一个options的位置,我们在这里放一个Window Scale,它向对方声明一个Shift count,它作为2的指数然后乘以TCP头中定义的接收窗口的大小,作为真正的接收窗口的大小。
网络之所以能够限制发送窗口的大小是因为,如果发送端一个口气发送太多的数据就会导致网络拥塞,拥塞的结果就是丢包,这是发送方最忌讳的,因为丢包就得重传,重传又会导致拥塞。在这种情况下发送方如何避免触碰到拥塞点呢?要避免触碰到拥塞点,首先就得知道拥塞点,但是自网络诞生的几十年以来,没有一个人能有一个很好的方法来获取拥塞点的大小。但是我们却有另一个比较靠谱的策略。这个策略就是在发送方维护一个虚拟的拥塞窗口,并使用各种算法使其尽可能接近真实的拥塞点。网络对发送窗口的限制就是通过拥塞窗口实现的。这里需要明确一个概念,就是拥塞窗口并不是一个新的东西,它实际上就是在进行拥塞控制的时候发送方的发送窗口。就是下面来看看拥塞窗口是如何维护的。
连接刚刚建立的时候,发送方对网络状况一无所知。如果一口气发太多数据就可能遭遇拥塞,所以发送方把拥塞窗口的初始值定得很小。RFC的建议是2个、3个或者4个MSS,具体视MSS的大小而定。
如果发出去的包都得到确认,表明还没有达到拥塞点,可以增大拥塞窗口。由于这个阶段发生拥塞的概率很低,所以增速应该快一些。RFC建议的算法是每收到n个确认,可以把拥塞窗口增加n个MSS。比如发了2个包之后收到2个确认,拥塞窗口就增大到2+2=4,接下来是4+4=8,8+8=16……这个过程的增速很快,但是由于基数低,传输速度还是比较慢的,所以被称为慢启动过程。
慢启动过程持续一段时间后,拥塞窗口达到一个较大的值。这时候传输速度比较快,触碰拥塞点的概率也大了,所以不能继续采用翻倍的慢启动算法,而是要缓慢一点。RFC建议的算法是在每个往返时间增加1个MSS。比如发了16个MSS之后全部被确认了,拥塞窗口就增加到16+1=17个MSS,再接下去是17+1=18,18+1=19……这个过程称为拥塞避免。从慢启动过渡到拥塞避免的临界窗口值很有讲究。如果之前发生过拥塞,就把该拥塞点作为参考依据。如果从来没有拥塞过就可以取相对较大的值,比如和最大接收窗口相等。 全过程可以用下图表示。
触碰到拥塞点之后的反应就是,发出去的包不能像往常一样得到确认,不过发出去的包得不到确认可能是网络延迟所引起的,所以这个时候发送方决定等待一段时间之后再判断,如果迟迟收不到确认,就认定包丢失了,只能重传。这段等待的时间称为RTO。有些操作系统提供了调节RTO大小的参数。
既然发生了超时重传,为了不给拥塞的网络带来更大的负担,拥塞窗口必然要得到调整。RFC建议将拥塞窗口直接下降到1个MSS,然后进入慢启动阶段。最后到到拥塞避免的时候的临界窗口值,选为发生拥塞的时候没有被确认的数据量的1/2,但不小于2个MSS。比如发送方发送了19个包出去,但是只得到了前3个包的确认。就将临界窗口值设为(19-3)/2 = 8;如下图所示:
可以看到超时重传对传输性能的巨大影响。不但耽误了RTO的时间,还将窗口变得非常小。
以上情况是发生在大量的数据包得不到确认的情况下所发生的,这很有可能触碰到了拥塞点。而有时候拥塞很轻微,只有少量的包丢失。还有些偶然因素,比如校验码不对的时候,会导致单个丢包。这两种丢包症状和严重拥塞时不一样,因为后续有包能正常到达。当后续的包到达接收方时,接收方会发现其Seq号比期望的大,所以它每收到一个包就Ack一次期望的Seq号,以此提醒发送方重传。当发送方收到3个或以上重复确认(DupAck)时,就意识到相应的包已经丢了,从而立即重传它。这个过程称为快速重传。之所以称为快速,是因为它不像超时重传一样需要等待一段时间。不过对于小文件,凑不满3个重复确认,只能等待超时重传了。
为什么要规定凑满3个呢?这是因为网络包有时会乱序,乱序的包一样会触发重复的Ack,但是为了乱序而重传没有必要。由于一般乱序的距离不会相差大大,比如2号包也许会跑到4号包后面,但不太可能跑到6号包后面,所以限定成3个或以上可以在很大程度上避免因乱序而触发快速重传。
如果发生了快速重传,是没有必要像超时重传那样将拥塞窗口重新设置得很小的,接下来传慢一点就行。RFC 5681规定,发生了快速重传后,将临界窗口值设置为发生拥塞的时候没有被确认的数据量的1/2,但不小于2个MSS,然后将临界窗口值加3个MSS,继续保留在拥塞避免过程。这个过程称为快速恢复,如下图所示。
总结来说就是:
不过需要注意,什么情况下会发生超时重传,什么情况下会发生快速重传。
在上面讲述快速重传的时候,我们讲到为什么会发生快速重传,是因为有一个数据包没有被接收端收到,导致后面每收到一个数据包就向发送端请求一次期望的seq号数据包。如果有两个或者多个数据包没有被接收到那么如何处理呢?可以使用SACK方案。
比如接收端没有收到2、3号数据包,那么在接收到后面的数据包的时候它会这样回复:
这时候重传2号包之后,又会重传3号包。之后正常发送后面的数据包。
相信读者可能还有一个疑问,就是在拥塞控制的时候,拥塞窗口在逐渐增大,那么一定会触碰到拥塞点吗?其实不一定会,比如Windows中不启用我们之前所讲的"TCP Window Scale",那么接收窗口最大就是64K,那么发送发送窗口(拥塞窗口)到达64k之后就不会增长了,而很多环境的拥塞点大于64K,所以永远不会触碰到拥塞点。再者有很多时候我们发送的是小数据包,在拥塞窗口增长的过程就已经发送完了。
除了上面所说的对拥塞窗口计算的算法。还有很多其他算法,比如对于上面描述的临界窗口的计算除了RFC 5681给出的算法,还有Westwood+算法。甚至还有抛弃上面所说的慢启动,临界窗口,拥塞避免等概念建立新的算法的Vegas算法。还有Window中的Compound算法,它维护两个拥塞窗口,一个类似Vegas算法,一个类似RFC 2581,两者共同决定拥塞窗口大小。总结来说就是:
这些算法没有绝对的好坏,最重要的是根据具体的网络平台选择合适的算法。
最后虽然我在文中描述了发送窗口不可能大于发送窗口的字样,但是从参考资料中的一些语言描述中,似乎又可以大于发送窗口,但是查找资料并没有给出一个明确的答案,也许是自己有误解,不过还是希望知道的读者可以在下方评论。总的来说在实际过程中还有很多复杂的情况,这里只是做出一个大致的描述,读者有一个大概的印像即可,以次为依托从而深入更加细节的地方。