前言

大家都知道因特网中传输层的两大协议是面向连接的、提供可靠传输的 TCP 协议和面向无连接不提供可靠传输的 UDP 协议,那么 TCP 协议是怎么实现可靠传输呢?接下来会从怎么设计可靠传输协议来一步一步的揭开 TCP 实现可靠传输的面纱。本篇文章会先介绍可靠传输原理和 GBN 协议以及 SR 协议,后面的文章会介绍 TCP 的可靠传输原理以及它的流量控制、拥塞控制等。

如果要让我们设计一个传输层协议来保证数据的可靠传输,我们应该怎么来做呢?要保证可靠传输,要做到“不错”、“不丢”和“不乱”。

“不错”,指的是传输的数据不能错误,比如发送端发送 11110000,接收端收到的一定也是 11110000,而不能是别的数据。数据在信道上传输可能会发生错误,比如收到外界的干扰,在物理线路上传输的高电平可能变成了低电平,低电平也可能变成了高电平。

“不丢”,指的是在传输的过程中,不能出现丢包。比如当路由器的缓存满后,再来的分组会被路由器丢掉,这样接收方就不能正确收到了,我们要设计的协议就要保证路由器丢包后接收方还能正确接收。

“不乱”,指的是传输的分组不能是乱的,比如说 分组10分组1 先到达,或者一下子收到好多个 分组1,再交付给上层协议时,一定要保证分组是按顺序的并且没有重复的。

物理信道的不可靠性就决定了可靠传输协议必定是复杂的。可以采取假设的思想来一步步实现 rdt(Reliable Data Transfer,可靠数据传输协议)协议。

rdt 实现原理

rdt 1.0

假设信道上不会出现位错误(有时候也称作位翻转),不会出现丢包的现象。那这个就很简答了,发送方只管发送数据,接收方只管接收数据就行。

rdt 2.0

显然 rtd 1.0 太过于理想化了,现实中的信道并不像我们假设的那样。现在假设信道上不会发生丢包,但是可能会发生位错误的,也就是说发送方发送的 1 到接收方变成了 0,或者发送方发送的 0 到接收方变成了 1。怎么解决这种信道上可能发生的位错误来保证呢数据的可靠传输呢?
可以加入校验和,也就是说发送方在发送数据的最后加上一个校验和,接收方通过校验和来校验发送方发过来的数据有没有发生错误。如果没发生错误就通知发送方发送下一个包,如果发生错误就通知发送方重新发送本次数据。在接收方检测到数据正确后可以返回给发送方一个 ACK(ACKnowledge Character,肯定字符),检测到错误后返回给发送方法一个 NAk (Negative Acknowledgment,否定应答)。

rdt 2.0 在可能会发生位错误,但不会丢包的假设下,通过校验和机制能实现可靠数据传输。但它有什么问题吗?很明显,如果接收方返回的 ACK 或者 NAK 在传输过程中发生了错误,发送方接收到错误的数据后就一脸懵逼了,是该继续发送下一个包呢,还是重传上一个包呢?带着这个问题我们一起进入 rdt 2.1

rdt 2.1

rdt 2.0 的致命缺陷就是 ACK 或者 NAK 发生错误后,发送方不知道该怎么办了。怎么解决这种问题呢?有以下几种思路:

  • 为 ACK/NAK 增加校验和,检错并纠错?检错可以,但是纠错是不是成本更高了呢?
  • 发送方接收到被破坏的 ACK/NAK 时不知道接收方发生了什么,添加额外的控制消息?这是否可行呢,显然控制信息越多,传输的数据的效率就越低。
  • 如果 ACK/NAK 坏掉,发送方重传?不能简单的重传:产生重复分组,接收方怎么做?

其实可以在发送方为每一个分组添加一个序列号(Sequence number),在发送方收到出错的 ACK 或者 NAK 后就重传分组,在接收方收到重复的分组后丢弃即可。
流程图如下所示(以 ACK 在传输过程中发生错误为例,NAK 也一样):

上面的方法似乎无完美的解决了 ACK 或者 NAK 出错后导致发送方不知道该怎么办的问题。但是想想真的需要两个应答类型吗?即用一个 ACK 是否能代替以前的 ACK 和 NAK 呢?

rdt 2.2

针对上面的问题,当然可以用一个 ACK 来达到应答,具体就是 ACK 后面跟上序列号,这个序列号表示接收端已经接收到该序列号以及之前所有包的正确分组。

rdt 3.0

通过上面的假设我们已经能在只发生位错误,不发生丢包的信道上用校验和和 ACK 机制实现了可靠数据传输。但是现实世界总是那么的残酷,如果信道既可能发生位错误,也可能丢包,该怎么设计可靠传输协议呢?

在信道上发生位错误的问题,之前已经有办法完美的解决了,现在要考虑的问题是如果发生丢包,该怎么解决?

1.假设发送方发送的包在传输过程中发生了丢包,此时发送方是不会收到接收方返回的 ACK 的,这时候也不能傻傻的等着啊,所以需要设置一个“合理”的时间(这个合理的时间设置有一定的难度,以后的文章会重新提到),如果在这个时间内没有收到 ACK (不管是因为丢包还是延时),发送方就会重传之前发的包。显然,这样需要设置一个定时器来定时了。

2.假设 ACK 丢失了或者超时,也能利用定时器超时重传的方法来解决。

这样看起来,rdt 3.0 在信道可能发生位错误,可能丢包的情况下能保证可靠数据传输。但是效率是不是太低了呢?效率低的原因是因为停等机制导致的,即发了一个分组之后一直等到确认了这个分组才发下一个。怎么优化呢?

流水线机制

上面 rdt 3.0 效率低的问题是因为发了一个分组之后一直等到确认了这个分组才发下一个,可不可以在未确认之前连续发送多个分组呢?答案是可以用流水线机制来提高效率。即在收到确认之前发送多个分组。

滑动窗口协议

有了流水线机制,我们是不是就能在未确认之前连续发送无数的分组了呢?这就好比工厂上的流水线上,前面的工序处理的非常快,后面处理的比较慢,这样就会导致整条流水线瘫痪。同样,发送方如果不限制发送速率的话,可能造成路由器大量的丢包,接收方如果处理不过来的话也势必造成数据传输失败。滑动窗口协议就能很好的解决这一问题,能控制发送方发送的速率,从而达到路由器不出现大量丢包以及接收方能处理过来接收到的数据。滑动窗口协议控制发送方发送速率主要是通过控制滑动窗口大小来实现的。比如说设置滑动窗口为 5,即表示发送方在未确认之前最多能连续发送 5 个分组,如果未收到接收方的确认就不能再发送新分组,只有收到 ack 后,发送方才能再次发送新分组(如果有的话)。

滑动窗口管理着发出去但是还未被确认的分组,当收到一个 ACK 后,窗口便能向后滑动了(滑动窗口名称的来源)。例子如下所示:假设滑动窗口的大小为 4

第一步,发送方先后发送 4 个分组,这时候滑动窗口满了便不允许发送方发送数据。

第二步,接收方收到一个 ack ,这时候滑动窗口便向后移动,发送方就可以重新发送一个分组了,在本例中即发送分组 5

第三步,接收方再次收到了一个 ack,滑动窗口再次向后移动,以此类推

GBN(Go-Back-N,回退 N 步)

在发送方:

  • 窗口大小为 N,最多允许 N 个已发送但未被确认的分组
  • 收到 ACK(n)表示确认到序列号 n 的分组均已被正确接收,注意可能会收到重复的 ACK
  • 为分组设置一个定时器,当发生超时时间时,会回退 N 步重新传整个窗口中分组(GBN 名称的由来),即一旦发生超时,就回重新传送整个在窗口中的分组,不管接收方是否收到之前窗口中的分组

如果有乱序到达的分组到达接收方

  • 乱序到达后直接丢掉,接收方没有缓存
  • 重新确认序列号最大的、按序到达的分组

SR (Selective Repeat,选择重传)

想一下上面的 GBN 有上面缺陷?很明显单个分组的差错就能够引起 GBN 重传大批分组,还有分组已经到达接收方了,但发现不是自己期望的分组会直接丢掉,是不是可以先存到接收方那里以提高效率呢?当然可以,只不过接收方需要设置一个接收缓存。于是就有了 SR 协议,它的做法是

  • 发送方给每一个分组设置单独的定时器,这样发生超时后只传本分组就可以
  • 在接收方缓存已经收到的乱序分组

GBN 和 SR 对比

在发送方,GBN 只给一个窗口设置一个定时器,而 SR 是为窗口中每一个分组都设置一个定时器。当定时器超时时,GBN 重传整个窗口中的分组,SR 只重传定时器超时的那一个分组。
在接收方,GBN 没有缓存,乱序到达的分组直接丢掉,并回复收到的最大分组,SR 缓存乱序到达的分组,并回复 ACK 已经收到分组。

写到这里,本篇基本上就要结尾了,可以想想 TCP 更像 GBN、SR 中的哪一个呢?TCP 为什么要有流量控制和拥塞控制?UDP 不需要流量控制和拥塞控制吗? UDP 不进行流量控制和拥塞控制不会造成网络的瘫痪吗?这些问题以后的文章会慢慢解答。