TCP连接详解

TCP在计算机网络中是一个非常重要的概念,不论是考试还是面试出现的频率都非常的高,本篇我们就来聊聊TCP连接,它涉及到基本TCP的连接过程,以及TCP中拥塞窗口的讲解,其中对拥塞窗口的问题讲解是本篇中价值最大的。

参考资料:《Wireshark 网络分析就这么简单》 作者:林沛满

一、TCP的基本连接过程

1.1 TCP连接中的基本参数

首先我们来看一个HTTP请求的过程:
TCP连接详解
这里是前面的5个数据包,下面是最后几个数据包:
TCP连接详解
下面是数据流图:
TCP连接详解
TCP连接详解
我们可以看到在其中有很多参数,首先我们先来来了解一下这些参数的意义。

seq:表示该数据段的序号,如图中第一个数据包中显示seq=951057939

TCP提供的是有序连接,所以每一个数据包都需要标上一个序号,这个序号是由上一个数据包的Seq+数据长度而来的,当然,有一个起始的***,在这里就是我们看到的seq=951057939。因为TCP是双向的,所以双方都需要维护这么一个值。
TCP连接详解
len:该数据段的长度
这个数据段长度不包括TCP头的长度,比如上面的那个数据包长度就是1448

ack: 确认号
接收方用来向发送方告知收到了哪些数据。不需要每收到一个数据包就告知发送方收到了这个数据包,而是可以累积来告诉,比如对于上面的数据包,接受方只需要发送ack=2897,就表示数据段1和数据段2都已经收到。

除了这些参数,TCP头还有很多标志:

  • SYN:携带这个标志的包,表示正在发起连接请求,因为连接是双向的,所以双方都需要发送。
  • FIN:携带这个标志的包,表示正在发起连接终止请求,因为连接是双向的,所以双方都需要发送。
  • RST:用于重置一个混乱的连接,或者拒绝一个无效的请求

现在我们可以来看看TCP是如何管理连接的:

1.2 建立连接过程

首先在建立连接的过程中会发送三个包,就是所谓的“三次握手”。就是上图中的前3个包

  1. 3372 → 80 [SYN] Seq=951057939 Win=8760 Len=0 MSS=1460 SACK_PERM=1
  2. 80 → 3372 [SYN, ACK] Seq=290218379 Ack=951057940 Win=5840 Len=0 MSS=1380 SACK_PERM=1
  3. 3372 → 80 [ACK] Seq=951057940 Ack=290218380 Win=9660 Len=0

翻译成文字就是:

  1. 客户端:我能和你建立连接吗?我的初始seq=951057939,如果同意就发送Ack=951057940
  2. 服务器:我同意,Ack=951057940,我也想和你建立连接,我的初始Seq=290218379,如果同意就发送Ack=290218380
  3. 客户端:我同意,Ack=290218380

为什么需要三个包来确认连接呢?如果使用两个的话,我可以看看一个场景:

首先客户端发送的第一个数据包跑到一个延迟严重的路径上,服务器迟迟没有收到这个请求,客户端由于没有收到来自服务器的响应,自然认为第一个数据包丢失了,又发送了一遍,这次成功建立了连接。然后,跑到一个延迟严重的路径上数据包终于到达了服务器,服务器会认为这是一个新的连接,发送确认包给客户端,如果只有两个包来确认连接,这就结束了,但是服务器白白建立了一个多余的连接,但是如果有三个包,当服务器发送确认包给客户端时,客户端知道这是一个无效的连接,就会发送拒绝包给服务器,服务器收到后,就会放弃这个连接。

1.3 断开连接过程

在断开连接的过程中会发送四个包,就是所谓的“四次握手”。就是上图中的最后4个包

  1. 80 → 3372 [FIN, ACK] Seq=290236744 Ack=951058419 Win=6432 Len=0
  2. 3372 → 80 [ACK] Seq=951058419 Ack=290236745 Win=9236 Len=0
  3. 3372 → 80 [FIN, ACK] Seq=951058419 Ack=290236745 Win=9236 Len=0
  4. 80 → 3372 [ACK] Seq=290236745 Ack=951058420 Win=6432 Len=0

翻译成文字就是:

  1. 客户端:我希望断开连接
  2. 服务器:知道了,断开吧
  3. 服务器:我也想断开连接
  4. 客户端:知道了,断开吧

这样双方都断开了连接,实际上四次挥手的断开也并完全不可靠。有兴趣的读者可以阅读其他资料。

二、TCP/IP的窗口控制

在上面的讲解过程中,实际上我们还会发现一个参数Win。这个参数就是接收方用来声明自己的接收窗口的。什么是接收窗口?我们先来看看一个例子:

如果一个快递员需要送100个包裹去公司,如何发送比较合理?如果一口气送100个,公司狭小的前台只够容纳20个包裹的位置,需要等待签收完了再发送,同时快递员受限于电瓶车的运力,一次只能送10包裹,这个时候电瓶车的运力又是效率的瓶颈了。其实这个和TCP中传输大块的数据原理很相似。

在实际发送数据的过程中,如果一口气把数据全部发送过去,接收方的接收缓存(接收窗口)可能无法接收这么多数据。同时网络带宽也是一个需要考虑的问题,一口气发太多数据可能会导致丢包。这两个因素共同限制了发送方的发送窗口。

2.1 发送窗口对通信时间的限制

发送窗口代表的含义是,在没有的得到接收端确认的情况下发送端最多可以发送的字节数。这里要注意,我说的是发送窗口,它的大小是接收窗口和网络通信质量两个方面共同决定的。举个例子,如果发送方的发送窗口比较大,可能直接可以一下子发送几十个数据包而不用考虑接收方的应答,如果发送窗口比较小,可能需要发送两三个就停下来等待接收端的应答,如果接收到应答,发送窗口又可以恢复了。显然,如果发送两三个就停下来等待应答,就会导致总的通信时间的上升。

2.2 关于窗口的几个小问题

2.2.1 如何判断发送窗口的大小?

我们知道接收窗口的大小可以在包中的win参数的值看出,发送窗口的大小怎么看呢?答案是没有方法,在通信的过程中,我们没有办法通过数据包来判断发送窗口的大小,我们只能大致的推断出发送窗口的大小。因为发送窗口如果只是由接收窗口限制的话,还比较简单,这时候接收窗口和发送窗口一样大。而如果还有网络其他因素影响的话,就会使事情变得复杂起来。但是我们知道发送窗口不可能大于接收窗口,如果接收窗口为0,发送窗口一定为0。再者,如果接收窗口非常大,而发送端发送一两个包就立刻停下来等待确认,我们也可以判断发送窗口就是发送的那一两个包的大小。

2.2.2 发送窗口和MSS的关系

在建立连接请求的数据包中还有一个MSS参数,这个MSS的含义是每个TCP数据包所能携带的最大数据量。这样一来我们也就知道了,发送窗口决定了一口气可以发送多少个字节,MSS决定了要分多少个包发送完成。

2.2.3 接收端什么时候发送ACK信号给发送端

我们知道ack信号的发送可以积累,但是实际上接收端发送ACK信号非常有讲究,具体可以参考这里

2.2.4 "TCP Window Scale"是什么

在TCP刚被发明的时候,全世界的网络带宽都很小,所以最大接收窗口被定义为65535个字节。后来随着硬件革命,显然65536已经无法表示了,但是TCP头中给接收窗口大小值只留了6bit。无法超过65536。但是TCP头有一个options的位置,我们在这里放一个Window Scale,它向对方声明一个Shift count,它作为2的指数然后乘以TCP头中定义的接收窗口的大小,作为真正的接收窗口的大小。

2.3 网络对发送窗口的限制

网络之所以能够限制发送窗口的大小是因为,如果发送端一个口气发送太多的数据就会导致网络拥塞,拥塞的结果就是丢包,这是发送方最忌讳的,因为丢包就得重传,重传又会导致拥塞。在这种情况下发送方如何避免触碰到拥塞点呢?要避免触碰到拥塞点,首先就得知道拥塞点,但是自网络诞生的几十年以来,没有一个人能有一个很好的方法来获取拥塞点的大小。但是我们却有另一个比较靠谱的策略。这个策略就是在发送方维护一个虚拟的拥塞窗口,并使用各种算法使其尽可能接近真实的拥塞点。网络对发送窗口的限制就是通过拥塞窗口实现的。这里需要明确一个概念,就是拥塞窗口并不是一个新的东西,它实际上就是在进行拥塞控制的时候发送方的发送窗口。就是下面来看看拥塞窗口是如何维护的。

  1. 连接刚刚建立的时候,发送方对网络状况一无所知。如果一口气发太多数据就可能遭遇拥塞,所以发送方把拥塞窗口的初始值定得很小。RFC的建议是2个、3个或者4个MSS,具体视MSS的大小而定。

  2. 如果发出去的包都得到确认,表明还没有达到拥塞点,可以增大拥塞窗口。由于这个阶段发生拥塞的概率很低,所以增速应该快一些。RFC建议的算法是每收到n个确认,可以把拥塞窗口增加n个MSS。比如发了2个包之后收到2个确认,拥塞窗口就增大到2+2=4,接下来是4+4=8,8+8=16……这个过程的增速很快,但是由于基数低,传输速度还是比较慢的,所以被称为慢启动过程。

  3. 慢启动过程持续一段时间后,拥塞窗口达到一个较大的值。这时候传输速度比较快,触碰拥塞点的概率也大了,所以不能继续采用翻倍的慢启动算法,而是要缓慢一点。RFC建议的算法是在每个往返时间增加1个MSS。比如发了16个MSS之后全部被确认了,拥塞窗口就增加到16+1=17个MSS,再接下去是17+1=18,18+1=19……这个过程称为拥塞避免。从慢启动过渡到拥塞避免的临界窗口值很有讲究。如果之前发生过拥塞,就把该拥塞点作为参考依据。如果从来没有拥塞过就可以取相对较大的值,比如和最大接收窗口相等。 全过程可以用下图表示。
    TCP连接详解

2.3.1 超时重传

触碰到拥塞点之后的反应就是,发出去的包不能像往常一样得到确认,不过发出去的包得不到确认可能是网络延迟所引起的,所以这个时候发送方决定等待一段时间之后再判断,如果迟迟收不到确认,就认定包丢失了,只能重传。这段等待的时间称为RTO。有些操作系统提供了调节RTO大小的参数。

既然发生了超时重传,为了不给拥塞的网络带来更大的负担,拥塞窗口必然要得到调整。RFC建议将拥塞窗口直接下降到1个MSS,然后进入慢启动阶段。最后到到拥塞避免的时候的临界窗口值,选为发生拥塞的时候没有被确认的数据量的1/2,但不小于2个MSS。比如发送方发送了19个包出去,但是只得到了前3个包的确认。就将临界窗口值设为(19-3)/2 = 8;如下图所示:
TCP连接详解
可以看到超时重传对传输性能的巨大影响。不但耽误了RTO的时间,还将窗口变得非常小。

2.3.2 快速重传

以上情况是发生在大量的数据包得不到确认的情况下所发生的,这很有可能触碰到了拥塞点。而有时候拥塞很轻微,只有少量的包丢失。还有些偶然因素,比如校验码不对的时候,会导致单个丢包。这两种丢包症状和严重拥塞时不一样,因为后续有包能正常到达。当后续的包到达接收方时,接收方会发现其Seq号比期望的大,所以它每收到一个包就Ack一次期望的Seq号,以此提醒发送方重传。当发送方收到3个或以上重复确认(DupAck)时,就意识到相应的包已经丢了,从而立即重传它。这个过程称为快速重传。之所以称为快速,是因为它不像超时重传一样需要等待一段时间。不过对于小文件,凑不满3个重复确认,只能等待超时重传了。

为什么要规定凑满3个呢?这是因为网络包有时会乱序,乱序的包一样会触发重复的Ack,但是为了乱序而重传没有必要。由于一般乱序的距离不会相差大大,比如2号包也许会跑到4号包后面,但不太可能跑到6号包后面,所以限定成3个或以上可以在很大程度上避免因乱序而触发快速重传。

如果发生了快速重传,是没有必要像超时重传那样将拥塞窗口重新设置得很小的,接下来传慢一点就行。RFC 5681规定,发生了快速重传后,将临界窗口值设置为发生拥塞的时候没有被确认的数据量的1/2,但不小于2个MSS,然后将临界窗口值加3个MSS,继续保留在拥塞避免过程。这个过程称为快速恢复,如下图所示。
TCP连接详解
总结来说就是:

  • 如果发生超时重传,则进入慢启动。
  • 如果发生快速重传,则进入快速恢复。

不过需要注意,什么情况下会发生超时重传,什么情况下会发生快速重传。

2.3.3 SACK

在上面讲述快速重传的时候,我们讲到为什么会发生快速重传,是因为有一个数据包没有被接收端收到,导致后面每收到一个数据包就向发送端请求一次期望的seq号数据包。如果有两个或者多个数据包没有被接收到那么如何处理呢?可以使用SACK方案。

比如接收端没有收到2、3号数据包,那么在接收到后面的数据包的时候它会这样回复:

  • 收到4号数据包:告诉发送方,“我已经收到4号数据包,请给我2号”
  • 收到5号数据包:告诉发送方,“我已经收到4、5号数据包,请给我2号”
  • 收到6号数据包:告诉发送方,“我已经收到4、5、6号数据包,请给我2号”

这时候重传2号包之后,又会重传3号包。之后正常发送后面的数据包。

相信读者可能还有一个疑问,就是在拥塞控制的时候,拥塞窗口在逐渐增大,那么一定会触碰到拥塞点吗?其实不一定会,比如Windows中不启用我们之前所讲的"TCP Window Scale",那么接收窗口最大就是64K,那么发送发送窗口(拥塞窗口)到达64k之后就不会增长了,而很多环境的拥塞点大于64K,所以永远不会触碰到拥塞点。再者有很多时候我们发送的是小数据包,在拥塞窗口增长的过程就已经发送完了。

除了上面所说的对拥塞窗口计算的算法。还有很多其他算法,比如对于上面描述的临界窗口的计算除了RFC 5681给出的算法,还有Westwood+算法。甚至还有抛弃上面所说的慢启动,临界窗口,拥塞避免等概念建立新的算法的Vegas算法。还有Window中的Compound算法,它维护两个拥塞窗口,一个类似Vegas算法,一个类似RFC 2581,两者共同决定拥塞窗口大小。总结来说就是:

  • RFC 5681:如前文所描述的算法
  • Westwood+:和RFC 5681差不多,只是临界窗口大小计算不一样
  • Vegas:另辟蹊径,和前面两个算法都不一样,有兴趣的读者可以自己查阅
  • Compound:结合Vegas和RFC 2518

这些算法没有绝对的好坏,最重要的是根据具体的网络平台选择合适的算法。

最后虽然我在文中描述了发送窗口不可能大于发送窗口的字样,但是从参考资料中的一些语言描述中,似乎又可以大于发送窗口,但是查找资料并没有给出一个明确的答案,也许是自己有误解,不过还是希望知道的读者可以在下方评论。总的来说在实际过程中还有很多复杂的情况,这里只是做出一个大致的描述,读者有一个大概的印像即可,以次为依托从而深入更加细节的地方。