Skip to content
BinWatson edited this page Nov 22, 2021 · 1 revision

TCP

可靠传输

原理

实现

流量控制

所谓的流量控制就是希望发送方的发送速率不要太快,要让接收方来得及接收。

1. Nagle算法与延时ACK

Nagle算法

当一个TCP连接中有在传数据,小的报文段(长度小于SMSS)就不能被发送,直到所有的在传数据都收到ACK。在收到ACK后,TCP需要收集这些小数据,将其整合道一个报文段中发送。

这种方法精妙之处在于,ACK返回越快,报文段发送越快。

IMG_20211121_114333
延时ACK

TCP并不对每个数据包都返回ACK,利用TCP积累ACK,只要回复最终的ACK就可以了,可以降低网络流量。

2. 窗口管理

滑动窗口
IMG_20211121_115345
  • 发送窗口结构:

    • $可用窗口 = 窗口大小 - 已发送但未得到确认的数据值$
  • 接收窗口结构:

IMG_20211121_115351

拥塞控制

出现拥塞原因: $\sum=对资源的需求 > 可用资源$

拥塞控制流量控制的区别:

  • 拥塞控制:防止过多的数据注入网络,避免网络中的路由器和链路过载。

  • 流量控制:抑制发送端发送数据的速率,以便接收端来得及接收。

    拥塞控制是一个全局的过程,涉及到网络上的所有路由器、主机以及与降低网络传输性能有关的因素;流量控制强调的是点对点的通信量控制,是端到端的问题。

    某些拥塞控制协议,是通过控制发送端降低发送速率从而使整个网络降低拥塞。

IMG_20211117_193135

传统TCP拥塞控制的方法

TCP进行拥塞控制的四种方法:慢启动拥塞避免快速恢复快速重传

1. 慢启动

目的:防止在TCP连接刚建立时,在短时间内向网络中注入大量的数据,导致拥塞。

拥塞窗口cwnd增长为:

$$cwnd = cwnd + min(N, SMSS)$$

$N$ 刚被收到的确认报文段的字节数。

例如:一开始$cwnd = 1$ 发送方,发送一个数据包给接收方,接收方收到后回复一个$ACK$ 给发送方。接收方接收后,此时 $cwnd = 1$ —> $cwnd = 2$;发送方就发送两个数据包,收到回复后,发送四个数据包...以此类推。每个ACK都会造成两数据包被发送,一个占用原先从网络走出去包的容量,另一个是新产生的。

IMG_20211117_203107

2. 拥塞避免

目的:慢启动是指数增长,拥塞控制的目的是让 $cwnd$ 缓慢增长。

使用前提:假设网络中比特错误导致丢包的概率很小(远小于1%),因此有丢包产生就表明从源到目的端口必有某处发送了拥塞。

当慢启动的数据包增长达到 $ssthresh$ 时,就会触发TCP连接改为拥塞避免算法。

拥塞窗口cwnd增长为:

$$cwnd_{t+1} = cwnd_{t} +SMSS * \frac{SMSS}{cwnd_{t}} $$

即第 $cwnd_{t+1}$ 的窗口大小为 $cwnd_{t}*\frac{1}{k}$ (其中 $k$ 为第$cwnd_{t}$次时发送的SMSS的段数)

例如:

$$cwnd_{1}=cwnd_{0}+SMSS*\frac{SMSS}{cwnd_{0}}$$

​ $$=kSMSS+SMSS\frac{SMSS}{k*SMSS}$$ $$(cwnd_{0} = k * SMSS)$$

$$=k*SMSS+\frac{1}{k}*SMSS$$

$$=k+\frac{1}{k}*SMSS$$

$$=cwnd_{0}+\frac{1}{k}*SMSS$$

也就是说,随着ACK的到达,cwnd 会有小幅度增长,整体的增长率呈现轻微的次线性。

IMG_20211117_203113

3. 快速恢复

3.1 引入
  • Tahoe算法:

    • 在慢启动阶段,若检测到丢包无论是超时还是快速重传引起的,都会重新进入慢启动阶段。即有丢包产生时,Tahoe算法只是简单将cwnd减为初始值。
    • 拥塞避免呢?
  • Reno算法:

    • 若丢包是由快速重传引起的(三次连续ACK),则设置$cwnd_{t+1}=ssthresh_{t}$ ,然后继续执行拥塞算法。

    • 在恢复阶段,每收到一个$ACK$就会使$cwnd$的值(临时)增长1个$SMSS$,相应地就意味着能发送一个新的数据包,直到收到一个不重发的$ACK$。

      基于包守恒原理,即每收到一个$ACK$说明,链路就有一个包到达了接收方,因此可以多发一个数据包。

3.2 标准TCP
  • 标准TCP:

    • 当收到三次重复的$ACK$(或者其它表明需要快速重传的信息)时,会执行以下行为:

      1. $ssthresh$更新为大于$max(外在数据/2,2*SMSS)$中的值。

      2. 启动快速重传算法,将$cwnd$设置为$(ssthresh+3*SMSS)$。

        3个$SMSS$是因为接收到了3个$ACK$说明链路有三个包出去了

      3. 每接受到一个重复的$ACK$,$cwnd$的值就暂时增加一个$SMSS$。

      4. 当接收到一个好的$ACK,将$$cwnd$重设为$ssthresh$。(然后进入拥塞算法。)

2和3就构成了快速恢复

在《计算机网络第八版》中是这样描述快速恢复的情况的,设置$ssthresh_{t+1}=cwnd_{t}/2$,然后启动慢启动算法。

4.快速重传

4.1 基于定时器的重传

在设定计时器前,需要记录计时的报文段序列号,若及时收到了该报文段的ACK,那么计时器就被取消。

4.2 快速重传
什么是快速重传?

快速重传是基于接收端的反馈信息来引发重传,快速重传能更加及时有效地修复丢包情况。

当接收到失序报文段时,TCP需要立即生成确认信息(重复的ACK),并且失序情况表明在后续数据到达前出现了丢段,即接收端缓存出现了空缺。

但是当网络出现失序分组时,我们并不能马上断言就是出现了丢包,有可能是接收端在收到当前盼望序列号的报文段之前,先一步失序地接收到了该报文段的后续报文段,即出现失序。因此TCP需要等待一定数目的重复ACK(称为重复ACK阈值dupthresh,一般为3个ACK),来决定数据是出现丢失还是仅仅只是失序到达,若出现丢失则触发重传。

当出现重复dupthresh个ACK时,TCP可以立即触发重传,不必等待计时器超时。

快速重传如何工作?
IMG_20211121_083645

​ 第一次重传,在0.890s,0.926s和0.964s时刻到达的均为序列号为23801的重复ACK。第三个重复ACK的到达触发了报文段23801的快速重传,时间为0.993s。

第一次重传发生时,发送端在执行重传前已发送的最大序列号为(43401 + 1440 = 44801),称为恢复点(recovery point)。TCP在接收到序列号等于或大于恢复点的ACK时,才会被认为从重传中恢复。

​ 第二次重传,在1.322s和1.321s时刻的ACK并不是44801,而是26601,该序列号大于之前的23801,但仍然不足以到达恢复点。这种类型的ACK被称为部分ACK(partial ACK)。当部分ACK到达时,TCP发送端立即发送可能丢失的报文段,并且维持这一过程直到到达或超过恢复点。

如果拥塞控制允许,也就是说每收到一个重复的ACK,发送端也会发送一个新的报文段。^快速恢复

上面提到的部分ACK,是基于[New Reno](#New Reno)算法[^New Reno]才有的,而老的TCP接收到一个可接受的ACK时,就能使发送端结束恢复阶段。

4.3 基于SACK的重传
什么是SACK?

选择确认SACK选项,能够提供接收方的当前空洞给发送方,它能够在报文段丢失或者接收方遗漏时更好地进行重传工作。

空洞:可以理解为,在接收方的窗口中,某个报文段缺失了,而缺失报文段序列,之前那些报文段和之后一部分报文段被正确接收了,就好像在接收窗口中缺了一块。

IMG_20211121_095332

SACK是一个选项,当接收端接收到乱序的数据时,就能提供一个带SACK选项的报文段来描述这些乱序的数据,从而帮助发送方有效进行重传。

”带SACK选项的报文段“:在《TCP/IP详解卷一》中,这样描述’虽然只有SYN报文段才能包含'允许确认‘选项,但是只要发送方已经发送了该选项,SACK块就能通过任何报文段发送出去”。也就是说,一开始在建立连接时,需要双方协商好使用SACK,在后续的报文段传输中,就可以随意使用SACK选项来携带SACK块了。

在TCP中,TCP头部最多能有60个字节,基本的TCP也就是不带选项的TCP头部为20个字节,因此选项最多能占40个字节。

SACK信息保存在SACK选项中,包含了接收方已经成功接收的数据块的序列范围,每一个范围被称作一个SACK块,一个SACK块占32位,即8字节,因此最多有$(40-2)/8=4块$,其中2字节用于保存SACK的种类和长度,但是呢通常SACK会与TSPOT一同使用,TSPOT需要10个字节,因此最多其实只能携带3块。

SACK重传如何工作?
  • 接收端:

    • 首先在TCP连接建立期间就与发送端协商接收SACK许可选项,即可生成SACK。
    • 第一个SACK块内包含的是最近接收到的报文段的序列号范围,第二个SACK包含的是上一个接收到的报文段序列范围,第三个SACK包含的是上上一个接收到的报文段序列范围。原因是SACK可能丢失,也就是说上一个报文段范围的SACK可能丢失在网络中,因此重复发送前面的SACK块,目的是提供一个备份。
  • 发送端:

    • 发送端可以利用SACK功能,合理利用收到的SACK块来进行重传,该过程也称为选择性重传或者选择性重发

    • “食言”,SACK告诉发送端已成功接收到一定序列号范围的数据,而之后做出变更。出于这个原因,SACK发送端不能在收到一个SACK后立即清空其重传缓存中的数据,只有收到普通的ACK号大于其最大序列号值时才可清除。

      也就是不能完全根据SACK来判断接收端是否确定接收到某个系列号的报文段,ACK才是唯一判断的依据。SACK只是一种建议。

      为什么会出现这种情况?在[RFC2018]中这样描述

      "Note that the data receiver is permitted to discard(抛弃) data in its queue that has not been acknowledged to the data sender, even if the data has already been reported in a SACK option. Such discarding of SACKed packets is discouraged, but may be used if the receiver runs out of buffer space."

      最后一句话也就说,当接收方缓冲区用完时,可能会采取丢弃报文段的措施。

IMG_20211121_083704

​ 第一次重传,在0.890s到达一次23061段的ACK,在0.926s到达了一次23061段的SACK,因此触发了第一次重传,在接收到两个ACK后,发送方又接着重传第二个数据段。TCP SACK发送端也借鉴了New Reno算法中的恢复点思想。

4.4 SACK和快速重传的对比

SACK更适合在高拥塞的网络下运行,丢失的数据包可以快速得到重传。

而在大多数情况下快速重传能有更好的效果。

[^New Reno]: New Reno 当接收的序列号不小于恢复点时,才停止快速恢复。

对传统TCP的改进

标准TCP存在的问题:

  1. 当出现多个包丢失时,第一个丢失的包被接收后会发送一个好的$ACK$,导致快速恢复停止,$cwnd$的值不再变化。但其实其它丢失的包并未完成重传。(1)
  2. 当链路中没有足够的数据包时,就不可能产生三个$ACK$,因此就无法触发快速重传,会因为超时而使得TCP进入慢启动。(2)

New Reno算法:当接收的序列号不小于恢复点时,才停止快速恢复。(1)

**限制传输:**在窗口较小的情况下,当出现丢包时,网络中可能没有足够的包去引起快速重传/恢复机制。因此,TCP每接收到两个重复$ACK$就能发送一个新数据包,这就使得网络中的数据包维持一定数量,能够触发快速重传。(2)

拥塞窗口校验

Congestion Window Validation

1. 空闲发送端和应用受限发送端

  • 空闲发送端:没有数据要发送,之前发送的数据也已经成功接收到ACK。

  • 受限发送端:需要传输数据,由于某种原因无法发送。(可能是处理器或者下层链路阻塞)

2. CWV算法

  • 当需要发送新数据时,首先查看距离上次发送操作是否已经超过一个RTO。如果超过,则:
    • 更新$ssthresh$值——设置$max(ssthresh, \frac{3}{4}*cnwd)$。
    • 每经过一个空闲的$RTT$时间,$cwnd$值就减半,但不小于1个$SMSS$。
  • 对于应用受限阶段,执行相似操作:
    • 设置已经使用的窗口大小记为$W_used$。
    • 更新$ssthresh$值——设为$max(ssthresh, \frac{3}{4}*cwnd)$。
    • $cwnd$设为$(cwnd + W_used)/2$。

采用该算法的结果是,在长时间发送发暂停后,发送发会进入慢启动阶段。Linux TCP实现了CWV并默认启用

SACK TCP拥塞控制的方法

TCP连接管理

IMG_20211120_090244

连接建立

IMG_20211120_090235

建立一个TCP连接的步骤(三次握手):

  1. 客户端A向服务器B(LISTEN(监听)状态)发送连接请求报文,此时TCP的首部SYN位(同步位)被置1,并在首部中指明想要连接的端口号和设置客户端的初始序列号为$Seq=ISN(c)$。此时客户端就进入了SYN-SENT(同步已发送)状态。

    《计网》:SYN数据包不携带数据,但是会消耗一个序列号。

    《卷一》:TCP的SYN可以承载应用数据,但是由于Berkeley的socket不支持,因此很少用。

    初始序列号:初始序列号会随时间改变而变化,因此每一个连接都拥有不同的初始序列号。目的是防止出现与其他连接的序列号重叠,尤其是对于同一连接的两个不同实例而言。现代操作系统如Linux,通常采用半随机的方式选择初始序列号。

    防止将老连接遗留的网络中的报文段视为新连接的一部分。

  2. 服务器B在接收到请求后,若同意建立连接,则向A发送确认。则也发送自己是SYN报文段作为响应,并且包含了它的初始序列号$Seq=ISN(s)$。此外为了响应A的SYN,TCP首部的ACK标志位被置1,并设置确认号$ACK=ISN(c)+1$。这时B进入SYN-RCVD状态,当A接收到B的响应数据包时,A进入ESTABLISHED状态,

    B发送回给A的数据包分为两部分:第一部分为响应A的ACK报文和第二部分为请求连接的SYN报文,为了方便一般会把两个报文整合在一起发送。也就是将四个报文的握手,降为三个。

  3. 客户端A在接收到服务器B的SYN报文后,还需要向B进行响应。因此,客户端将$ISN(s)$的数值加1后作为$ACK$的数值,然后发送给B。B在接收到客户端的响应后,进入ESTABLISHED状态,至此TCP连接建立。

    ACK报文可以携带数据,但是如果不携带数据则不消耗序列号。

    例如:在ssh对进行远程操作时,每键入一个字符就需要在TCP中传输4个数据包,一个是客户端击键产生的数据字符例如'd',服务器需要对该数据包进行确认,然后发送一个回显的'd',接着客户端需要对回显的'd'进行确认。通过服务器会把确认和回显放在一个包中,因此此时的ACK也带有数据,也会消耗一个序列号。

连接释放

IMG_20211120_090254

释放一个TCP连接(四次挥手):

  1. 客户端A将释放报文段首部的终止标志位FIN置1,设置序列号为$Seq=u$^1。然后发送给服务器B,此时A就进入了FIN-WAIT-1(终止等待1)状态,等待B的确认。

    《计网》:FIN不携带数据,但是要消耗一个序列号。

  2. 服务器B在接收到释放请求后,发出确认,确认号$ACK=u+1$,并且设置序列号为$v$^2。然后B就进入了CLOSE-WAIT(关闭等待)状态,A在接收到响应的ACK后,进入FIN-WAIT-2状态,等待服务器B发送连接释放报文段。此时TCP进入半关闭(half-close)状态。

    B发送的报文段为单纯的ACK报文段,因此不消耗序列号。

    IMG_20211120_103457
  3. 若B已经没有要向A发送的数据,其应用程序就通知TCP释放连接。这时B发出的连接释放报文的FIN设置为1。假设现在B的序列号为$w$^3,同时B还需要设置上次发送过的确认号$ACK=u+1$。当B将FIN报文段发送出去后,B就进入了LAST-ACK(最后确认)状态,等待A的确认。

  4. A在收到B的FIN报文段后,发送确认报文,将标志位ACK置1,确认号设置为$ACK=w+1$,发送序列号设为$Seq=u+1$。然后A就进入到TIME-WAIT(时间等待)状态,B在接收到确认报文段后,进入CLOSE状态。此时TCP连接还没有断掉,必须等待TIME-WAIT设置的时间$2MSL$过后,A才进入CLOSE状态,那时TCP连接才正式释放。

    为什么要设置TIME-WAIT状态等待$2MSL$?

    1. 为了保证A发送的最后一个ACK报文段可以到达B。
    2. 防止出现“已失效的连接请求报文段”,A在发送完最后一个ACK报文段后,再经过$2MSL$的时间,就可以使本连接持续的时间内产生的所以报文都从网络中消失。

TCP状态转移图

IMG_20211120_090919

具体实现

在Linux中,将适用以下规则:

  1. 当一个请求连接(SYN报文段)到达时,将会检查系统范围参数net.ipv4.tcp_max_syn_backlog(默认为1000),若处于SYN_RCVD状态的连接超过这一阈值,就拒绝进入的连接。

  2. 每一个处于侦听状态下的节点都拥有一个固定长度的连续队列。其中的连接已经被TCP完全接受(即三次握手已经完成),但未被应用程序所接受。应用程序可以对这一队列做出限制,通常称为未完成连接(blacklog)。blacklog的数目在0到系统最大值之间net.core.somaxconn,默认值为128。

    • 如果侦听队列中仍然有空间,TCP模块会应答SYN并完成连接。直到三次握手完成,与侦听节点相关的应用程序才会知道新的连接。在完成三次握手后,客户端会认为服务器已经能够接受数据了,实际上服务器可能此时还未收到新连接的通知,因此此时客户端发送的数据会被服务器的TCP模块存入一个队列中。

    • 如果侦听队列中已经没有足够的空间分配给新的连接,TCP将会延长对SYN的做出响应,从而给上层应用程序一个跟上节奏的机会。如果系统变量net.ipv4.tcp_abort_on_已被设定,那么新进入的连接就会被RST重置报文段重新置位。

      在队列满时,发送重置报文一般是不可取的,因为这会造成客户端尝试与服务器联系时,在SYN时期接收到一个重置报文段,那么它可能会错误的认为没有服务器存在。太忙实际上是一种软的或者临时的错误,而不是一种硬性的错误。

      在正常情况下,当队列满时,根据TCP机制,客户端的主动打开操作最终会超时,在Linux中,连接的客户端将会明显放缓一段时间——它们既不会超时也不会重置。

参考

  1. 《TCP/IP详解卷一》
  2. 《计算机网络第八版(谢希任)》