计算机网络之TCP篇

149 阅读9分钟

传输层负责在计算机之间提供可靠的或不可靠(UDP)的端到端的数据传输.

在OSI七层网络模型中TCP输入传输层协议(HTTP属于应用层协议).

由此看来传输层是非常重要的一层,所以为了保证TCP的可靠性,用了很多的机制来保证.

我们希望数据在传输的时候不会发生丢失,但是在当代复杂的网络环境中,这点很难保证,于是就有了重传机制.

重传机制

重传机制:顾名思义,就是当数据发生丢失之后,重新发一份不就好了,但是什么时候发,该发哪一个数据包?那就有了我们介绍的第一个重传机制:

超时重传

image.png

在服务端发送数据的时候,设置一个定时器,此种情况下有两种会发生超时的情况:

1.数据丢失(如图没有收到101-200的数据包).

2.客户端相应的ACK丢失.

当定时器超时之后,服务端就要重新发动数据了,关键的一点在于这个定时器的时长设置为多少? 直接说结论,不必管其中的推导过程:

服务器会在传输的过程中通过RTT和RTO经过一系列的计算出来一个时间,注意这个时间不是固定的,会动态的变化.

每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍。两次超时,就说明网络环境差,不宜频繁反复发送。

超时触发重传存在的问题是,超时周期可能相对较长。那是不是可以有更快的方式呢?

快速重传

image.png

服务器连续收到客户端返回的seq2三次,此时就会触发重传.

这里有一个问题,在图中重传seq2之后,客户端收到了,接着返回了ack5,因为seq3和seq4已经收到了.但一定收到了么,不一定,有可能seq3也丢失了,所有快速重传就面临一个问题,当要重传的时候重传多少,是只重传一个还是后面的都重传?

  1. 都重传:就如上图中,重新传seq3显然会浪费资源
  2. 只重传一个:那如果seq3也丢失了,在服务端重传seq2之后还要再等三次客户端发动的seq3而再次重传,这样传输的效率会很低.

SACK 方法 (选择性确认)

image.png

发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK 信息发现只有 200~299 这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。

滑动窗口

机智的你一定发现了数据传递的效率很慢,服务端发一个数据包,等待客户端确认,然后再发下一个.TCP的设计者也想到了这一点,于是就引入了窗口的概念

有了窗口之后,无需等待确认应答,服务端可以一直发送数据,直到达到窗口容量的最大值.

其实窗口就类似操作系统的缓存空间,服务器发送的数据放到缓存区间,客户端收到并确认之后,在把这个数据从缓存区间移除.如图

image.png

虽然ACK500丢失了,但是在下一次确认应答中拿到了接收方发来的ACK600,说明前599的数据包接收方都接收到了.

滑动窗口的大小怎么设定

TCP 头里有一个字段叫 Window,也就是窗口大小。

这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。

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

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

如果你感兴趣,想要更多的了解这一部分知识,推荐你去看图解网路

流量控制

发送数据包的时候发送方不能无脑的发,要考虑接收方的接收能力,如果无脑的发送,接收方可能处理不过来,导致超时,触发重传,造成了网络流量的浪费.

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

发送方和接收方各有一个缓冲区间,假设他们的大小都是360字节

场景一:

  1. 发送方发送200字节,接收方接收200字节,但是接收方繁忙,没时间处理,就把这200字节放在缓冲区间
  2. 发送方在确认应答中知道接收方的缓冲区间还有160字节,然后就又发了160字节过去,但接收方还是在忙,于是缓冲区间就满了.

最终:接收方关闭了接收窗口,发送方的发送窗口也是0,即:窗口关闭,但实际上会定时发送窗口探测报文,以便知道接收方的窗口是否发生了改变.

场景二:

  1. 发送方发送 140 字节的数据,服务端因为现在非常的繁忙,操作系统于是就把接收缓存减少了 120 字节,当收到 140 字节数据后,又因为没有读取任何数据,所以 140 字节留在了缓冲区中.
  2. 但此时发送方还没有收到接收方的相应报文,于是就又发了180字节数据,但是此时接收方的缓冲区大小是(360-100-140)120字节,超出了缓冲区60字节,于是就把数据包丢失了。

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

窗口关闭有什么潜在的危险

当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口的 ACK 报文在网络中丢失了,这会导致发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据(类比死锁)

TCP 是如何解决窗口关闭时,潜在的死锁现象呢?

在发送方接收到接收方窗口关闭的信息之后,会启动一个定时器,每隔一段时间发送一个探测报文,

  • 如果接收窗口仍然为 0,那么就会重新启动持续计时器;
  • 如果不是 0,那么死锁的局面就可以被打破了。

窗口探测的次数一般为 3 次,每次大约 30-60 秒(不同的实现可能会不一样)。如果 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发 RST 报文来中断连接。

还有什么潜在的危险

接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症。因为TCP+IP头就有40字节,为了传输几个字节的数据,要搭上这么大的开销,这太不经济了。

怎么让接收方不通告小窗口呢?

接收方通常的策略如下:

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

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

怎么让发送方避免发送小数据呢?

发送方通常的策略如下:

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

  • 条件一:要缓冲区窗口大小 >= MSS 并且 数据大小 >= MSS
  • 条件二:收到之前发送数据的 ack 回包;

只要上面两个条件都不满足,发送方一直在囤积数据,直到满足上面的发送条件。

拥塞控制

在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大....

拥塞控制主要是四个算法:

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

慢启动

顾名思义:就是发送方通过不断的尝试,先发送小数据,慢慢加大发送数据量.

image.png

那慢启动涨到什么时候是个头呢?

有一个叫慢启动门限 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 大小的数据,变成了线性增长。

image.png

就这么一直增长着后,网络就会慢慢进入了拥塞的状况了,于是就会出现丢包现象,这时就需要对丢失的数据包进行重传。

当触发了重传机制,也就进入了「拥塞发生算法」。

后面的两个算法比较难懂,几句话介绍不清楚,建议去看小林的原文,描述的很清楚....

参考资料

[1] 小林图解网络