字节面试被虐后,是时候搞懂 TCP 数据可靠传输了

1,253 阅读11分钟

本人这段时间参加了一些面试,先总结一下被问到的关于 TCP 的问题

  1. 描述一下 TCP 的三次握手,第一次握手的报文段里除了 SYN 标志位,还包含了什么?
  2. 描述一下 TCP 的四次挥手,为什么不能是三次挥手?第四次挥手客户端发出最后的确认报文段之后,就会马上断开连接吗?
  3. TCP 是一个包发送出去并得到确认后,才可以发送下一个包,还是无需等待确认就可以发送下一个包?
  4. TCP 和 UDP 的区别是什么?
  5. TCP 是怎么保证可靠数据传输的?
  6. TCP 是如何进行流量控制的?
  7. TCP 是如何进行拥塞控制的?
  8. TCP 的慢启动是什么?快启动又是什么?

以上每一个问题,都是我在不同的面试中被问到过的,其中 2、3、4、5、8 都是在同一轮字节面试中被问到的,可见把网络基础打实还是很重要的。对于数据的可靠传输这块,我之前的理解不太完整,知道 TCP 有丢包重传的机制,但是要我比较清晰地描述出来,还是比较困难。

所以打算通过这篇文章来捋一捋。如有不妥之处,希望大家能够帮忙指出,共同进步。

什么是可靠数据传输

TCP 的可靠数据传输服务确保一台主机上的一个进程,发送给另一个主机上的一个进程的数据,是无损和按序的。

序号、确认号

TCP 报文段由首部字段和一个数据字段组成,数据字段包含了应用数据,首部则包含了跟三次握手、流量控制、拥塞控制、可靠传输等相关的一系列字段,下图展示了 TCP 报文段的首部结构:

TCP 报文段结构.png

本文我们只关心跟可靠数据传输相关的首部字段,也就是序列号和确认号,这两个字段是 TCP 可靠数据传输的关键部分。上图的单位为字节,所以序列号和确认号各占 4 个字节,即 32 比特。

我们先假设主机 A 和主机 B 建立了 TCP 连接,它们可以向对方发送数据。

  • 序号

比如主机 A 发送给主机 B 一个序号为 1000 的报文段,且该报文段中包含 500 字节的数据,那么,主机 A 下一个发往主机 B 的报文段的序号就应该是 1500,注意,不是 1001。

序号字段包含了该报文段首字节的字节流编号。 注意,序号不都是从 0 开始,并且一般不会从 0 开始,发送端和接收端在三次握手时,会随机生成一个初始序号,并告诉对方,接下来我传输的数据会从这个序号开始。

  • 确认号

比如主机 A 发送给主机 B 一个序号为 1000 的报文段,且该报文段中包含 500 字节的数据,那么主机 B 在发送给主机 A 的 ACK 报文段中,就会将确认号置为 1500,表示 1500 之前的数据我已经都拿到了,接下来你给我发 1500 之后的数据吧。而主机 A 在收到这个 ACK 报文后,就会知道主机 B 已经收到了 1500 之前的那个报文段。

主机 B 填充进报文段的确认号,是它期望从主机 A 收到的下一字节的序号,同时主机 B 也是在通过这个确认号告诉主机 A,之前的报文段我已经收到了。

TCP 连接是一条“流水线”

我们思考一下文章开头的第 3 个问题:TCP 是一个包发送出去并得到确认后,才可以发送下一个包,还是无需等待确认就可以发送下一个包?

让我们想象一下,如果是一个包发送出去并得到确认后,才可以发送下一个包,也就是需要等待至少一个 RTT(往返时延),才能发送下一个包,如果算上在发送方和接收方的底层协议处理时间、中间任何一台路由器的处理与排队时延,这个等待时间会更长。这样的性能是无法接受的。

如果某个协议采取了上面描述的这种行为,我们称之为停等协议,显然,TCP 不是停等协议,TCP 允许发送方发送多个报文段而无须等待确认,这些报文段就像被填充到一条流水线里一样,所以这种技术被称为流水线

发送端的限制

那在流水线上可以同时存在的报文段(已发送但未被确认的报文段)的数量是没有限制的吗?

显然不是。

有限制,并且这个限制会根据接收端的接收能力和网络的拥塞情况做调整。在接收端的接收能力比较强或者网络比较不拥堵的时候,允许发送更多的报文段;相反,在接收端的接收能力比较弱或者网络出现拥堵的时候,允许发送较少的报文段。这就涉及到 TCP 的流量控制拥塞控制,本文不做展开。

现在我们只需要知道,每个 TCP 发送方会维护两个变量,一个是 SendBase,表示最小的未确认报文段的序号,另一个是 NextSeqNum,表示最小的未使用序号(即下一个待发报文段的序号),下图将序号范围分割成了 4 段:

TCP 发送窗口.png

“发送,还未确认” 以及 “可用,还未发送” 的序号,就是当前可用的序号,大于或等于 SendBase + N 的序号是不能使用的。一旦“发送,还未确认”的报文段数量达到了 N,这个 TCP 发送方就无法再发送下一个报文段。N 通常被称为窗口长度。我们假设窗口长度为 4,每个报文段的数据大小都为 1 个字节,且发送方始终有包可发,下图展示了无丢包时的数据传输过程:

TCP(不丢包).png

(图中接收方维护一个接收窗口,跟发送方的窗口完全不是一个东西,大家不要混淆,在本文的讨论中可以忽略接收窗口的存在)

由图可见,发送了报文段 3 之后,窗口满,发送方就无法继续发送报文段 4。直到 ACK 0 到达发送端,TCP 会更新 SendBase 的值,前面我们提到过,SendBase 指的是最小的未确认报文段的序号,此时这个序号应为 1,所以发送端会将 SendBase 置为 1,将窗口右移,变为 1 2 3 4,这时候,序号为 4 的报文段才能够被发送。

想象一下,如果由于某些网络原因,上图中的 ACK 0、ACK 1 丢失,当 ACK 2 到达发送端的时候,窗口应该怎么变化?

bqb9.png

公布答案,发送端收到 ACK 2 之后,就会直接将窗口右移,窗口变为 3 4 5 6,是不是有点小意外?怎么不管 ACK 0 和 ACK 1 了?

之所以会这样,是因为 TCP 采用的是累积确认,ACK 2 实际上是在告诉发送端,2 及 2 之前的所有字节我都已经收到了,这个 ACK 是在确认一个或多个先前未被确认的报文段。因此发送端会直接将 SendBase 更新为最小的已发送待确认的序号,也就是 3。

了解了窗口的概念,以及 TCP 是如何通过窗口对发送端进行限制的之后,我们就可以进一步来看看 TCP 是怎么保证数据的可靠传输的。

可靠数据传输的原理

数据可靠传输的本质,简单来说,就是我把一个报文段发出去之后,我要知道对方有没有成功收到这个报文段,当我发现对方没收到时,就得重新发送一次这个报文段,直到对方确认收到为止。

TCP 是怎么感知到丢包的?

TCP 遇到以下两种情况时,就会认为丢包了

定时器超时

TCP 发送端会维护一个单一定时器,我们可以简单地理解为,定时器是跟窗口绑定的,定时器始终监控着窗口中第一个包的状态,窗口发生变化,就必须重启定时器。于是,定时器超时意味着,当前窗口最前面的包丢失了,也就是 SendBase 对应的包丢失了,需要重传这个包。

下图展示了定时器超时的情况下,TCP 是怎么处理的:

TCP(超时).png

结合上图,我们可以发现:

  • 当发送端接收到 ACK 0 和 ACK 1 后,窗口向右移动,有效窗口为 2 3 4 5,此时的 SendBase 是 2,之后窗口就没有再右移

bqb10.png

“你刚刚不是说,TCP 采用的是累积确认吗?报文段 2 丢失了,没有 ACK 2,那后面要是收到了 ACK 3、ACK 4,窗口不就右移了?”

emmm...

甚是有理,但 TCP 不会干这么自相矛盾的事情,我们再仔细看看上面的图,接收方在收到报文段 3、4、5 的时候,并没有回复对应的 ACK 3、4、5,而是都回复了 ACK 1(原因我们下一节会细讲),所以发送端根本收不到 2 及 2 之后的任何 ACK。

再等等,这,是不是刚好验证了 TCP 的累积确认?如果接收端在还没收到报文段 2 的时候,收到了报文段 3,就回复了 ACK 3,那这个 ACK 3,并不能达到累积确认的效果,因为报文段 2 实际上还不能被确认。有点绕,你细品。

好像扯得有点远了...我们回到定时器

  • 从图中可以看到,定时器超时的时候,当前的窗口是 2 3 4 5SendBase 是 2,说明报文段 2 丢失了,发送端于是重传报文段 2

  • 大家注意,接收端收到序号为 3、4、5 的失序报文时,并不会将其丢弃,而是将其缓存起来,等到期望报文 2 到达后,再将 2、3、4、5 一并交付给上层应用

收到 3 个冗余 ACK

在上面的例子中,我们提到了,当接收端收到序号为 3、4、5 的报文段时,由于还没有收到序号为 2 的报文段,接收端不会回复 ACK 3、4、5,而是统一回复了 ACK 1,这几个 ACK 1 就是冗余 ACK

当 TCP 接收端收到比期望序号大的失序报文时,会立即发送冗余 ACK,对最后一个按序的报文段进行确认,将冗余 ACK 的确认号置为期待的下一个按序的序号,告诉发送端,这个序号的报文我还没收到呢,怎么就给我发了更大序号的报文?

还是看一个例子:

TCP(冗余 ACK).png

如图所示,接收端收到序号为 3、4、5 的失序报文段时,会返回 ACK 1,ACK 1 是对序号为 1 的报文段的确认报文,该确认报文会将确认号置为 2,表示我期望的下一个报文段的序号是 2,怎么 2 还没收到,就给我发了 3、4、5,赶紧给我发 2 的报文段。

而发送端在定时器过期之前,连续收到了 3 个 ACK 1,发送端就会意识到,这个被冗余确认了 3 次的报文段后面的那个报文段(报文段 2)已经丢失了。这时候,TCP 发送端就会进行快速重传,即不用等到定时器过期,立即重传丢失的报文段。

TCP 意识到丢包后,做了什么操作?

其实上面的内容里已经提到了,TCP 意识到丢包后,就会重传丢失的那个报文段。

对于通过定时器超时意识到的丢包,TCP 会重传具有最小序号但仍未被确认的报文段;通过冗余 ACK 意识到的丢包,TCP 会重传冗余 ACK 中的确认号对应的报文段。

小结

TCP 通过两种方式判断丢包,定时器超时,或收到 3 个相同确认号的冗余 ACK。前者通过观察当前窗口,判断丢的是哪个包,后者通过冗余 ACK 里的确认号,判断丢的是哪个包。意识到丢包后,TCP 会重传该包。

总结

本文举的都是很简单的例子,实际的场景要比这复杂得多,涉及到的知识点也很多。TCP 是前端面试的常见问题,想要进大厂的朋友,只简单地了解三次握手、四次挥手已经远远不够了(当然如果你有其他突出的亮点就当我没说)。共勉。

参考

计算机网络:自顶向下方法