前置芝士
在讲解 TCP 可靠传输之前,我们先搞清楚什么是 TCP 可靠传输?
TCP 可靠传输就是保证接收方收到的字节流和发送方发出的字节流是完全一样的。
那为什么需要 TCP 的可靠传输呢?
由于网络层是没有可靠传输机制的,尽自己最大的努力进行交付。而传输层使用 TCP 实现可靠传输,TCP 保证可靠传输的机制有如下几种:
1)校验和
2)序列号和确认应答机制
3)重传机制
4)滑动窗口
5)流量控制
6)拥塞控制
校验和
所谓 TCP 的校验和(Checksum)就是说:由发送端计算待发送 TCP 报文段的校验和,然后接收端对接收到的 TCP 报文段验证其校验和(TCP 的校验和是一个端到端的校验和)。其目的是为了发现 TCP 的首部和数据在发送端到接收端之间是否发生了变动。如果接收方检测到校验和有差错,则该 TCP 报文段会被直接丢弃。
序列号和确认应答机制
TCP 报文段的首部中有一个序号字段指的是该报文段第一个字节的序号(一个字节占一个序号)。确认应答机制就是接收方收到 TCP 报文段后就会返回一个确认应答消息,就像下图这样:
重传机制
上面只是简单的介绍了校验和、确认应答机制,下面的重头戏开始了。
上面我们介绍了序列号和确认应答机制,但是错综复杂的网络,并不一定能如上图那么顺利能正常的数据传输,万一数据在传输过程中丢失了呢?
那对于 TCP 针对数据包丢失的情况,会用重传机制解决。下面介绍几种常见的重传机制:
- 超时重传
- 快速重传
- SACK
下面我们逐个击破。
超时重传
超时重传就是在发送数据的时候,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK
确认应答报文,就会重发该数据。一般在下面两种情况下会发生超时重传:
- 数据包丢失
- 确认应答丢失
我们知道了超时重传,那这个超时时间设置为多少比较合适呢?
先介绍一下 RTT(Round-Trip Time 往返时延),如下图:
RTT
就是数据从网络的一端传送到另一端所需的时间两倍,也就是包的往返时间。
超时重传时间是以 RTO
(Retransmission Timeout 超时重传时间)表示。
假设在重传的情况下,超时时间 RTO
较长或较短时,会发生什么事情呢?
简单说一下图中的情况:
- 当超时时间 RTO 较大时,重发就慢,丢了老半天才重发,没有效率,性能差。
- 超时时间 RTO 较小时,会导致可能并没有丢就重发,于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。
由此可见,我们应该设置超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。这样就能确定丢包了,也不会导致丢了老半天才重发。但是这个只是我们想的理论值而已,实际的网络情况比这个复杂的多,有兴趣的同学可以自己的多了解。
如果我们超时重传的数据又超时了该怎么办呢?TCP 的策略是重传的超时间隔加倍。每进行一次超时重传,都会将下一次重传的超时时间间隔设为先前值的两倍。但是这样也会有问题,就是超时时间可能会很长,有没有更快的方式呢?没错,就是我们下面讲的快速重传。
快速重传
TCP 还有另外一种快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传。那快速重传机制是如何工作的呢?看下图:
从图中我们就很容易看出来快速重传的原理了:
- 第一份 Seq1 先送到了,于是就 Ack 回 2;
- 结果 Seq2 因为某些原因没收到,Seq3 到达了,于是还是 Ack 回 2;
- 后面的 Seq4 和 Seq5 都到了,但还是 Ack 回 2,因为 Seq2 还是没有收到;
- 发送端收到了三个 Ack = 2 的确认,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2;
- 最后,接收到收到了 Seq2,此时因为 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6 。
可见,快速重传机制就是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。
快速重传虽然解决了超时时间的问题,但是它也面临着这样一个问题:重传的时候,是重传之前的一个,还是重传所有的问题。
比如上面的例子,是重传 Seq2 呢?还是重传 Seq2、Seq3、Seq4、Seq5 呢?因为发送端并不清楚这连续的三个 Ack 2 是谁传回来的。为了解决不知道该重传哪些 TCP 报文,于是就有 SACK
方法。
SACK
SACK
( Selective Acknowledgment 选择性确认),这种方式需要在 TCP 头部选项字段里加一个 SACK
的东西,它可以将缓存的地图发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。
比如发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK
信息发现只有 200~299
这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。
滑动窗口
为啥需要滑动窗口呢?
我们都知道 TCP 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答, 再发送下一个。这种方式的效率比较低,而且数据包的往返时间越长,通信的效率就越低。
为解决这个问题,TCP 引入了窗口这个概念。即使在往返时间较长的情况下,它也不会降低网络通信的效率。那么有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。
窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。
下面我们举个例子:假设窗口大小为 3 个 TCP 段,那么发送方就可以「连续发送」 3 个 TCP 段,并且中途若有 ACK 丢失,可以通过下一个确认应答进行确认。如下图:
从图中可以知道,即使 ACK 600 确认应答报文丢失,也没关系,因为可以通过下一个确认应答进行确认,只要发送方收到了 ACK 700 确认应答,就意味着 700 之前的所有数据接收方都收到了,这个模式就叫累计确认或者累计应答。
TCP 头里有一个字段叫 Window
,也就是窗口大小。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来,所以,通常窗口的大小是由接收方的决定的。
发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。
下面我们看看发送方的窗口实际是咋样的:
下图就是发送方缓存的数据,根据处理的情况分成四个部分,其中深蓝色方框是发送窗口,紫色方框是可用窗口:
当发送方把数据「全部」都一下发送出去后,可用窗口的大小就为 0 了,表明可用窗口耗尽,在没收到 ACK 确认之前是无法继续发送数据了,如下图所示:
当收到之前发送的数据 32~36
字节的 ACK 确认应答后,如果发送窗口的大小没有变化,则滑动窗口往右边移动 5 个字节,因为有 5 个字节的数据被应答确认,接下来 52~56
字节又变成了可用窗口,那么后续也就可以发送 52~56
这 5 个字节的数据了,如下图所示:
我们知道了滑动窗口的四个部分,那这四个部分是怎么表示出来的呢?
TCP 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针(指特定的序列号),一个是相对指针(需要做偏移)。就像下面这样:
简单解释一下这三个指针的意思:
SND.WND
:表示发送窗口的大小(大小是由接收方指定的)。SND.UNA
:是一个绝对指针,它指向的是已发送但未收到确认的第一个字节的序列号,也就是 #2 的第一个字节。SND.NXT
:也是一个绝对指针,它指向未发送但可发送范围的第一个字节的序列号,也就是 #3 的第一个字节。- 指向 #4 的第一个字节是个相对指针,它需要
SND.UNA
指针加上SND.WND
大小的偏移量,就可以指向 #4 的第一个字节了。
说完了发送方的窗口,接下来说说接收方的窗口:
接收窗口相对简单一些,根据处理的情况划分成三个部分:
- #1 + #2 是已成功接收并确认的数据(等待应用进程读取);
- #3 是未收到数据但可以接收的数据;
- #4 未收到数据并不可以接收的数据;
其中三个接收部分,使用两个指针进行划分:
RCV.WND
:表示接收窗口的大小,它会通告给发送方。RCV.NXT
:是一个指针,它指向期望从发送方发送来的下一个数据字节的序列号,也就是 #3 的第一个字节。- 指向 #4 的第一个字节是个相对指针,它需要
RCV.NXT
指针加上RCV.WND
大小的偏移量,就可以指向 #4 的第一个字节了。
可能有的小伙伴就会问了,接收方的窗口大小和发送窗口大小是相等的吗?
并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。
因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小,是通过 TCP 报文中的 Windows 字段来告诉发送方。那么这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系。
流量控制
发送方不能无脑的发数据给接收方,要考虑接收方处理能力。如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致触发重发机制,从而导致网络流量的无端的浪费。
为了解决这种现象发生,TCP 提供一种机制可以让发送方根据接收方的实际接收能力控制发送的数据量,这就是所谓的流量控制。它的具体操作是, 接收端主机向发送端主机通知自己可以接收数据的大小, 于是发送端会发送不超过这个限度的数据。,该大小限度就被称作窗口大小。
下面举个例子,假设以下场景:
- 客户端是接收方,服务端是发送方
- 假设接收窗口和发送窗口相同,都为
200
- 假设两个设备在整个传输过程中都保持相同的窗口大小,不受外界影响
上图就是流量控制,画的很清楚了。
拥塞控制
为什么要有拥塞控制呀,不是有流量控制了吗?
流量控制是避免发送方的数据填满接收方的缓存,但是并不知道网络中发生了什么,在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大….
于是,就有了拥塞控制,控制的目的就是避免发送方的数据填满整个网络。为了在发送方调节所要发送数据的量,定义了一个叫做拥塞窗口的概念。
那什么是拥塞窗口呢?
拥塞窗口 cwnd 是发送方维护的一个状态变量,它会根据网络的拥塞程度动态变化。
我们在前面提到过发送窗口 swnd 和接收窗口 rwnd 是约等于的关系,那么由于入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd)。
拥塞窗口 cwnd 变化的规则:
- 只要网络中没有出现拥塞,cwnd 就会增大;
- 但网络中出现了拥塞,cwnd 就减少;
那么怎么知道当前网络是否出现了拥塞呢?
其实只要发送方没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了拥塞。
拥塞控制主要是四个算法:
- 慢启动
- 拥塞避免
- 拥塞发生
- 快速恢复
慢启动
TCP 在刚建立连接完成后,首先是有个慢启动的过程,这个慢启动的意思就是一点一点的提高发送数据包的数量,如果一上来就发大量的数据,这不是给网络添堵吗?
慢启动的算法记住一个规则就行:当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。
举个例子:这里假定拥塞窗口 cwnd 和发送窗口 swnd 相等
- 连接建立完成后,一开始初始化 cwnd = 1,表示可以传一个 MSS 大小的数据。
- 当收到一个 ACK 确认应答后,cwnd 增加 1,于是一次能够发送 2 个。
- 当收到 2 个的 ACK 确认应答后, cwnd 增加 2,于是就可以比之前多发2 个,所以这一次能够发送 4 个。
- 当这 4 个的 ACK 确认到来的时候,每个确认 cwnd 增加 1, 4 个确认 cwnd 增加 4,于是就可以比之前多发 4 个,所以这一次能够发送 8 个。
如下图所示:
可以看出慢启动算法,发包的个数是指数性的增长。但是也不可能一直增长吧,那什么时候是个头呢?
有一个叫慢启动门限 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 大小的数据,变成了线性增长。
如下图所示:
所以,我们可以发现,拥塞避免算法就是将原本慢启动算法的指数增长变成了线性增长,还是增长阶段,但是增长速度缓慢了一些。就这么一直增长着后,网络就会慢慢进入了拥塞的状况了,于是就会出现丢包现象,这时就需要对丢失的数据包进行重传。当触发了重传机制,也就进入了拥塞发生算法。
拥塞发生
当网络出现拥塞,也就是会发生数据包重传,重传机制主要有两种:
- 超时重传
- 快速重传
我们首先来说说超时重传:
当发生了超时重传,则就会使用拥塞发生算法,这个时候,sshresh 和 cwnd 的值会发生变化:
- ssthresh 设为 cwnd/2
- cwnd 重置为 1
接着,就重新开始慢启动,慢启动是会突然减少数据流的。这真是一旦超时重传,马上回到解放前。但是这种方式太激进了,反应也很强烈,会造成网络卡顿。
那快速重传呢?
TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,则 ssthresh 和 cwnd 变化如下:
cwnd = cwnd/2
ssthresh = cwnd
- 进入快速恢复算法
快速恢复
快速重传和快速恢复算法一般同时使用,快速恢复算法是认为,你还能收到 3 个重复 ACK 说明网络也不那么糟糕,所以没有必要像 RTO 超时那么强烈。那快速恢复算法是咋样的呢?
- 拥塞窗口
cwnd = ssthresh + 3
( 3 的意思是确认有 3 个数据包被收到了) - 重传丢失的数据包。
- 如果再收到重复的 ACK,那么 cwnd 增加 1。
- 如果收到新数据的 ACK 后,设置 cwnd 为 ssthresh,接着就进入了拥塞避免算法。
如下图所示:
也就是没有像超时重传一夜回到解放前,而是还在比较高的值,后续呈线性增长。
好了,到这里我们的 TCP 可靠传输机制就讲解完了,我们下次再见!
引用: