TCP\IP协议详解

本文中用到的图片转自: 头条文章

TCP与UDP协议有什么区别?

TCP是一个面向连接的、可靠的、基于字节流的传输层协议。

UDP是一个面向无连接的传输层协议。

与UDP相比,TCP有以下特性:

  1. 面向连接。所谓的连接,指的是客户端和服务器的连接,在双方互相通信之前,TCP 需要三次握手建立连接,而 UDP 没有相应建立连接的过程。
  2. 可靠性。TCP 使用了非常多机制的保证连接的可靠,这个可靠性体现在哪些方面呢?一个是有状态,另一个是可控制。
  3. 面向字节流。UDP 的数据传输是基于数据报的,这是因为仅仅只是继承了 IP 层的特性,而 TCP 为了维护状态,将一个个 IP 包变成了字节流。

TCP 会精准记录哪些数据发送了,哪些数据被对方接收了,哪些没有被接收到,而且保证数据包按序到达,不允许半点差错。这是有状态

当意识到丢包了或者网络环境不佳,TCP 会根据具体情况调整自己的行为,控制自己的发送速度或者重发。这是可控制

相应的,UDP 就是无状态, 不可控的。

三次握手

在介绍三次握手时我们先来看看TCP的报文头格式:

img
img

  • 端口号:用来标识同一台计算机的不同的应用进程。
  • 源端口:源端口和IP地址的作用是标识报文的返回地址。
  • 目的端口:端口指明接收方计算机上的应用程序接口。(TCP报头中的源端口号和目的端口号同IP数据报中的源IP与目的IP唯一确定一条TCP连接。)
  • 序号和确认号:是TCP可靠传输的关键部分。序号是本报文段发送的数据组的第一个字节的序号。在TCP传送的流中,每一个字节一个序号。e.g.一个报文段的序号为300,此报文段数据部分共有100字节,则下一个报文段的序号为400。所以序号确保了TCP传输的有序性。确认号,即ACK,指明下一个期待收到的字节序号,表明该序号之前的所有数据已经正确无误的收到。确认号只有当ACK标志为1时才有效。比如建立连接时,SYN报文的ACK标志位为0。
  • 数据偏移/首部长度:4bits。由于首部可能含有可选项内容,因此TCP报头的长度是不确定的,报头不包含任何可选字段则长度为20字节,4位首部长度字段所能表示的最大值为1111,转化为10进制为15,15*328 = 60,故报头最大长度为60字节。首部长度也叫数据偏移,是因为首部长度实际上指示了数据区在报文段中的起始偏移值。
  • 保留:为将来定义新的用途保留,现在一般置0。
  • 控制位:URG ACK PSH RST SYN FIN,共6个,每一个标志位表示一个控制功能。
    1. URG:紧急指针标志,为1时表示紧急指针有效,为0则忽略紧急指针。
    2. ACK:确认序号标志,为1时表示确认号有效,为0表示报文中不含确认信息,忽略确认号字段。
    3. PSH:push标志,为1表示是带有push标志的数据,指示接收方在接收到该报文段以后,应尽快将这个报文段交给应用程序,而不是在缓冲区排队。
    4. RST:重置连接标志,用于重置由于主机崩溃或其他原因而出现错误的连接。或者用于拒绝非法的报文段和拒绝连接请求。
    5. SYN:同步序号,用于建立连接过程,在连接请求中,SYN=1和ACK=0表示该数据段没有使用捎带的确认域,而连接应答捎带一个确认,即SYN=1和ACK=1。
    6. FIN:finish标志,用于释放连接,为1时表示发送方已经没有数据发送了,即关闭本方数据流。
  • 窗口:滑动窗口大小,用来告知发送端接受端的缓存大小,以此控制发送端发送数据的速率,从而达到流量控制。窗口大小16bit,因此窗口大小最大为65535。
  • 校验和:奇偶校验,此校验和是对整个的 TCP 报文段,包括 TCP 头部和 TCP 数据,以 16 位字进行计算所得。由发送端计算和存储,并由接收端进行验证。
  • 紧急指针:只有当 URG 标志置 1 时紧急指针才有效。紧急指针是一个正的偏移量,和顺序号字段中的值相加表示紧急数据最后一个字节的序号。 TCP 的紧急方式是发送端向另一端发送紧急数据的一种方式。
  • 选项和填充:最常见的可选字段是最长报文大小,又称为MSS(Maximum Segment Size),每个连接方通常都在通信的第一个报文段(为建立连接而设置SYN标志为1的那个段)中指明这个选项,它表示本端所能接受的最大报文段的长度。选项长度不一定是32位的整数倍,所以要加填充位,即在这个字段中加入额外的零,以保证TCP头是32的整数倍。
  • 数据部分: TCP 报文段中的数据部分是可选的。在一个连接建立和一个连接终止时,双方交换的报文段仅有 TCP 首部。如果一方没有数据要发送,也使用没有任何数据的首部来确认收到的数据。在处理超时的许多情况中,也会发送不带任何数据的报文段。

报头格式介绍完了,接下来我们看看TCP的三次握手过程:

img
img

过程描述:

  • 客户端发起连接请求,报头中的SYN=1,ACK=0,TCP规定SYN=1时不能携带数据,但要消耗一个序号,因此声明自己的序号是 seq=x。此时客户端状态CLOSED->SYN-SENT
  • 服务器接收到连接请求,然后进行回复确认,发送SYN=1 ACK=1 seq=y ack=x+1。此时服务端状态LISTEN->SYN-RCVD
  • 客户端收到对SYN的确认包之后再次确认,SYN=0 ACK=1 seq=x+1 ack=y+1,此时客户端状态SYN-SENT->ESTABLISHED
  • 服务端接收到客户端的确认包后状态变为ESTABLISHED

为什么要进行三次握手?

保证通信是“全双工”的,即客户端和服务器都具备发送和接收能力,假如没有客户端的第二次确认,那么服务端无法确保客户端已经接收到自己发出去的SYN确认包。而且假如只有两次握手,那么会存在资源浪费的情况:

客户端发出SYN请求后,由于网络复杂情况,这个请求一直没有发送到服务器端,这时候客户端超时重发第二个请求,然后第二个请求服务端正常接收并建立连接,数据传输完毕后双方断开了连接,但这时候第一次发送的那个SYN包终于到达服务端了,由于是二次握手,所以服务端发送确认包,并建立连接,但是客户端实际上已经断开连接了,而服务端建立了一条“没有”客户端的连接,造成了资源浪费。

重传机制

三次握手结束之后就可以开始收发数据了,那要怎么保证我们发送出去的消息确实被接收到了呢?以寄快递为例,虽然我们寄快递前确认收件人信息无误,但是快递寄出去的时候,如果我们没有收到反馈(比如收件人告知你快递我已经收到了或者是快递公司的收件通知)。那么我们是无法知道快递是否准确送达的。

在TCP中,是通过序列号与确认应答来保证的(回想下上面的报头格式)。正常的传输过程如下:

img
img

但是实际情况是非常复杂的,假如网络出现丢包的情况要怎么办呢?这就涉及到TCP的重传机制

  • 超时重传
  • 快速重传
  • SACK
  • D-SACK

超时重传

在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据。

数据包丢失或者确认应答丢失时会发生超时重传:

img
img

问题来了,这个“特定的时间间隔”应该设置成多少合适呢?这就引入了RTT(Round-Trip Time 往返时延)的概念:

img
img

RTT简单来说就是数据一次往返的时间,超时重传时间是以 RTO (Retransmission Timeout 超时重传时间)表示。这个重传时间的设置比较玄学,设置高了或者低了都会有问题:

img
img

其实不用看图片大家应该也能猜到,超时时间过大,那么就会导致效率的降低(大部分时间都用来等待)。超时时间过低的话那就有可能造成不必要的重传(重传刚发出去结果就收到了应答)。

所以RTO的计算方法比较复杂,因为网络状态是时时刻刻变化的,而且也会存在波动较大的情况,所以RTO的值也是动态变化的,这里面的算法就不赘述了~~~有兴趣的小伙伴可以网上查找相关资料。

快速重传

我们一直强调网络情况是非常复杂的,所以上面提到的超时重传无法解决所有问题,设想这么一种场景:数据包只是因为某一个网络节点的异常而丢失了,实际上网路是“畅通”的,假如还是使用超时重传那么效率显然太低了,因此,快速重传机制诞生了。

img
img

  • 发送端发送了5份数据;
  • 接收方接收到seq1时返回ACK2,但是seq2由于网络异常丢包没有收到;
  • 这时候接收端又接收到seq3,在应答的时候依然返回ACK2;
  • 后续seq4和seq5也收到了,但是seq2还没有收到,依然返回ACK2。
  • 发送端连续收到三次同样的ACK2,知道seq2没有被接收到,就会在定时器任务触发之前重传seq2。
  • 接收端接收到seq2,而且检测到seq3、4、5都已经收到。所以返回ACK6

所以,快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。

但是快速重传也有问题,发送端只知道seq2丢失了,但是不知道seq3、4、5有没有被接收到。所以发送端并不清楚要不要把后续的seq3、4、5一并重传。于是便引入了SACK

SACK( Selective Acknowledgment 选择性确认)

这种方式需要在 TCP 头部「选项」字段里加一个 SACK,他可以记录已接收的数据,随着报头一起发送给发送方,这样发送方就清楚什么数据被接收了,什么数据没有接收,这样就可以只重传丢失的数据。

img
img

发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过SACK 信息发现只有 200~299 这段数据丢失,重发时只需要重传这段数据即可。

如果要支持 SACK,必须双方都要支持。在 Linux 下,可以通过 net.ipv4.tcp_sack参数打开这个功能(Linux 2.4 后默认打开)。
Duplicate SACK

Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。 主要解决下面两种情况:

img
img

ACK丢包。

img
img

网络延时。

在 Linux 下可以通过 net.ipv4.tcp_dsack 参数开启/关闭这个功能(Linux 2.4 后默认打开)。

滑动窗口

细心的小伙伴可能发现了,上面的图片里面并不是等到上一次数据的确认收到之后才发送下一批数据,因为串行发送数据效率太低,所以TCP引入了滑动窗口的概念。

有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值

窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。

假设窗口大小为 3 个 TCP 段,那么发送方就可以「连续发送」 3 个 TCP 段,并且中途若有 ACK 丢失,可以通过「下一个确认应答进行确认」。如下图:

img
img

图中的 ACK 600 确认应答报文丢失,也没关系,因为可以通过下一个确认应答进行确认,只要发送方收到了 ACK 700 确认应答,就意味着 700 之前的所有数据「接收方」都收到了。这个模式就叫累计确认或者累计应答

那么窗口大小应该怎么设置呢?

回想一下TCP报头格式,是不是有一个“窗口”字段?这个“窗口”字段就是用来表示窗口大小的。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据接收端的处理能力来发送数据,而不会导致接收端处理不过来。

所以,通常窗口的大小是由接收方的决定的。

发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。

下图就是发送方缓存的数据,根据处理的情况分成四个部分,其中深蓝色方框是发送窗口,紫色方框是可用窗口:

img
img

在下图,当发送方把数据「全部」都一下发送出去后,可用窗口的大小就为 0 了,表明可用窗口耗尽,在没收到 ACK 确认之前是无法继续发送数据了。

img
img

在下图,当收到之前发送的数据 32~36 字节的 ACK 确认应答后,如果发送窗口的大小没有变化,则滑动窗口往右边移动 5 个字节,因为有 5 个字节的数据被应答确认,接下来 52~56 字节又变成了可用窗口,那么后续也就可以发送 52~56 这 5 个字节的数据了。

img
img

TCP 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针(指特定的序列号),一个是相对指针(需要做偏移)。

img
img

  • SND.WND:表示发送窗口的大小(大小是由接收方指定的);
  • SND.UNA:是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。
  • SND.NXT:也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节。
  • 指向 #4 的第一个字节是个相对指针,它需要 SND.NXT 指针加上 SND.WND大小的偏移量,就可以指向 #4 的第一个字节了。

可用窗口大小 = SND.WND -(SND.NXT - SND.UNA)

接下来是接收端的窗口

img
img

  • RCV.WND:表示接收窗口的大小,它会通告给发送方。
  • RCV.NXT:是一个指针,它指向期望从发送方发送来的下一个数据字节的序列号,也就是 #3 的第一个字节。
  • 指向 #4 的第一个字节是个相对指针,它需要 RCV.NXT 指针加上 RCV.WND大小的偏移量,就可以指向 #4 的第一个字节了。

接收窗口≈发送窗口

因为滑动窗口是一直在变化的,假如接收方的应用读取数据的速度很快,那么接收窗口就很快就可以腾出空间,但是在把新的接收窗口大小发给发送端的时候是存在时延的,所以发送窗口约等于接收窗口。

流量控制

从上面我们可以知道接收端是有接收窗口的,接收方的应用在处理数据时也需要一定的时间,假如发送方不加以控制,那么接收方将无法处理过多的数据,导致出发重传机制,从而浪费网络流量。

为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。

我们先看看假设接收窗口和发送窗口保持200不变的发送过程:

img
img

  1. 客户端发送请求数据报文。
  2. 服务端接收到请求,发送确认以及80字节数据,这时发送窗口的SND.UNA = 241, SND.NXT = SND.WND + 80 = 321。可用发送窗口大小 = 200 - ( 321 - 241) = 120。
  3. 客户端接收到数据,这时接收窗口RCV.NXT = RCV.NXT + 80 = 321,发送确认报文。
  4. 发送端继续发送120个字节,由于这时还没收到客户端的确认数据包,发送窗口SND.UNA = 241, SND.NXT = 321 + 120 = 441,可用发送窗口大小 = 200 - (441 - 241) = 0,可用窗口大小为0,这时无法再发送数据。
  5. 客户端接收到120字节数据,RCV.NXT = 321 + 120 = 441,发送确认报文。
  6. 服务端收到客户端的第一个确认报文,窗口往右”滑动“80字节,SND.UNA = 241 + 80 = 321, 可用发送窗口大小 = 200 - (441 - 321) = 80。
  7. 服务端收到客户端的第二个确认报文,窗口往右”滑动“120字节,SND.UNA = 321 + 120 = 441, 可用发送窗口大小 = 200 - (441 - 441) = 200。
  8. 服务端继续发送160个字节数据,SND.NXT = 441 + 160 = 601,可用发送窗口大小 = 200 - (601 - 441) = 40。
  9. 客户端接收到数据,RCV.NXT = 441 + 160 = 601,发送确认报文。
  10. 服务端收到确认报文,窗口右移160个字节,SND.UNA = 441 + 160 = 601,可用发送窗口大小 = 200 - (601 - 601) = 200。

上面的例子我们假定了发送窗口和接收窗口是不变的,但是实际上,发送窗口和接收窗口中所存放的字节数,都是放在操作系统内存缓冲区中的,而操作系统的缓冲区,会被操作系统调整。当应用进程没办法及时读取缓冲区的内容时,也会对我们的缓冲区造成影响。

我们来看下面这个例子:

img
img

  1. 客户端发送 140 字节数据后,可用窗口变为 220 (360 - 140)。
  2. 服务端收到 140 字节数据,但是服务端非常繁忙,应用进程只读取了 40 个字节,还有 100 字节占用着缓冲区,于是接收窗口收缩到了 260 (360 - 100),最后发送确认信息时,将窗口大小通过给客户端。
  3. 客户端收到确认和窗口通告报文后,发送窗口减少为 260。
  4. 客户端发送 180 字节数据,此时可用窗口减少到 80。
  5. 服务端收到 180 字节数据,但是应用程序没有读取任何数据,这 180 字节直接就留在了缓冲区,于是接收窗口收缩到了 80 (260 - 180),并在发送确认信息时,通过窗口大小给客户端。
  6. 客户端收到确认和窗口通告报文后,发送窗口减少为 80。
  7. 客户端发送 80 字节数据后,可用窗口耗尽。
  8. 服务端收到 80 字节数据,但是应用程序依然没有读取任何数据,这 80 字节留在了缓冲区,于是接收窗口收缩到了 0,并在发送确认信息时,通过窗口大小给客户端。
  9. 客户端收到确认和窗口通告报文后,发送窗口减少为 0。

可见最后窗口都收缩为 0 了,也就是发生了窗口关闭。我们再来看下面的例子:

当服务端系统资源非常紧张的时候,操心系统可能会直接减少了接收缓冲区大小,这时应用程序又无法及时读取缓存数据,会出现数据包丢失的现象。

img
img

  1. 客户端发送 140 字节的数据,于是可用窗口减少到了 220。
  2. 服务端因为现在非常的繁忙,操作系统于是就把接收缓存减少了 100 字节,当收到 对 140 数据确认报文后,又因为应用程序没有读取任何数据,所以 140 字节留在了缓冲区中,于是接收窗口大小从 360 收缩成了 100,最后发送确认信息时,通告窗口大小给对方。
  3. 此时客户端因为还没有收到服务端的通告窗口报文,所以不知道此时接收窗口收缩成了 100,客户端只会看自己的可用窗口还有 220,所以客户端就发送了 180 字节数据,于是可用窗口减少到 40。
  4. 服务端收到了 180 字节数据时,发现数据大小超过了接收窗口的大小,于是就把数据包丢弃了。
  5. 客户端收到服务端发送的确认报文和通告窗口报文,尝试减少发送窗口到 100,把窗口的右端向左收缩了 80,此时可用窗口的大小就会出现诡异的负值。

所以,如果发生了先减少缓存,再收缩窗口,就会出现丢包的现象。

为了防止这种情况发生,TCP 规定是不允许同时减少缓存又收缩窗口的,而是采用先收缩窗口,过段时间再减少缓存,这样就可以避免了丢包情况。

窗口关闭

如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。

窗口大小是通过ACK报文发送的,当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口的 ACK 报文在网络中丢失了,那么就会产生“死锁”:

img
img

发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,如不不采取措施,这种相互等待的过程,会造成了死锁的现象。

为了解决这个问题,TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。

如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。窗口探测的次数一般为 3 次,每次大约 30-60 秒(不同的系统版本实现可能会不一样)。如果探测 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发 RST 报文来中断连接。

img
img

糊涂窗口综合征

如果接收方太忙了,来不及取走接收窗口里的数据,那么就会导致发送方的发送窗口越来越小。

到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症

我们的 TCP + IP 头至少有 40 个字节,假如接收窗口只有几字节,那么就会出现大马拉小车的情况,造成资源浪费。

img
img

参考上图我们不难想象会有两种场景导致“大马拉小车”:

  • 接收端发送了一个接收窗口很小的ACK包;
  • 发送端任性的发送小数据包。

如何避免发送小窗口ACK包

当「窗口大小」小于 min( MSS,缓存空间/2 ) ,也就是小于 MSS 与 12 缓存大小中的最小值时,就会向发送方通告窗口为 0,也就阻止了发送方再发数据过来。

等到接收方处理了一些数据后,窗口大小 >= MSS,或者接收方缓存空间有一半可以使用,就可以把窗口打开让发送方发送数据过来。

如何防止发送端发送小数据包

使用 Nagle 算法,该算法的思路是延时处理,它满足以下两个条件中的一条才可以发送数据

  • 要等到窗口大小 >= MSS 或是 数据大小 >= MSS
  • 收到之前发送数据的 ack 回包

只要没满足上面条件中的一条,发送方就一直囤积数据,直到满足上面的发送条件。

另外,Nagle 算法默认是打开的,如果对于一些需要小数据包交互的场景的程序,比如,telnet 或 ssh 这样的交互性比较强的程序,则需要关闭 Nagle 算法。

可以在 Socket 设置 TCP_NODELAY 选项来关闭这个算法(关闭 Nagle 算法没有全局参数,需要根据每个应用自己的特点来关闭)

setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&value, sizeof(int));

拥塞控制

可能有的小伙伴会有疑问,前面既然有了流量控制,为什么还需要一个拥塞控制呢?其实细想我们不难发现,流量控制只是避免了发送方向接收方发送过多的数据,防止接收方无法及时响应。他们并不能感知到当前的网络状态是否拥堵。这就好比平时节假日出游时,我们总能遇到堵车的情况一样,因为在出门的时候我们没有查询当前的交通情况,刚好大部分人都选择在同一天出行,那显然就容易出现交通拥堵的情况了。

要解决这个问题也不难,只要大家在出门前先通过APP查询当前交通状况,如果发现交通已经比较拥挤了,那就选择改天再出门,选择错峰出行,那交通出现拥堵的概率就会降低。

TCP其实也是通过类似的方式去解决网络拥塞的,当网络发送拥塞时,TCP 会选择降低发送的数据量。

拥塞窗口

在介绍拥塞控制算法前先介绍一个概念,拥塞窗口(CWND)。它是发送方维护的一个 的状态变量,它会根据网络的拥塞程度动态变化,前面提到过发送窗口 swnd 和接收窗口 rwnd 是约等于的关系,那么由于入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。

  • 只要网络中没有出现拥塞,cwnd 就会增大
  • 网络中出现了拥塞,cwnd 就减小

接下来我们先解决第一个问题,TCP怎么知道当前的网络是否拥塞呢?

很简单,只要发送端没有在规定的时间内收到ACK应答报文(触发了超时重传),那么就认为网络出现了拥塞。

判断网络出现拥塞后,TCP就会执行下面四个算法:

  • 慢启动
  • 拥塞避免
  • 拥塞发生
  • 快速恢复
慢启动

TCP在连接建立之时,会通过慢启动的方式逐渐增加发送数据量。慢启动算法核心是:当发送方每收到一个 ACK,就拥塞窗口 cwnd 的大小就会加 1。假定拥塞窗口 cwnd 和发送窗口 swnd 相等。

  • 连接建立完成后,一开始初始化 cwnd = 1,表示可以传一个 MSS(MSS就是连接请求时在报头设置的字段)大小的数据。
  • 当收到一个 ACK 确认应答后,cwnd 增加 1,于是一次能够发送 2 个
  • 当收到 2 个的 ACK 确认应答后, cwnd 增加 2,于是就可以比之前多发2 个,所以这一次能够发送 4 个
  • 当这 4 个的 ACK 确认到来的时候,每个确认 cwnd 增加 1, 4 个确认 cwnd 增加 4,于是就可以比之前多发 4 个,所以这一次能够发送 8 个。

img
img

慢启动窗口是呈指数增长的,如果任由它一直增长下去毫无疑问网络会出现拥堵,所以TCP设置了一个阈值,它就是慢启动门限 ssthresh (slow start threshold)状态变量。

  • 当 cwnd < ssthresh 时,使用慢启动算法。
  • 当 cwnd >= ssthresh 时,就会使用「拥塞避免算法」。
拥塞避免算法

当拥塞窗口 cwnd 「超过」慢启动门限 ssthresh 就会进入拥塞避免算法。一般来说 ssthresh 的大小是 65535 字节。进入拥塞避免算法后,它的规则是:每当收到一个 ACK 时,cwnd 增加 1/cwnd。

接上前面的慢启动的例子,假定 ssthresh 为 8:

当 8 个 ACK 应答确认到来时,每个确认增加 1/8,8 个 ACK 确认 cwnd 一共增加 1,于是这一次能够发送 9 个 MSS 大小的数据,变成了线性增长

img
img

拥塞避免算法就是将原本慢启动算法的指数增长变成了线性增长。

随着发送量增大,网络慢慢就会出现拥塞,这时候就会出现丢包,进而触发重传。当触发重传机制,那么就会启动拥塞发生算法

拥塞发生

需要注意的是,上面我们介绍过两种重传机制:

  • 超时重传
  • 快速重传

这两种重传机制对应两种不同的异常:超时重传是网络出现拥堵,无法快速处理所有数据包,所以导致数据包“堵”在网络当中;而快速重传是由于网络异常从而导致某一个数据包“失踪”了,而网络实际是畅通的。

因此针对这两种异常,TCP同样选择两种算法处理:

拥塞发生<=>超时重传

  • ssthresh 设为 cwnd/2,
  • cwnd 重置为 1

img
img

从图中不难看出,这种算法呈现的是“断崖式下跌”(还好不是股市)。如果只采用这种拥塞发生算法的话,那么网络的波动就会非常大,体现在打游戏时突然延迟很高~~

接下来就介绍针对快速重传的拥塞发生算法——快速恢复,因为发送方还能连续收到3个ACK,说明网络情况还比较乐观。ssthresh 和cwnd 变化如下:

  • cwnd = cwnd/2 ,也就是设置为原来的一半;
  • ssthresh = cwnd;
快速恢复
  • 拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了)
  • 重传丢失的数据包
  • 如果再收到重复的 ACK,那么 cwnd 增加 1
  • 如果收到新数据的 ACK 后,设置 cwnd 为 ssthresh,接着就进入了拥塞避免算法

img
img

四次挥手说再见

img
img

MSL是Maximum Segment Lifetime的英文缩写,可译为“最长报文段寿命”,它是任何报文在网络上存在的最长的时间,超过这个时间报文将被丢弃。我们都知道IP头部中有个TTL字段,TTL是time to live的缩写,可译为“生存时间”,这个生存时间是由源主机设置初始值但不是存在的具体时间,而是一个IP数据报可以经过的最大路由数,每经过一个路由器,它的值就减1,当此值为0则数据报被丢弃,同时发送ICMP报文通知源主机。RFC793中规定MSL为2分钟,但这完全是从工程上来考虑,对于现在的网络,MSL=2分钟可能太长了一些。因此TCP允许不同的实现可根据具体情况使用更小的MSL值。TTL与MSL是有关系的但不是简单的相等关系,MSL要大于TTL。

在TIME_WAIT状态时两端的端口不能使用,要等到2MSL时间结束才可继续使用。当连接处于2MSL等待阶段时任何迟到的报文段都将被丢弃。不过在实际应用中可以通过设置SO_REUSEADDR选项达到不必等待2MSL时间结束再使用此端口。

当客户端没有东西要发送时就要释放连接,客户端会发送一个报文(没有数据),其中 FIN 设置为1, 服务端收到后会恢复确认,这时客户端的连接已经关闭,即客户端不再发送信息(但仍可接收信息)。 客户端收到确认后进入等待状态,等待服务端请求释放连接, 服务端数据发送完毕后就向客户端请求连接释放,同样是用FIN=1 表示, 并且用 ack = u+1(如图), 客户端收到后同样回复一个确认,并进入 TIME_WAIT 状态, 等待 2MSL 时间。

为什么需要等待?

因为服务端接收到释放连接的请求后会发送确认信息,这个确认信息是有可能丢失的,所以客户端就需要等待,如果超时还没收到确认,那么就会再次发送释放连接的请求。

为什么客户端在FIN-WAIT-2阶段发送ACK后还需要等待2MSL

1.为了保证客户端(我们记为A端)发送的最后一个ACK报文段能够到达服务器端。这个ACK报文段有可能丢失,因而使处在LASK—ACK端的服务器端(我们记为B端)收不到对已发送的FIN+ACK报文段。B会超时重传这个FIN+ACK报文段,而A就能在2MSL时间内收到这个重传的FIN+ACK报文段。接着A重传一次确认,重新启动2MSL计时器。最后,A和B都正常进入到CLOSED状态。如果A在TIME_WAIT状态不等待一段时间,而是在发送完ACK确认后立即释放连接,那么就无法收到B重传的FIN+ACK报文段,因而也不会再发送一次确认报文段,这样,B就无法正常进入CLOSED状态。

2.我们都知道,假如A发送的第一个请求连接报文段丢失而未收到确认,A就会重传一次连接请求,后来B收到了确认,建立了连接。数据传输完毕后,就释放了连接。A共发送了两个连接请求报文段,其中第一个丢失,第二个到达了B。假如现在A发送的第一个连接请求报文段没有丢失,而是在某些网络节点长时间都留了,以至于延误到连接释放后的某个时间才到达B,这本来是已失效的报文段,但B并不知道,就会又建立一次连接。而等待的这2MSL就是为了解决这个问题的,A在发送完最后一个确认报后,在经过时间2MSL,就可以使本链接持续时间内所产生的所有报文段都从网络中消失,这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段。