当前位置:   article > 正文

TCP 可靠传输机制详解

可靠传输机制

目录

TCP协议的特点

TCP 报文段

TCP "三次握手"

TCP "四次挥手"

客户端和服务器端所经历的状态

TCP 可靠传输

TCP流量控制

TCP拥塞控制

面试相关问题


前言

本篇博文主要是为了复习 TCP 协议而做的总结。其中很多内容都是来自于《计算机网络》,《Linux网络编程》,《TCP/IP详解》等书籍。首先可以从 TCP 协议思维导图看到本文的大致内容。虽然有很多内容,但 "三次握手""四次挥手"可靠传输机制 才是本文的重点,其中会用 wireshark 抓取相应的包用于协议分析。最后也会总结一些面试常问的问题。TCP 协议思维导图如下:

TCP协议的特点

TCP 是在不可靠的网络层之上实现的可靠的数据传输协议,它主要解决传输的可靠、有序、无丢失和不重复的问题。TCP 的主要特点有:

(1) TCP 是面向连接的传输层协议。

(2) 每一条 TCP 连接只能有两个端点,每一条 TCP 连接只能是点对点的(一对一)。

(3) TCP 提供可靠的交付服务,保证传送的数据无差错、不丢失、不重复且有序。

(4) TCP 提供全双工通信,TCP 允许通信双方的应用进程在任何时候都能发送数据,为此 TCP 连接的两端都设有发送缓存和接收缓存,用来临时存放双向通信的数据。

发送缓存用来暂存以下数据:①应用程序传送给发送方 TCP 准备发送的数据;② TCP 已发送出但尚未收到的确认的数据。

接收缓存用来暂存以下数据:①按序到达的但尚未被接收应用程序读取的数据;②不按序到达的数据。

(5) TCP 是面向字节流的,虽然应用程序和 TCP 的交互是一次一个数据块,但 TCP 把应用程序交下来的数据看成仅仅是一连串的无结构的字节流。

TCP 报文段

TCP 传输的数据单元称为报文段。一个 TCP 报文段分为 TCP 首部和 TCP 数据两部分,整个 TCP 段作为 IP 数据报的数据部分封装在 IP 数据报中。

各字段的含义如下:

(1) 源端口和目的端口:各占 2 个字节。端口是运输层与应用层的服务接口。运输层的复用和分用功能都要通过端口才能实现。

(2) 序号:占 4 个字节。TCP是面向字节流的,所以 TCP 连接中传送的数据流中的每一个字节都编上一个序号。序号字段的值则指的是本报文段所发送的数据的第一个字节的序号。例如,一个报文段的序号字段值是 200,携带的数据总共有 100 字节,表明这个报文段的数据的最后一个字节的序号是 299,所以下一个报文段的数据序号应从 300 开始。

(3) 确认号:占 4 个字节,是期望收到对方的下一个报文段的数据的第一个字节的序号。若确认号为 N,表明前 N-1 的所有数据都已经正确接收。例如,B 正确的收到了 A 发送过来的一个报文段,其序号字段是指 501,而数据长度是 200 字节(序号 501~700),表明 B 正确的收到了 A 发送的序号 700 之前的数据。因此 B 希望收到 A 的下一个数据序号是 701,所以 B 在发送给 A 的确认报文段中应把确认号设置成 701。

(4) 数据偏移量:占 4 位,这里不是 IP 数据报分片的那个数据偏移,而是表示首部长度,它指出 TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远。该字段若为 15,则表明 TCP 首部达到最大长度 60 字节(以 4 字节为计算单位乘以 15)。

(5) 保留:占 6 位,目前不使用,所以置为 0。

(6) 紧急位 URG:当其值为 1 时,表明紧急指针字段有效。它会告诉系统此报文段中有紧急数据,应该尽快传送。但是 URG 需要和紧急指针配套使用,即从第一个字节到紧急指针所指字节就是紧急数据。

(7) 确认位 ACK:只有当 ACK = 1 时,确认号字段才有效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置为 1。

(8) 推送位 PSH:TCP 收到 PSH = 1 的报文段,就尽快的交付接收应用进程,而不再等到整个缓存都填满了后再向上交付。

(9) 复位位 RST:当 RST = 1 时,表明 TCP 连接中出现了严重的错误,必须释放连接,然后再重新建立运输连接。

(10) 同步位 SYNSYN = 1表示这是一个连接请求或连接接收报文。当 SYN = 1,ACK = 0 时,表明这是一个连接请求报文,对方若同意建立连接,则在响应报文中使用 SYN = 1,ACK = 1。

(11) 终止位 FIN:用来释放一个连接。FIN = 1 表明此报文段的发送方的数据已经发送完毕,并要求释放传输连接。

(12) 窗口:占 2 个字节。它指出现在允许对方发送的数据量,接收方的数据缓存空间是有限的,所以使用窗口值作为接收方让发送方设置其发送窗口的依据,单位是字节。例如,设确认号是 101,窗口字段是 1000。这就表明,从 101 号开始,发送此报文段的一方还有接收 1000 字节数据(字节序号是 101~1100)的接收缓存空间。

(13) 校验和:占 2 个字节。它的检验范围包括首部和数据部分。计算时要在 TCP 报文段前面加上 12 字节的伪首部。

(14) 紧急指针:占 16 位,指出在本报文段中紧急数据共有多少个字节。

(15) 选项:长度可变。TCP 最初只规定了一种选项,即最大报文段长度(MSS)。

(16) 填充:这是为了使整个首部长度是 4 字节的整数倍。

主要需要理解以下几个字段:

源端口号和目标端口号:谁发的和发给谁(你是谁?从哪里来?到哪里去?)

序号:为了解决乱序问题;

确认序号:发出去的包应该有确认,没有收到就应该重新发送,直到送达;

状态位:常见的有 SYN、ACK、FIN,分别表示发起一个连接、确认和结束连接;

窗口大小:用于 TCP 流量控制,通信双方各声明一个窗口,说明自己当前的处理能力发送的太快,处理不了,发的太慢,影响发送的效率

TCP "三次握手"

第一次握手:客户端首先向服务器发送一个连接请求报文段。这个特殊的报文段中不含应用层数据,其首部中的 SYN 标志位被置为 1。另外,客户端会随机选择一个起始号 seq = x (连接请求报文不携带数据,但要消耗掉一个序号)客户端进程进入 SYN_SENT(同步发送)状态。 

第二次握手:服务器收到连接请求报文段后,如同意建立连接,就向客户端发回确认,并为该 TCP 连接分配 TCP 缓存和变量。在确认报文段中,SYN 和 ACK 位都被置为 1,确认号字段的值为 x+1,并且服务器端随机产生起始序号 seq = y(确认报文不携带数据,但也要消耗掉一个序号)。确认报文段同样不包含应用层数据。服务端进入SYN_RCVD(同步接收)状态。

第三次握手:当客户端收到确认报文段后,还要向服务器给出确认,并且也要给该连接分配缓存和变量。这个报文段的 ACK 标志位被置为 1,序号字段为 x+1,确认号字段为 ack = y+1。该报文可以携带数据,如果不携带数据则不消耗序号。客户端进入 ESTABLISHED(建立连接)状态。

在成功的完成以上三步后,TCP 连接就建立了,接下来就可以传送应用层数据了。TCP 提供的是全双工通信,因此通信双方的应用进程在任何时候都能发送数据。注意:服务器端的资源是在完成第二次握手时开始分配的,所以服务器易于受到 SYN 洪泛攻击。

TCP "三次握手" 报文详细分析

  1. TCP报文:[29 05 01 bb f4 0a 2c 77 1f a9 79 d2 50 10 01 00 b9 84 00 00]
  2. 为了更易理解:[29 05 01 bb]
  3.             [f4 0a 2c 77]
  4. [1f a9 79 d2]
  5. [50 10 01 00]
  6. [b9 84 00 00]
  7. 源端口:2905(HEX) = 10501(DEC)
  8. 目的端口:01bb(HEX) = 443(DEC)
  9. 序号:1 为什么连接过程中的"序号"显示1,而相应字段的值不是1。详情请看面试问题的(6)
  10. 确认号:1
  11. 十六进制[50 10]转化成二进制[0101 0000 0001 0000]
  12. 数据偏移(占4位):单位是4个字节,此字段值为5,所以TCP首部的长度是20字节。
  13. 保留(占6位):保留为今后使用,目前置为0
  14. 紧急位URG:0
  15. 确认位ACK:1
  16. 推送位PSH:0
  17. 复位位RST:0
  18. 同步位SYN:0
  19. 终止位FIN:0
  20. 窗口:0100(HEX) = 256(DEC)
  21. 检验和:0xb984
  22. 紧急指针:0

TCP "四次挥手"

参与 TCP 连接的两个进程中的任何一个都能终止该连接。

第一步:若客户端打算关闭连接,就向其服务器发送一个连接释放报文段,并停止再发送数据,主动关闭 TCP 连接,该报文段的 FIN 标志位被置为 1,seq = u,它等于前面已传送过的数据的最后一个字节的序号加 1(FIN 报文段即使不携带数据,也要消耗掉一个序号)。TCP 是全双工的,即可以想象成是一条 TCP 连接上有两条数据通路。当发送 FIN 报文时,发送 FIN 的一端就不能再发送数据,也就是关闭了其中一条数据通路,但另一条没有关闭的数据通路仍可以发送数据。

第二步:服务器收到连接释放报文段后就会发出确认,确认号为 ack = u+1,而这个报文段自己的序号是 v,等于它前面已传送过的数据的最后一个字节的序号加 1。此时,从客户端到服务器这个方向的连接就释放了,TCP 连接处于半关闭状态。但服务器若发送数据,客户端仍能接收,即从服务器到客户端这个方向的连接并未关闭。

第三步:若服务器已经没有要向客户端发送的数据,就释放连接,此时发出 FIN = 1 的连接释放报文段。

第四步:客户端收到连接释放报文段后,必须发出确认。在确认报文段中,ACK 字段被置为 1,确认号 ack = w+1,序号 seq = u+1。此时 TCP 连接还没有释放掉,必须经过 2MSL 后,客户端才进入连接关闭状态。

客户端和服务器端所经历的状态

结合连接和释放过程图看状态转换图,以下是客户端的状态转换图:

服务器端的状态转换图:

完整的状态转换图(红线是客户端,蓝线是服务器,它们表示正常的连接和断开的过程):

断开过程中的状态解析

(1) FIN_WAIT_1:  FIN_WAIT_1 和 FIN_WAIT_2 状态的真正含义都是表示等待对方的 FIN 报文。而这两种状态的区别是:  FIN_WAIT_1 状态实际上是当 socket 在 ESTABLISHED 状态时,它想主动关闭连接,向对方发送了 FIN 报文,此时该 socket  即进入到 FIN_WAIT_1 状态。而当对方回应 ACK 报文后,则进入到 FIN_WAIT_2 状态。当然在实际的正常情况下, FIN_WAIT_1 状态一般是比较难见到的(这个过程比较快),而 FIN_WAIT_2 状态还有时常常可以用 netstat 看到(因为有一方还在发数据)。(主动关闭连接)

(2) FIN_WAIT_2:上面已经详细解释了这种状态,实际上 FIN_WAIT_2 状态下的 socket 表示半连接,即有一方要求 close 连接,但另外一方告诉对方,我暂时还有点数据需要传送给你(发送 ACK 报文),稍后再关闭连接。(主动关闭连接)

(3) TIME_WAIT:表示收到了对方的 FIN 报文,并发送出了 ACK 报文,就等 2MSL 后即可回到 CLOSED 可用状态了。如果 FIN_WAIT_1 状态下,收到了对方同时带 FIN 标志和 ACK 标志的报文时(被动方没有数据发送),可以直接进入到 TIME_WAIT 状态,而无须经过 FIN_WAIT_2 状态。(主动关闭连接)

(4) CLOSING(比较少见): 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送 FIN 报文后,按理来说是应该先收到(或同时收到)对方的 ACK 报文,再收到对方的 FIN 报文。但是 CLOSING 状态表示你发送 FIN 报文后,并没有收到对方的 ACK 报文,反而却也收到了对方的 FIN 报文。什么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时关闭一个 socket 的话,那么就出现了双方同时发送 FIN 报文的情况,也即会出现 CLOSING 状态,表示双方都正在关闭 socket 连接。

(5) CLOSE_WAIT: 被动方需要查看是否还有数据发送给对方,如果没有的话,那么你也就可以关闭这个 socket,发送 FIN 报文给对方,即关闭连接。所以在 CLOSE_WAIT 状态下,需要完成的事情是等待被动方去关闭连接。(被动方)

(6) LAST_ACK: 它是被动关闭的一方在发送 FIN 报文后,最后等待对方的 ACK 报文。当收到 ACK 报文后,也即可以进入到 CLOSED 可用状态了。(被动方)

(7) CLOSED: 表示连接中断。

小总结:

半关闭状态的含义是:通信的一端可以发送 FIN 报文段给对方,告诉它本端已经完成了数据的发送,但允许继续接收来自对方的数据,直到对方也发送 FIN 报文段以关闭连接。

当客户端处于 FIN_WAIT_2 状态时,服务器处于 CLOSE_WAIT 状态,这时可能会处于半连接状态,即服务器还有数据发给客户端。如果服务器没有数据发送,则发送 FIN 报文段关闭连接,而客户端将确认并进入 TIME_WAIT 状态。

上面的状态转换图还给出了 FIN_WAIT_1 状态直接进入 TIME_WAIT 状态的一条线路,前提是处于 FIN_WAIT_1 的客户端直接收到了带有确认信息的 FIN 报文段(而不是正常情况下的先收到 ACK 报文段,再收到 FIN 报文段)。

断开连接时的意外

当 TCP 连接发生一些物理上的意外情况时,例如网线断开,linux 上的 TCP 实现会依然认为该连接有效,而 windows 则会在一定时间后返回错误信息。这似乎可以通过设置 SO_KEEPALIVE 选项来解决,不过不知道这个选项是否对于所有平台都有效。

TIME_WAIT 状态所带来的影响

当某个连接的一端处于 TIME_WAIT 状态时,该连接将不能再被使用。事实上,对于我们比较有现实意义的是,这个端口将不能再被使用。某个端口处于 TIME_WAIT 状态(其实应该是这个连接)时,这意味着这个 TCP 连接并没有断开(完全断开),那么,如果你 bind 这个端口,就会失败。对于服务器而言,如果服务器突然 crash 掉了,那么它将无法再 2MSL 内重新启动,因为 bind 会失败。解决这个问题的一个方法就是设置 socket 的 SO_REUSEADDR 选项。这个选项意味着你可以重用一个地址。

注意:当建立一个 TCP 连接时,服务器端会继续用原有端口监听,同时用这个端口与客户端通信。而客户端默认情况下会使用一个随机端口与服务器端的监听端口通信。有时候,为了服务器端的安全性,我们需要对客户端进行验证,即限定某个 IP 某个特定端口的客户端。客户端可以使用 bind 来使用特定的端口。对于服务器端,当设置了 SO_REUSEADDR 选项时,它可以在 2MSL 内启动并 listen 成功。 但是对于客户端,当使用 bind 并设置 SO_REUSEADDR 时,如果在 2MSL 内启动,虽然 bind 会成功,但是在 windows 平台上 connect 会失败。而在 linux 上则不存在这个问题。

TCP 可靠传输

TCP 的任务是在 IP 层的不可靠、尽力而为服务的基础上建立一种可靠数据传输服务。TCP 提供的可靠数据传输服务就是要保证接收方进程从缓冲区读出的字节流与发送方发出的字节流是完全一样的。TCP 使用了校验、序号、确认和重传机制来达到这个目的。

校验

在计算校验和时,要在 TCP 数据报之前增加 12 个字节的伪首部,伪首部并不是 TCP 报文段真正的首部。只是在计算校验和时,临时添加在 TCP 数据报文段的前面,得到一个临时的 TCP 报文段。伪首部既不向下传送也不向上递交,而仅仅是为了计算校验和。注意:IP 数据报的校验和只检验 IP 数据报的首部,但 TCP 的校验和会把首部和数据部分一起检验。

序号

TCP 首部的序号字段用来保证数据能有序提交给应用层,TCP 把数据看成一个无结构但是有序的字节流,而序号是建立在传送的字节流之上,而不是建立在报文段之上。TCP 连接中传送的数据流中的每一个字节都编上一个序号。序号字段的值则指的是本报文段所发送的数据的第一个字节的序号。

确认

TCP 首部的确认号是期望收到对方的下一个报文段的数据的第一个字节的序号。TCP 默认使用累计确认,即 TCP 只确认数据流中至第一个丢失字节为止的字节。例如,接收方 B 收到了发送方 A 发送的包含字节 0~2 和 6~7 的报文段。由于某些原因,B 还没有收到字节 3~5 的报文段,此时 B 仍在等待字节 3(和其后面的字节),因此,B 到 A 的下一个报文段将确认号字段设置为 3。

重传

有两种事件会导致 TCP 对报文段进行重传:超时冗余 ACK

(1) 超时

TCP 每发送一个报文段,就对这个报文段设置一次计时器。只要计时器设置的重传时间到期但还没有收到确认,就要重传这一报文段。当检测到接收数据有错误时,会采取直接丢弃出错的数据,发送端等待接收端的确认超时后,会自动重发该报文段。

由于 IP 数据报在传输的时候选择的路由变化很大,因此传输层的往返时延的方差很大。为了计算超时计时器的重传时间,TCP 采用一种自适应算法,它记录一个报文段发出的时间,以及收到相应确认的时间,这两个时间之差叫做报文段的往返时间(RTT)。TCP 保留了 RTT 的一个加权平均往返时间 RTTs,当第一次测量 RTT 样本时,RTTs 值就为所测量到的 RTT 样本的值,但之后每测量一个新的 RTT 样本,就按下式重新计算一次 RTTs:

新的 RTTs = ( 1- a ) * (旧的 RTTs) + a(新的RTT样本)

在上式中 0 <= a < 1。若 a 很接近于零,表示新的 RTTs 值和旧的 RTTs 值相比变化不大,而受新的 RTT 样本影响不大(RTT值更新较慢)。若 a 接近于 1,则表示新的 RTTs 值受新的 RTT 样本的影响较大(RTT值更新较快)。[RFC 2988] 推荐的 a 值为 0.125。

所以超时计时器设置的超时重传时间(RTO)应略大于上面得出的加权平均往返时间 RTTs。即 RTO = RTTs + 4RTTd。其中 RTTd 是 RTT 的偏差的加权平均值,它与 RTTs 和新的 RTT 样本之差有关。当第一次测量时,RTTd 取为测量到的 RTT 样本值的一半,以后测量中,使用下式计算:新的 RTTd = (1-β) *(旧的 RTTd) + β*|RTTs - 新的 RTT 样本|,其中 β 是个小于 1 的系数,它的推荐值是 0.25。

(2) 冗余 ACK

超时触发重传存在的一个问题就是超时周期往往太长。幸运的是,发送方通常可在超时事件发生之前通过注意冗余 ACK 来较好地检测丢包情况。冗余 ACK 就是再次确认某个报文段的 ACK,而发送方先前已经收到过该报文段的确认。例如,发送方 A 发送了序号为 1、2、3、4、5 的 TCP 报文段,其中 2 号报文段丢失了,它将无法到达接收方 B。因此,3、4、5 号报文段对于 B 来说就成了失序报文段。TCP 规定每当比期望序号大的失序报文段到达时,发送冗余 ACK,指明下一个期望字节的序号 [RFC 1122, RFC 2581]。在这个例子中,3、4、5 号报文到达 B,但它们不是 B 所期待的下一个报文,于是 B 就发送 3 个对 1 号报文段的冗余 ACK,表示自己期望接收 2 号报文段。TCP 规定当发送方收到对同一个报文段的 3 个冗余 ACK 时,就可以认为跟在这个被确认报文段之后的报文段已经丢失。就前面的例子而言,当 A 收到对于 1 号报文段的 3 个冗余 ACK 时,则它可以认为 2 号报文段已经丢失。这时发送方 A 可以立即对 2 号报文执行重传,这种技术成为快速重传

TCP流量控制

TCP 提供了流量控制服务以消除发送方使接收方缓存区溢出的可能性,因此 TCP 流量控制是为了匹配发送方的发送速率与接收方的读取速率。TCP 提供一种基于滑动窗口协议的流量控制机制。其原理是:在通信过程中,接收方根据自己接收缓存的大小,动态地调整发送方的发送窗口大小,这就是接收窗口 rwnd,即调整 TCP 报文段首部中的 "窗口" 字段的值,来限制发送方向网络注入报文的速率。同时,发送方根据其对当前网络拥塞程度的估计而确定窗口值,称为拥塞窗口 cwnd,其大小与网络的带宽和时延密切相关。

例如,在通信中,有效数据只从 A 发往 B,而 B 仅向 A 发送确认报文,这时,B 就可以通过设置确认报文段首部的窗口字段来将 rwnd 值来限制自己发送窗口的大小,这样可以将未确认的数据量控制再 rwnd 大小之内,保证了 A 不会使 B 得接收缓存溢出。A 的发送窗口的实际大小取的是 rwnd 和 cwnd 中的最小值。

设主机 A 向主机 B 发送数据,在连接建立时,B 告诉 A:"我的接收窗口 rwnd = 400(字节)"。

传输层和数据链路层的流量控制的区别在于:传输层定义了端到端用户之间的流量控制,数据链路层定义了两个中间的相邻结点的流量控制。另外,数据链路层滑动窗口协议的窗口大小不能动态变化,传输层则可以动态变化。

TCP拥塞控制

所谓的拥塞控制就是为了防止过多的数据注入网络中,这样可以使网络中的路由器或链路不会过载。当出现拥塞时,端点并不能了解到拥塞发生的细节,对通信连接的端点来说,拥塞往往表现为通信时延的增加。拥塞控制和流量控制相似的地方是通过控制发送方发送数据的速率来达到效果。

拥塞控制与流量控制的区别:拥塞控制是让网络能够承受现有的网络负荷,它是一个全局性的过程,涉及所有的主机、所有的路由器,以及与降低网络传输性能有关的所有因素。而流量控制往往使指点对点通信量的控制,即接收端控制发送端,它所做的就是抑制发送端发送数据的速率,以便使接收端来得及接收。

为了更好地对传输层进行拥塞控制,有以下四种算法:慢开始、拥塞避免、快重传、快恢复。发送方在确定发送报文段的速率时,既要根据接收方的接收能力,又要从全局考虑不要使网络发生拥塞。因此,TCP 协议要求发送方维护以下两个窗口:

(1) 接收窗口 rwnd,接收方根据目前接收缓存大小所许诺的最新的窗口值,反映了接收方的容量。有接收方根据其放在 TCP 报文的首部的 "窗口" 字段通知发送方。

(2) 拥塞窗口 cwnd,发送方根据自己估算的网络拥塞程度而设置的窗口值,反映了网络当前容量。只要网络没有出现拥塞,拥塞窗口就再增大一些,以便把更多的分组发送出去。但只要网络出现拥塞,拥塞窗口就减少一些,以减少注入网络中的分组数。发送窗口的上限值应当取接收窗口 rwnd 和拥塞窗口 cwnd 中较小的一个,即:Min[rwnd, cwnd]。

慢开始和拥塞避免

(1) 慢开始算法

在 TCP 刚刚连接好,开始发送 TCP 报文段时,先让拥塞窗口 cwnd = 1,即一个最大报文段长度 MSS。而在每收到一个对新的报文段的确认后,将 cwnd 加倍,即刚开始会增大一个 MSS。用这样的方法逐步增大发送方的拥塞窗口 cwnd,可以使分组注入到网络的速率更加合理。例如,A 向 B 发送数据,当发送时 A 的拥塞窗口为 2,那么 A 一次可以发送两个 TCP 报文段,当经过一个 RTT 后,A 收到 B 对刚才两个报文的确认,于是就把拥塞窗口调整为 4,即下一次发送时就可以发送 4 个报文段。

使用慢开始算法后,每经过一个传输轮次,拥塞窗口 cwnd 就会加倍,即 cwnd 的大小呈指数形式增长。这样慢开始一直把拥塞窗口 cwnd 增大到一个规定的慢开始门限 ssthresh(阈值),然后改用拥塞避免算法。

(2) 拥塞避免算法

拥塞避免算法的做法是:发送端的拥塞窗口 cwnd 每经过一个往返时延 RTT 就增加一个 MSS 的大小,而不是加倍,使 cwnd 按线性规律缓慢增长(即加法增大),而当出现一次超时(网络拥塞)时,会令慢开始门限 ssthresh 等于当前 cwnd 的一半(即乘法减小)。

根据 cwnd 的大小执行不同的算法,可归纳为:

当 cwnd < ssthresh 时,使用慢开始算法。

当 cwnd > ssthresh 时,改用拥塞避免算法。

当 cwnd = ssthresh 时,既可以使用慢开始也可以使用拥塞避免算法。

(3) 网络拥塞的处理

当网络出现拥塞时,无论是在慢开始阶段还是在拥塞避免阶段,只要发送方检测到超时事件的发生(没有按时收到确认,重传计时器超时),就把慢开始门限 ssthresh 设置为出现拥塞时的发送窗口 cwnd 值的一半(但不能小于2)。然后把拥塞窗口 cwnd 重新设置为 1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够的时间把队列中积压的分组处理完。拥塞避免是指在拥塞避免阶段把拥塞窗口控制为线性规律增长,使网络比较不容易出现拥塞,利用以上措施想要完全避免网络拥塞是不可能的。

1. 初始时,拥塞窗口置为 1,即 cwnd = 1,慢开始门限置为 16,即 ssthresh = 16。慢开始阶段,cwnd 初值为 1,以后发送方每收到一个确认 ACK,cwnd 值加倍,即经过每个传输轮次(RTT),cwnd 呈指数规律增长。

2. 当拥塞窗口 cwnd 增长到慢开始门限 ssthresh 时(即当 cwnd = 16 时),就改用拥塞避免算法,cwnd 按线性规律增长。

3. 若此时 cwnd = 24 时,网络发生拥塞,更新 ssthresh 值为 12(即变为超时的时侯 cwnd 值的一半),cwnd 重置为 1,并执行慢开始算法,当 cwnd = 12 时,改为拥塞避免算法。

注意,在慢开始阶段,若 2*cwnd > ssthresh,则下一个 RTT 的 cwnd 应等于 ssthresh,而不是 2*cwnd,即 cwnd 不能越过 ssthresh 值。如上图,在 16 个轮次时,cwnd = 8、ssthresh = 12,第 17 轮次时,cwnd = 12,而不是 16。

快重传和快恢复

(1) 快重传

TCP 可靠传输机制中,快速重传技术使用了冗余 ACK 来检测丢包的发生。同样,冗余 ACK 也用于网络拥塞的检测。快重传并非取消重传计时器,而是在某些情况下可更早的重传丢失的报文段。当发送方连续收到三个重复的 ACK 报文时,直接重传对方尚未收到的报文段,而不必等待那个报文段设置的重传计时器超时。

如果收到 3 个相同的 ACK。TCP 在收到乱序到达包时就会立即发送 ACK,TCP 利用 3 个相同的 ACK 来判定数据包的丢失,此时进行快速重传,快速重传做的事情有:

1. 把 ssthresh 设置为 cwnd 的一半。

2. 把 cwnd 再设置为 ssthresh 的值(具体实现有些为 ssthresh + 3)。

3. 重新进入拥塞避免阶段。

(2) 快恢复

后来的 "快速恢复" 算法是在上述的 "快速重传" 算法后添加的,当收到 3 个重复 ACK 时,TCP 最后进入的不是拥塞避免阶段,而是快速恢复阶段。快速重传和快速恢复算法一般同时使用。快速恢复的思想是 "数据包守恒" 原则,即同一个时刻在网络中的数据包数量是恒定的,只有当 "老" 数据包离开了网络后,才能向网络中发送一个 "新" 的数据包,如果发送方收到一个重复的 ACK,那么根据 TCP 的 ACK 机制就表明有一个数据包离开了网络,于是 cwnd 加 1。如果能够严格按照该原则那么网络中很少会发生拥塞,事实上拥塞控制的目的也就在修正违反该原则的地方。

具体来说快速恢复的主要步骤是:

1. 当收到 3 个重复ACK时,把 ssthresh 设置为 cwnd 的一半,把 cwnd 设置为 ssthresh 的值加 3(有的实现版本不加 3),然后重传丢失的报文段,加 3 的原因是因为收到 3 个重复的ACK,表明有 3 个 "老" 的数据包离开了网络。

2. 再收到重复的 ACK 时,拥塞窗口增加 1。

3. 当收到新的数据包的 ACK 时,把 cwnd 设置为第一步中的 ssthresh 的值。原因是因为该 ACK 确认了新的数据,说明从重复 ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态。

快速重传算法首次出现在 4.3BSD 的 Tahoe 版本,快速恢复首次出现在 4.3BSD 的 Reno 版本,也称之为 Reno 版的 TCP 拥塞控制算法。

在流量控制中,发送方发送数据的量由接收方决定,而在拥塞控制中,有发送方自己通过检测网络状况而决定。实际上,慢开始、拥塞避免算法、快重传和快恢复集中算法应该是同时应用在拥塞控制机制之中的,当发送方检测到超时的时候就采用慢开始和拥塞避免,当发送方接收到冗余 ACK 的时候就采用快重传和快恢复。

面试相关问题

(1) 为什么四次挥手发送最后一次报文后要等待 2MSL(报文最大生存时间)的时间?

答:1) 为了保证 A 发送的最后一个确认报文段能够到达 B。如果 A 不等待 2MSL,若 A 返回的最后确认报文段丢失,则 B 不能进入正常关闭状态,而 A 此时已经关闭,也不可能再重传。

2) 防止出现 "已失效的连接请求报文段"。A 在发送完最后一个确认报文段后,再经过 2MSL 可保证本连接持续的时间内所产生的所有报文段从网络中消失。

(2) TCP 使用的是 GBN(后退N帧协议)还是 SR(选择重传协议)?

答:这是为了人踩坑而出的问题。因为 TCP 使用累计确认,看起来像是 GBN。但是,正确收到但失序的报文并不会被丢弃,而是缓存起来,并且发送冗余 ACK 指明希望收到的下一个报文段,这是 TCP 方式和 GBN 的显著区别。例如,A 发送了 N 个报文段,其中第 k(k < N)个报文段丢失,其余 N-1 个报文段正确地按序到达接收方 B。当使用 GBN 时,A 需要重传分组 k,以及所有后继分组 k+1,k+2,...,N。相反,TCP 却最多重传一个报文段,即报文段 k。另外,TCP 中提供一个 SACK(Selective ACK)选项,也就是选择确认选项。当使用选择确认选项的时候, TCP 看起来就和 SR 非常相似。因此,TCP 的差错恢复机制可以看成是 GBN 和 SR 协议的混合体。

(3) 为什么超时时间发生时 cwnd 被置为 1,而收到 3 个冗余 ACK 时 cwnd 只是减半?

答:首先应分析那种情况的网络拥塞程度更严重。其实不难发现,在收到 3 个冗余 ACK 的情况下,网络虽然拥塞,但至少 ACK 报文段能够被正确交付。而当超时发生时,说明网络可能已经拥塞的连 ACK 报文段都传输不了了,发送方只能等待超时后重传数据。因此,超时时间发生时,网络拥塞更严重,所以发送方应该最大限度地抑制数据发送量,所以 cwnd 置为 1;收到 3 个冗余 ACK 时,网络拥塞相对而言不是很严重,所以 cwnd 减半即可。

(4) 为什么不采用 "两次握手" 建立连接?

答:这主要是为了防止两次握手情况下已失效的连接请求报文段突然有传送到服务端,而产生了错误。考虑以下情况:客户 A 想服务器 B 发送 TCP 连接请求,第一个连接请求报文在网络的某个结点长时间滞留,A 超时后认为报文丢失,于是再重传一次连接请求,B 收到后建立连接。数据传输完毕后双方断开连接。此时,前一个滞留在网络中的连接请求到达了服务端 B,而 B 认为 A 有发来连接请求,此时若是使用 "三次握手",则 B 向 A 返回确认报文段,由于是一个失效的请求,因此 A 不予理睬,建立连接失败。若采用的是 "两次握手",则这种情况下 B 认为传输连接已经建立,并一直等待 A 传输数据,而 A 此时并无连接请求,因此不予理睬,这样就造成了 B 的资源白白浪费了。

(5) 是否 TCP 和 UDP 都需要计算往返时间 RTT?

答:往返时间 RTT 只是针对传输层 TCP 协议才很重要,因为 TCP 要根据 RTT 的值来设置超时计时器的超时时间。UDP 没有确认和重传机制,因此 RTT 对 UDP 没有什么意义。

(6) 为什么 TCP 在建立连接的时候不能每次选择相同的、固定的初始序号?

答:1) 假如 A 和 B 频繁地建立连接,传送一些 TCP 报文段后再释放连接,然后又不断的建立新的连接、传送报文段和释放连接。

2) 假如每一次建立连接时,主机 A 都选择相同的、固定的初始序号,如 1。

3) 若主机 A 发送出的某些 TCP 报文段在网络中会滞留较长的时间,以致造成主机 A 超时重传这些 TCP 报文段。

4) 若有一些在网络中滞留时间较长的 TCP 报文段最后终于到达了主机 B,但这时传送该报文段的那个连接早已释放了,而在到达主机 B 时的 TCP 连接是一条新的 TCP 连接。

以上这些情况可能会导致在新的 TCP 连接中的主机 B 有可能会接收在旧的连接传送的、已经没有意义的、过时的 TCP 报文段(因为这个 TCP 报文段的序号有可能正好处于新的连接所使用的序号范围内)。因为必须使得迟到的 TCP 报文段的序号不在新的连接中使用的序号范围内。所以,TCP 在建立新的连接时所选择的初始序号一定要和前面的一些连接所使用过的序号不一样。因此,不同的 TCP 连接不能使用相同的初始序号。

(7) 在使用 TCP 传输数据时,如果有一个确认报文段丢失了,也不一定会引起与该确认报文段对应的数据的重传。试说明理由。

答:这是因为发送方可能还未重传时,就收到了更高序号的确认。例如主机 A 连续发送两个报文段,均正确到达主机 B。B 连续发送两个确认 ACK1 和 ACK2(ACK2 的序号比 ACK1 的序号高)。但前一个确认帧在传输时丢失了。若在超时前,ACK2 被 A 接收,更高的序号代表该序号之前的所有字节都被接收了,所以 A 知道前一个报文也被正确的接收了,这种情况下 A 不会重传第一个报文段。

参考:《计算机网络》《Linux高性能服务器编程》

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/639221
推荐阅读
相关标签
  

闽ICP备14008679号