nEo眼中的TCP

你知道越多,你不知道的也就越多

The more you know, the more you know you don't know.

  在计算机网络中,协议占据了非常重要的一部分,TCP、UDP、IP、ARP 等等...好像学了很多,没有对常见问题进行一个汇总,本文用于复习 TCP 常见的几个知识。这篇文章我想由浅到深地过一遍 TCP,不是生硬的搬出各个知识点,从问题入手,然后从发展、演进的角度来看 TCP。今天我就大致的浅显地说一说我对 TCP 这些要点的理解。

TCP 是用来解决什么问题的?

TCP(Transmission Control Protocol)传输控制协议,重点在于这个控制

  控制什么?不仅要控制可靠、按序地传输以及端对端之间的流量控制。还需要加个拥塞控制,为整体网络的情况考虑。

为什么要 TCP,IP 层实现控制不行吗?

  我们知道网络是分层实现的,网络协议的设计就是为了通信,从链路层到 IP 层已经实现了任意两台主机的通信,为什么需要 TCP 呢?直接在通信的时候进行控制不就好了。

  之所以单独提取一个传输层,设计 TCP/UDP 协议来实现控制是因为 IP 层涉及到的设备更多,一条数据在网络上传输需要经过许多设备,而设备之间需要靠 IP 来寻址。

  假设在 IP 层实现了控制,那是不是涉及到的设备都需要关心很多事情?整体传输效率就会大打折扣。首先我们都知道,数据在网络中是以一个一个的数据包进行传输的。例如,假设 A 要传输一个信息给 F,但是无法直接传输到,需要经过 B、C、D、E 这几个中转站之手,这里有两种情况:

  • 假设 B、C、D、E 都需要关心这个信息的格式出错了没,那么 B、C、D、E 都需要打开、检查信息,如果没有问题在把信息封装成一个数据包传给下一个 IP。
  • 假设 B、C、D、E 都不关心这个信息格式错了没,只负责转接首发,由最终的 F 来检查这个信息错了没。

  很显然,第二种效率高,因为转发的设备是不需要关心信息的内容的,它们只负责转发。所以,把控制的逻辑独立出来成 TCP 层,让真正的接收端来处理,这样网络的整体传输速率就高了。

连接到底是什么?

  我们都知道 TCP 是面向连接的,那这个连接到底是个什么东西?真的是拉了一条线让端与端之间连起来了?

  所谓的连接其实只是双方都维护了一个状态,通过每一次通信来维护状态的变更,使得看起来好像有一条线关联了对方。

TCP 协议头

            BLZLSs.png
详细的报文解释请看这里协议格式,这里不再一一赘述,挑一些重要的说。

  首先,很明显的发现 TCP 包只有端口,没有 IP 地址。

  • $Seq$(Sequence Number)即序号,它是用来解决乱序问题的。
  • $Ack$ (Acknowledgment Number)即确认号,它是用来解决丢包情况的,告诉发送方这个包我收到啦(确认序号 = Seq + Len + 1)。
  • 标志位就是 TCP flags 用来标记这个包是什么类型的,用来控制 TPC 的状态。
  • 窗口就是滑动窗口($Win$),Sliding Window,用来进行流量控制。

三次握手

  基本的三次握手过程就不再过多赘述,主要看一下三次握手过程中发生了哪些事情。
BLGbfx.png

  首先,为什么要握手?其实,握手的主要作用就是初始化 $Seq$, $SYN$ 的全称是 Synchronize Sequence Numbers,这个序号是用来保证之后传输数据的顺序性。当然说是为了测试双方的发生接收功能也是正确的,但是我认为重点还是在于同步消息的序号。因为,TCP 是一个可靠的字节流服务,需要应包号来确定信息的完整,可靠。

  那为什么要三次,就拿 A 和 B 两个角色来说,首先 A 告诉 B,自己的初始序号,B 听到了后和 A 说我知道了,然后 B 再告诉 A 自己的初始序号,A 再给 B 回信息,我知道了。这么一来一回好像是四次传输了,但是中间的一步,就是当 B 要给 A 回我知道你的初始序号后,可以顺带把自己的初始序列号带出去,这样四次握手就变成了三次握手。

  考虑这样一个情况,当 A 和 B 同时开口告诉对方,自己的初始序列号,然后分别回应自己收到了,这样是不是就是四次握手了?情况如下
        BLtW1P.png
  这样的话,没有一步是可以合并的,不过具体几次还要看实现,有些情况可能不允许这种情况出现,但不影响思考几次握手的问题,因为握手的真正意义在于重点还是在于同步初始序列号

初始序列号 ISN 的取值

  耳熟能详的序号,确认序号,三次握手初始化序列号,那么在三次握手的时候将序列号初始化成为多少是合适的呢?每次随机 rand 一个数值?代码写死初始化的值?

  想想一下写死初始化为零的情况。假设此时连接已经建立完成,$client$ 已经发送了 20 个包出去,然后此时网络中断, $client$ 需要与之前的端口重新建立连接,然后序列号又从 0 开始,此时 $server$ 返回了第 20 个包的 $ack$,此时 $client$ 该如何处理这个确认序号,服务端是不是傻了?

  所以 RFC793 中认为 $ISN$ 要和一个假的时钟绑定在一起 $ISN$ 每四微秒加一,当超过 $2^{32}$ 之后又从 $0$ 开始,要四个半小时左右发生 $ISN$ 回绕。

  所以 $ISN$ 变成一个递增值,真实的实现还需要加一些随机值在里面,防止被不法份子猜到 $ISN$。看起来真的是 rand 一个值,但是这个值是不用写在代码里面的。

SYN 超时了怎么处理?

  也就是 client 发送 SYN 至 server 然后就挂了,此时 server 发送 SYN+ACK 就一直得不到回复,怎么办?

  脑海中第一反应是重试,重新发送 SYN + ACK,但是不能连续快速重试,因为既然 client 挂了,要给与一定的恢复时间,所以要阶梯式的重试,不同时间间隔的重试。

  在 Linux 中就是默认重试 5 次,并且就是阶梯性的重试,间隔就是1s、2s、4s、8s、16s,再第五次发出之后还得等 32s 才能知道这次重试的结果,所以说总共等 63s 才能断开连接。

SYN Flood 攻击

  由上述分析得到,SYN 超时需要耗费服务端 63s 的时间才能断开连接,也就是说在这 63s 内服务端需要保持这个资源,所以不法分子就会通过这个构造出大量的 client 向 server 发送 SYN 但就是不给 server 回复。使 server 的 SYN 队列耗尽,导致无法处理正常的连接请求。
          BLyJWF.png
出现了这种情况时怎么办呢?
可以开启 tcp_syncookies,那么就用不到 SYN 队列了。

  SYN 队列满了之后 TCP 根据自己的 ip、端口、对方 SYN 的序号、时间戳等一波操作后生成一个特殊的序号(即cookie)发回去,如果对方是正常的 client 会把这个序号发回来,然后 server 根据这个序号建立连接。跟 HTTP 的 cookie 差不多,都是用来确定 “你是你”。

  或者调整 tcp_synack_retries 减少重试的次数,设置 tcp_max_syn_backlog 增加 SYN 队列数,设置 tcp_abort_on_overflow SYN 队列满了直接拒绝连接。

为什么要四次挥手?

跟三握一样,又老又臭的问题了,建议学的时候带上 TCP 状态转换时序图一起。
     BL6oH1.png

为什么需要四次挥手?

  因为 TCP 是全双工协议,也就是说,双方都要进行关闭操作,每一方都要向对方发送 $FIN$ 以及回复相应的 $ACK$。就向我对你说,我的数据发完了,然后你回复好的你收到了。然后你对我说,你的数据发送完了,然后我回复好的我知道了。看起来就是四次。

  从上图中可以看到,主动关闭方的状态是 FIN_WAIT_1FIN_WAIT_2,然后再到 TIME_WAIT,而被动关闭的一方是 CLOSE_WAITLAST_ACK

四次挥手状态一定是这样变迁的吗?

BLcOI0.png
  可以看到双方同时主动发送断开请求,所以各自都是主动发起方,状态会从 FIN_WAIT_1 都进入 CLOSING 状态,然后再进入 TIME_WAIT

挥手一定需要四次吗?

  假设 client 已经没有数据要发送给 server 了,所以它发送 FIN 给 server 表明自己数据发完了,不再发了,如果这时候 server 还有数据要发送给 client 那么它就要先回复 ack,然后继续发送数据。等 server 发送完数据后再向 client 发送 FIN 表明自己发送完了,然后等 client 的 ack,这种情况下会四次挥手。

  那么假设,client 发送 FIN 给 server 的时候 server 数据也都发送结束,那么 sercer 就可以在回复 ack 的时候带着 FIN 一起发送给 client,然后等待 client 的 ack,这样就是三次握手了。

为什么要有 TIME_WAIT?

  断开连接发起方在接收到接受方的 FIN 并回复 ack 后并没有直接进入 CLOSED 状态,而是进行了一波等待,等待时间为 2MSL,这个状态称为 TIME_WAIT 状态。

  MSL 是 Maximum Segment Lifetime,即报文最长生存时间,RF 793 中定义的 MSL 时间是 2 分钟, Linux 实际实现是 30s,那么 2MSL 就是 一分钟。

那为什么要等待 2MSL 呢?

  • 就是怕被动关闭方没有收到最后的 ack,如果被动方由于网络波动等原因,那么它会再次发送 FIN ,如果此时主动关闭的一方已经 CLOSED 那就傻了,所以要再等一会。
  • 假设立马断开连接,但是又重用了这个连接,就是五元组完全一致,并且序列号还在合适的范围内,虽然概率极低但理论上可能,那么新的连接会被已关闭的链接路上的一些残留数据干扰,因此给与一定的时间处理一些残留数据。

等待 2MSL 会产生什么问题?

  如果服务器主动关闭大量连接,那么会出现大量的资源占用,需要等待 2MSL 后才能重新使用。

  如果是客户端主动关闭了大量的连接,那么在 2MSL 里面那些端口都是被占用的,端口只有 655355 个,如果端口耗尽了就无法建立新的连接,不过概率应该很低,毕竟端口实在太多。

如何解决 2MSL 产生的问题?

  • 给服务器的socket设置 SO_REUSEADDR 选项。这样的话就算熟知端口处于 TIME_WAIT 状态,在这个端口上依旧可以将服务启动。当然,虽然有了 SO_REUSEADDR 选项,但 socktpair 这个限制依旧存在。

  • 设置 TCP 参数。 Linux 的参数是 tcp_tw_recycle,还有 tcp_timestamps 不过默认是打开的。但是这样会出现很多问题,TCP服务端收到syn但是不回复syn ack问题分析。所以不建议开启,而且 Linux 4.12 版本后已经咔擦了这个参数了。

  • 重用。即开启 tcp_tw_reuse 当然也是需要 tcp_timestamps 的。这里有个重点,tcp_tw_reuse 是用在连接发起方的,而服务端一般是连接被动接收方。tcp_tw_reuse 是发起新连接的时候,可以复用超过 1s 处于 TIME_WAIT 状态的连接,所以他压根没有减少服务端的压力。它重用的是发起方处于 TIME_WAIT 的连接

      从上面来看好像 SO_REUSEADDR 和 tcp_tw_reuse 是一样的东西,首先 tcp_tw_reuse 是内核选项而 SO_REUSEADDR 是用户态选项。

      然后 SO_REUSEADDR 主要用在你启动服务的时候,如果此时端口被占用了并且这个连接处于 TIME_WAIT 状态,那么你可以重用这个端口,如果不是 TIME_WAIT 状态那么就给你一个 Address already in use

      这么来看,这两个玩意好像都不行,而且 tcp_tw_reusetcp_tw_recycle,其实是违反 TCP 协议的,说好的等我到天荒地老,你却偷偷放了手?BXhbdg.png

      要么就是调小 MSL 的时间,不过也不太安全,要么调整 tcp_max_tw_buckets 控制 TIME_WAIT 的数量,不过默认值已经很大了 180000,这玩意应该是用来对抗 DDos 攻击的。

      所以从上面分析下来,比较好的方法就是服务端不要主动关闭,把主动关闭方放到客户端。毕竟服务器是一对多客户端的,资源比较宝贵。

RST 攻击

  上述的 2MSL 解决方案还有很骚的一种 口胡ing 。TCP 的标志位中有一个 RST即 Reset。RST表示复位,用来异常的关闭连接。RST包关闭连接时,不必等缓冲区的包都发出去(不像上面的FIN包),直接就丢弃缓存区的包发送RST包。而接收端收到RST包后,也不必发送ACK包来确认。

  基于此项技术,产生了一个 RST 攻击。A 和服务器 B 之间建立了 TCP 连接,此时 C 伪造了一个 TCP 包发送给 B,使 B 异常的断开了与 A 之间 TCP 连接了,这就是 RST 攻击。

那么伪造什么样的TCP包可以达成目的呢?

  假设 C 伪装成 A 发过去的包,这个包如果是 RST 包的话,毫无疑问,B 将会丢弃与 A 的缓冲区上的所有数据,强制关掉连接。

  如果发过去的包是 SYN 包,那么 B 会表示 A 已经疯了,已经正常连接了又要新建连接,此时 B 会主动向 A 发送 RST 包,并在自己这端强制关掉连接。

  这两种方式都能够达到复位攻击的效果。似乎挺恐怖,然而关键是,如何能伪造成A发给B的包呢?这里有两个关键因素,源端口和序列号。

  Socket 有一个选项叫 IP_TRANSPARENT,可以绑定一个非本地的地址,然后服务端把建立连接的 ip 和端口号都记录下来,写入到本地的某个文件中。然后启动一个服务,假如现在服务端的资源很紧张,那么就设定一个时间,过了这段时间后将处于 TIME_WAIT 状态的对方 ip 和 端口告诉这个服务。然后服务就利用 IP_TRANSPARENT 伪装成之前的那个 client 向服务端发送一个请求,然后服务端收到后会给真的 client 回复一个 ACK,此时 client 已经关闭了,会觉得服务端有病,于是回复一个 RST,服务端就中止了这个连接。BjEGZV.png

超时重传机制是为了解决什么问题?

  TCP提供一种面向连接的、可靠的字节流服务。重点就是这个可靠的。那么在网络不稳定的时候对方没有收到正确的包,此时就必须要重传。

  TCP 的可靠性是通过确认序号,比如 $A$ 发送给 $B$ 1、2、3、4 这四个包,那么 $B$ 要回复一个 5,代表 前面四个包都收到了。

  有一个需要注意的地方,SeqNum 和 ACK 都是以字节数为单位的,也就是说当收到 1、2、4 但是没有 3 的时候是不能回复 ACK 5 的,如果回复的 ACK 5 代表 5 之前的包都收到了。所以只能回复最大连续收到包号,也就是 ACK 3

  而当发送方收到 ACK 3 时不知道 3、4 这两个包是丢了还是没到,于是发送方需要等待,这个等待的时间就比较讲究了。如果太心急可能此时 ACK 已经在路上了,再进行重传就是再浪费网络资源,如果一直等待,接收方反而着急,导致连接空置也是再浪费资源。

  所以这个等待的时间是一门非常重要的学问,设定成为一个什么值是一个比较好的选择呢?此时有一个东西就产生了 RTT(Round-Trip Time): 往返时延。在传输的时候记录一下数据往返一趟用的时间,然后根据这个时间来指定超时重传的时间 RTO(Retransmission Timeout):重传超时时间

  不过这里只说了 RTO 需要参考 RTT 来制定,但是具体要怎么算?我也不会,反正肯定不是直接 RTO = RTT,不过可以看两篇文章 TCP中RTT的测量和RTO的计算 以及 RFC793中 提供的公式,这里不做过多解释。

  有一个比较重要的问题, RTT 采样的时间是用一开始发送的时间到收到 ACK 的时间作为样本值还是重传的时间到 ACK 的时间做为样本值?

BjKQtP.png

  从上图的两种情况看出,一种情况算长了,另一种算短了,这样就比较为难了,因为不知道这个 ACK 是回复给谁的。此时就产生了两种计算方法。

  • Karn / Partridge 算法,不采样重传的RTT。

  发生重传的来回我不采样不就好了,我不知道这次 ACK 到底是回复谁的,我就不管他,我就采样正常的来回。但是不采样重传会有问题,比如某一时刻网络突然就是很差,你要是不管重传,那么还是按照正常的 RTT 来算 RTO, 那么超时的时间就过短了,于是在网络很差的情况下还疯狂重传加重了网络的负载。

  于是 Karn 算法用了一个取巧的方式 $\rightarrow$ 只要一发生重传,就对现有的 RTO 值翻倍(这就是所谓的 Exponential backoff),很明显,这种死规矩对于一个需要估计比较准确的 RTT也不靠谱。

  • Jacobson / Karels 算法

  它把最新的 RTT 和平滑过的 SRTT 做了波计算得到合适的 RTO,公式我就不贴了,因为懂的都懂,不懂的就去搜,因为我也不懂。

为什么还需要快速重传机制?

  超时重传是按时间来驱动的,如果网络状况特别不好的情况下,超时重传没问题,等就等了,但是如果网络状况好的时候,可能只是恰好发生了丢包,那么就没必要进行长时间的等待。

  于是引入了数据驱动的重传即快速重传。什么意思呢?如果连续三次收到对方相同的确认序号,那么马上进行数据重传。因为连续收到三次相同的 ACK 证明当前网络状况良好,确认是包丢了而不是包没到,于是立马开始重传。

BjQ0QU.png

  看起来好像挺完美的,但是你有没有想过我发送 1、2、3、4 这 4 个包,就 2 对方没收到,1、3、4都收到了,然后不管是超时重传还是快速重传反正对方就回 ACK 2。这时候要重传 2、3、4 呢还是就 2 呢?

SACK 的引入是为了解决什么问题?

  SACK 即 Selective Acknowledgment,它的引入就是为了解决发送方不知道该重传哪些数据的问题。

BjY39A.png

  SACK 就是接收方会回传它已经接受到的数据,这样发送方就知道哪一些数据对方已经收到了,所以就可以选择性的发送丢失的数据。

  通过 ACK 告知我接下来要 5500 开始的数据,并一直更新 SACK,6000 - 6500 我收到了,6000 - 7000 我收到了,6000 - 7500 的数据我收到了,发送方很明确的知道,5500-5999 的那一波数据应该是丢了,于是重传。而且如果数据是多段不连续的, SACK 也可以发送,比如 SACK 0-500,1000-1500,2000-2500。就表明这几段已经收到了。

滑动窗口干嘛用?

具体的叙述请看之前的文章浅谈滑动窗口(TCP流量控制)

  已经知道了 TCP 的序号,以及重传,但是这还不够,还要根据情况控制一下发送的速率,因为网络是复杂多变的,有时很通畅,有时很阻塞。所以发送方和接收方都要知道对方的情况,从而控制一下发送的速率,不至于一方一直发送导致另一方接收不过来,或者说接收一个 ack 发送一个 seq,两种都是不好的。

  因此 TCP 就有个叫滑动窗口的东西来做流量控制,也就是接收方告诉发送方我还能接受多少数据,然后发送方就可以根据这个信息来进行数据的发送。

以下是发送方维护的窗口,就是黑色圈起来的。

BjdqtU.png

图例解释:

  • #1:已经收到 ACK 的数据
  • #2:已经发送了但是没收到 ACK 的数据
  • #3:在窗口内可以发送但是还没发送的数据
  • #4:不能发送的数据

此时如果收到 ACK 36,并且发送了46 - 51 的字节,于是窗口开始向前右滑动

BjD97q.png

TCP/IP Guide 上还有一张完整的图,画的十分清晰,大家看一下。

BjDuH1.png

如果接收方回复的窗口一直是 0 怎么办?

  上文已经说了发送方根据接收方回应的 window 来控制能发多少数据,如果接收方一直回应 0,那发送方一直等着?发送方发的数据都得到 ACK 了,但是呢回应的窗口都是 0 ,这发送方此时不敢发了啊,那也不能一直等着啊,这 Window 啥时候不变 0 啊?

  于是 TCP 有一个 Zero Window Probe 技术,发送方得知窗口是 0 后,会发送一个 ZWP 包去探测探测这个接收方到底行不行。发送多少次就要具体看实现了,然后还要设置一定的时间间隔,多次之后还是 0,那么直接发送 RST 断开连接。

假设接收方每次回应窗口都很小怎么办?

想象一下,如果每次接收方都说我还能收 1 个字节,发送方该不该发?

  TCP + IP 头部就 40 个字节了,这传输不划算啊,如果傻傻的一直发这就叫 Silly Window。那咋办,一想就是发送端等着,等养肥了再发,要么接收端自己自觉点,数据小于一个阈值就告诉发送端窗口此时是 0 算了,也等养肥了再告诉发送端。

其中发送端等待的算法就是 Nagle 算法。

  1. 发送端的 TCP 将它从发送应用程序收到的第一块数据发送出去,哪怕只有一个字节。
  2. 在发送第一个报文段(即报文段1)以后,发送端的 TCP 就在输出缓存中积累数据,并等待:或者接收端的 TCP 发送出一个确认,或者数据已积累到可以装成一个最大的报文段。在这个时候,发送端的 TCP 就可以发送这个报文段。
  3. 对剩下的传输,重复步骤2。这就是:如果收到了对报文段x的确认,或者数据已积累到可以装成一个最大的报文段,那么就发送下一个报文段(x + 1)。

算法实现如下:

if there is new data to send
  if the window size >= MSS and available data is >= MSS
    send complete MSS segment now
  else
    if there is unconfirmed data still in the pipe
      enqueue data in the buffer until an acknowledge is received
    else
      send data immediately
    end if
  end if
end if

  简单的说就是当前能发送的数据和窗口大于等于 MSS 就立即发送,否则再判断一下之前发送的包 ACK 回来没,回来再发,不然就攒数据。

  Nagle算法的优点就是简单,并且它考虑到应用程序产生数据的速率,以及网络运输数据的速率。若应用程序比网络更快,则报文段就更大(最大报文段)。若应用程序比网络慢,则报文段就较小(小于最大报文段)。

  接收端自觉点的方案是 David D Clark’s 方案,如果窗口数据小于某个阈值就告诉发送方窗口 0 别发,等缓过来数据大于等于 MSS 或者接受 buffer 腾出一半空间了再设置正常的 window 值给发送方。

  提到了 Nagle 算法就不得不说 TCP 的延迟确认。格算法在等待接收方的确认,而开启延迟确认则会延迟发送确认,会等之后的包收到了再一起确认或者等待一段时候真的没了再回复确认。这就相互等待了,然后延迟就很大了,两个不可同时开启。另外,发送端的应用程序可能会禁用Nagle算法。

已经有滑动窗口了为什么还要拥塞控制?

  前面已经提到了,加了拥塞控制是因为 TCP 不仅仅就管两端之间的情况,还需要知晓一下整体的网络情形,毕竟只有大家都守规矩了道路才会通畅。

  前面我们提到了重传,如果不管网络整体的情况,肯定就是对方没给 ACK ,那我就无脑重传。如果此时网络状况很差,所有的连接都这样无脑重传,是不是网络情况就更差了,更加拥堵了?然后一直冲一直堵,无脑干就完了,然后就 GG 了。

Bj2YJ1.jpgBj2JiR.jpg

所以需要个拥塞控制,来避免这种情况的发生。

拥塞控制怎么搞?

主要有以下几个步骤来搞:

  1. 慢启动,探探路。
  2. 拥塞避免,感觉差不多了减速看看
  3. 拥塞发生快速重传/恢复

TCP拥塞控制.png

  慢启动,就是新司机上路慢慢来,初始化 cwnd(Congestion Window)为 1,然后每收到一个 ACK 就 cwnd++ 并且每过一个 RTT ,cwnd = 2*cwnd 。线性中带着指数,指数中又夹杂着线性增

  然后到了一个阈值,也就是 ssthresh(slow start threshold)的时候就进入了拥塞避免阶段。这个阶段是每收到一个 ACK 就 cwnd = cwnd + 1/cwnd 并且每一个 RTT 就 cwnd++。可以看到都是线性增。

  然后就是一直增,直到开始丢包的情况发生,前面已经分析到重传有两种,一种是超时重传,一种是快速重传。

  如果发生超时重传的时候,那说明情况有点糟糕,于是直接把 ssthresh 置为当前 cwnd 的一半,然后 cwnd 直接变为 1,进入慢启动阶段。

  如果是快速重传,那么这里有两种实现,一种是 TCP Tahoe ,和超时重传一样的处理。一种是 TCP Reno,这个实现是把 cwnd = cwnd/2 ,然后把 ssthresh 设置为当前的 cwnd 。

  然后进入快速恢复阶段,将 cwnd = cwnd + 3(因为快速重传有三次),重传 DACK 指定的包,如果再收到一个DACK则 cwnd++,如果收到是正常的 ACK 那么就将 cwnd 设为 ssthresh 大小,进入拥塞避免阶段。

  可以看到快速恢复就重传了指定的一个包,那有可能是很多包都丢了,然后其他的包只能等待超时重传,超时重传就会导致 cwnd 减半,多次触发就指数级下降。

  所以又搞了个 New Reno,多加了个 New,它是在没有SACK 的情况下改进快速恢复,它会观察重传 DACK 指定的包的响应 ACK 是否是已经发送的最大 ACK,比如你发了1、2、3、4,对方没收到 2,但是 3、4都收到了,于是你重传 2 之后 ACK 肯定是 5,说明就丢了这一个包。

  不然就是还有其他包丢了,如果就丢了一个包就是之前的过程一样,如果还有其他包丢了就继续重传,直到 ACK 是全部的之后再退出快速恢复阶段。

简单的说就是一直探测到全部包都收到了再结束这个环节

还有哪些拥塞控制算法?

BvZtQH.png

从维基上看有这么多。已经远超出我目前的知识范围了,后续需要了在学。

总结

说了这么多来总结一下吧。

  TCP 是面向连接的,提供可靠的、有序的传输并且还提供流控和拥塞控制,单独提取出 TCP 层而不是在 IP 层实现是因为 IP 层有更多的设备需要使用,增加了很多不必要的过程。

  三次握手主要是为了定义初始序列号为了之后的传输打下基础,四次挥手是因为 TCP 是全双工协议,因此双方都得说拜拜。

  SYN 超时了就阶梯性重试,如果有 SYN攻击,可以加大半队列数,或减少重试次数,或直接拒绝。

  TIME_WAIT 是怕对方没收到最后一个 ACK,然后又发了 FIN 过来,并且也是等待处理网络上残留的数据,怕影响新连接。

  TIME_WAIT 不建议设小,或者破坏 TIME_WAIT 机制,如果真想那么可以开启快速回收,或者重用,不过注意受益的对象。

  超时重传是为了保证对端一定能收到包,快速重传是为了避免在偶尔丢包的时候需要等待超时这么长时间,SACK 是为了让发送方知道重传哪些。

参考文献