TCP 是一套相当复杂的协议,包含的内容也非常多,面试也非常常见,不少新手面试这种面试,一头雾水,不知道如何下手,也不知道从何看起,拿起 TCP/IP 详解,也找不着重点,看两页就犯困。

为了解决大家的困惑,花了两天的时间,帮大家梳理一下,作为一名开发者,应该需要重点掌握哪些 TCP 知识。当然,由于只有一篇文章,不可能面面俱到,否则就要爆炸了!因此,我挑了重点中的重点来介绍。

关于参考书:

是的,本文几乎所有内容在上面的两本书你都能找到(除了实验之外),我当然不是抄书,那样有什么意义呢?本文的目的是梳理,就像老师划的重点一样,你才有复习的方向,特别适合那些时间紧迫,但是又来不及完整去看一遍书的同学。

提示:

接下来,我们拿起大刀—— tcpdump,开始 TCP 之旅吧。

1. 三次握手和四次挥手

这里打算使用借助三次握手和四次挥手,来熟悉一下 tcpdump 工具,后面你会多次使用这个工具,没用过的同学一定要认真看,工作的时候你也会用上它的,尤其是在分析网络数据传输异常的时候!

面试tcp ip问什么(面试需要知道的)(1)

图1 三次握手与四次挥手

上图是我使用 nc 命令配合 tcpdump 抓包工具演示的一次三次握手与四次挥手的过程。这个实验非常简单,只要你有搭载 Linux 或者 MacOS 的主机,就可以轻松的进行这个实验。实验步骤如下:

不出意外,你就能在下方的窗口里看到抓取到的三次握手的报文了。接下来,你顺便可以观察一下四次挥手的过程。步骤如下:

一旦退出客户端,你就能看到四次挥手的过程了。如果你在阿里云或者腾讯云有服务器的话,那就更好了,你可以在你的服务器上使用 nc 命令启动一个 TCP 服务器,在本地使用 nc 命令连接,这样更加真实哦!

好,咱们简单分析一下 tcpdump 命令的参数含义,以及报文的含义。

1.1 tcpdump

tcpdump 是在类 Unix 环境下的抓包神器,在你的 Linux 或 MaxOS 系统上都是默认安装好的,它可以非常方便的抓取网卡上的数据包,并且可以根据你指定的参数进行过滤。在上面的实验中,各个参数含义如下:

面试tcp ip问什么(面试需要知道的)(2)

更多的参数,你可以使用 man tcpdump来查阅文档,它的文档非常详尽,你可以找到关于 tcpdump 的一切。另外,MacOS 和 Linux 上的 tcpdump 有一点区别,但是这些影响都不大。

1.2 报文含义

结合图 1,分析一下每一包的含义。注意,这里使用 ACK 表示标志位,使用小写 ack 表示序号。 另外,C 表示客户端,S 表示服务器(就是使用 nc -l 8000 的那个)。

面试tcp ip问什么(面试需要知道的)(3)

上表是图 1 中的报文简化后的情况,这里提取了一些关键数据。

关于三次握手,待会在第 5 节,还有更重要的内容!这里只是让你先适应一下 tcpdump 工具,以及放松下心情。

2. Delay ACK

接下来的事情就好玩了,这是一个非常重要且鲜有人提起的东西,称之为 Delay ACK(延时确认)。话说,它到底是个什么玩意儿?很重要吗?废话不多说,先来做个实验。

面试tcp ip问什么(面试需要知道的)(4)

图2 Delay ACK

来看一个发生在互联网上的例子,这次我的服务器位于腾讯云主机上。建立 TCP 连接后,我从客户端发送了 4 次数据到一个名为 mars 的服务器上,第一次发送一个字母 a 再加一个字节的回车 \n,第二次发送了一个字母 b 加回车 \n,后面还有 c, d 同理。

echo.go(点我下载) 是我用 golang 写的一个简单的 TCP 服务器,默认情况下,这个服务器什么也不干,只管收数据,就像第 2 节里使用的 nc 命令一样,不过 echo.go 收到数据直接丢弃了,甚至也不显示在屏幕上。

如果你还没有安装 golang 编译器,我也为你编译好了一个 echo,就放在代码库里,你可以直接运行。不过还是强烈建议你自己安装一下 golang,安装方法,请参考文末,没把链接贴在这里,是因为我希望你不要现在就去尝试安装,等你看完文章再去做这件事。

这次在抓包程序 tcpdump 运行在服务器上,因为我想观察服务器端是如何回复 ACK 报文段

这次就不再分析三次握手和四次挥手了,从图 2 上看太简单了,一目了然是吧。重点放在服务器接收 4 次数据的行为,正好对应 TCP 的 4、5、6、7、8、9、10、11 号报文。同样这里我们用表格记录一些关键信息。

面试tcp ip问什么(面试需要知道的)(5)

一些观察到的现象:

这里你需要关注的问题是,服务器接收到数据后,为什么有时候没有立即返回 ACK,而要等待 40ms 呢? 很好,我希望你能看到这个现象,这不是巧合,而是 TCP 的特性,是一种机制,它就是 Delay ACK,即延迟 ACK。

为什么我没有使用 nc 继续做服务器,因为在我的 Linux 系统上,这个机制默认是关闭的,如果使用 nc 命令,你可能看不到这种现象,因此我使用 golang 写了一个简单的 TCP 服务器,来开启 Delay ACK 这个机制。当然,我希望你在你的服务器上使用 nc 工具尝试实验一下,也许能看到,也许看不到,具体取决于你机器的内核版本。

TCP 为什么要引入这种机制呢?目的是为了减少网络中 TCP 报文段的数量。在过去,带宽那可是相当的贵,其实现在也不便宜。你知道,一个 TCP 首部至少需要 20 字节(稍后会帮你梳理一下 TCP 首部字段),而引入了 Delay ACK,就可以做两件事:

好,接下来再谈谈 echo 是怎么把 Delay ACK 机制打开的。非常简单,你使用 man 手册查阅 man 7 tcp,就能看到一些关于 TCP 机制的文档,其中有一项是 TCPQUICKACK。

面试tcp ip问什么(面试需要知道的)(6)

图3 TCPQUICKACK

不过这个选项的名字和 Delay ACK 的含义是相反的。这意味着,如果你想开启 Delay ACK,你就得把这个选项设置成 false。Linux 提供了 setsockopt 系统调用来帮你设置,关于这个函数,你可以使用 man setsockopt 来查阅。

int opt = 0; setsockopt(sockfd, IPPROTO_TCP, TCP_QUICKACK, &opt, sizeof(int));

另外,你需要在每次 recv 数据后,都需要调用一下这个设置函数,因为在 man 手册中有明确说明,这个选项设置并不是永久的。

在有些低版本的内核里,Delay ACK 机制默认就是开启的哦!不过具体还需要你自己进行实验。

3. Nagle

Nagle 也是 TCP 协议中常见的算法,而且面试也会经常问。Nagle 算法的目的,也是为了减少网络中 TCP 报文段的数量。(你看,为了减少网络中报文段的数量,TCP 协议搞了很多机制,包括上一节学习的 Delay ACK。)

Nagle 算法的发明者 John Nagle 当时发明了这个算法,主要是解决福特汽车公司的网络拥塞问题。

Nagle 算法原理相当简单:

  • 一个 TCP 连接上最多只能有一个未被确认的未完成的小分组,在它到达目的地前,不能发送其它分组。
  • 在上一个小分组未到达目的地前,即还未收到它的 ACK 前,TCP 会收集后来的小分组。当上一个小分组的 ack 收到后,TCP 就将收集的小分组合并成一个大分组发送出去。

上面的分组说的就是 TCP 报文段,一个意思。不过有一点值得注意,Nagle 算法关心的是小分组,也就是大分组它并不管。分组要多小才算是小呢?一个字节?两个字节?一般来说,只要数据量小于 MSS,就是小分组。MSS 在大小在三次握手的时候就协商好了,不信你回去看看图 1 或者图 2(虽然图 1 中的 MSS 大的有点过分,毕竟是环回网卡上的报文)。

为了验证 Nagle 算法的确是存在的,再来个实验吧。

3.1 实验一(观察 Nagle 算法的存在)

这个实验看起来没那么容易做,如何在极短的时间里发送多个小分组呢?继续使用 nc 命令可以吗?第一次发送 a,第二次再输入一个 b回车发送,第三次输入c 回车发送出去。很遗憾,哪怕你单身 30 年,你的手速也不可能突破到 100ms 以内,还没等你 b 输入进来,a 的 ACK 就已经收到了,所以这样实验的话,你永远看不到 Nagle 是如何合并小分组这个过程。

既然如此,手速不够快,C/C 写起来太费事,咱们直接用 Python,方便快捷学习网络编程的神器。

  • 开三个窗口,一个抓包,一个服务器,还有一个客户端,写 Python 脚本。
  • Python 只要写 4 行就行了,重点在于使用 for 循环连续调用 5 次 send 的过程。

图 4 是实验的结果。具体代码我就不贴进来,你自己敲一遍,记得更加清楚。

面试tcp ip问什么(面试需要知道的)(7)

图4 Nagle 算法观察

3.2 结果分析

简单看一下图 4 的结果。

  • 第 4 包是第一次调用 send 发送的数据,只有一个字节的 a,中间经过了约 30ms 后,收到了第 5 包,也就是 ACK 报文。
  • 第 6 包是后 for 循环的后 4 次合并的数据,一共是 4 个字节,即 aaaa,一次全部发送出去了。所以可以看出来,Nagle 算法默认就是开启的。后面我们要想办法把它关闭。

实验非常简单,非常容易就验证了 Nagle 算法它的确是存在的,是不是很开心?但是在服务器开发中,通常我们都希望关闭 Nagle 算法,因为在互联网技术如此发达的现在,网速已经足够快了,也不那么拥堵,开启 Nagle 反而会影响程序的响应速度。

如果这时候对方再开启 Delay ACK 机制的情况下,发送方收到 ACK 的时间会拖慢 40ms(Delay ACK 简直就是猪队友),这在某些场景下几乎是无法接受的。想想你玩王者荣耀的时候,那可是毫秒必争啊,从 60ms 变成 100ms 那可能就是个人头的问题。

3.3 实验二(关闭 Nagle)

实验二自然就是关闭 Nagle 算法啦,非常简单,只要设置 TCP_NODELAY 选项就可以了,继续在刚刚的 Python 终端里键入命令 s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1),然后再连续发送 5 次报文。

面试tcp ip问什么(面试需要知道的)(8)

图5 关闭 Nagle

从上面的结果看到,有了十分明显的变化,第 8 到 12 包显然直接从客户端发送出去,没有合并报文。整体的分组数量(8~17号报文)相比开启 Nagle(4~7号报文)多了 6 包出来,如果忽略掉那 1 字节的数据,只看 TCP 首部的 20 字节,也就是多了 120 字节的在网络中。

从这个角度来看 Nagle 算法,可以发现 Nagle 对网络十分友好,而关闭 Nagle 可能会让网络造成拥塞(网络中充斥着小分组)。

3.4 Nagle 算法的影响

我们来看一个实际场景(这个例子来源于《Unix 网络编程》一书)。假设你基于 TCP 协议设计了一个应用层协议,这个协议一共有 400 字节的数据,其中首 4 个字节是消息的类型,剩余的 396 字节是数据。

如果客户端分两次发送数据,第一次发送 4 字节的数据类型,第二次再发送 396 字节的数据,也就是连续调用两次 send 函数。服务器收到这个 4 字节却什么都不能做,只能继续等待剩余的 396 字节。假设网络延时(RTT)是 1ms,另外服务器如果开启了 Delay ACK 机制,同时客户端开启了 Nagle 算法,那将会是这样一种情形:

  • 客户端发送 4 字节出去,经过 41ms 后(服务器的 Delay ACK 40ms 网络延时 1ms),收到了服务器返回的 ACK。
  • 客户端继续发送剩余的 396 字节。

这中间几乎有 40ms 的时间几乎浪费在了 Delay ACK 上,这简直不能忍啊!如果关闭 Nagle 算法,程序性能会有极大的提升,因为不必等待服务器的 ACK 返回,剩下的 396 字节就能直接发送出去。

不过《Unix 网络编程》一书(7.9节 TCP Socket Options)提到,关闭 Nagle 并不是最好的方案,作者认为这是一种有损于网络的做法,正确的做法是应用层自己把报文合并,一次调用 write 函数。我认为作者说的完全没有问题,但是这个语境我觉得放在网络延时较长的链路中更加合适(比如 RTT 在 60ms 以上)。通常在局域网中,网络延时几乎可以忽略,基本上都会选择关闭 Nagle,这主要是防止 Delay ACK 这个猪队友让你的网络程序性能变差。

而服务器端,最好也应该要开启 TCP_QUICKACK 选项(如今的 Linux 默认已经开启),关闭 Delay ACK 机制。

4. 流量控制与拥塞控制

TCP 协议的面试中,你经常会看到这两个名词,面试官基本上都会考察一下你是否对 TCP 真的熟悉,就会简单问一下,说说 TCP 流量控制算法是做什么的,拥塞控制呢?

其中有一个算法,直接体现在了 TCP 首部字段中,它就是流量控制算法,对应的字段是窗口大小。很多人分不表这两个算法之间的异同点。下面简单总结一下:

  • 相同点
  • 它们都是为了控制发送数据量的大小。
  • 不同点
  • 流量控制,是根据接收者能力情况,来控制发送数据量。
  • 拥塞控制,是根据网络的拥塞状态,来控制发送数据量。

最后实际要发送的数据量,取决于流量控制和拥塞控制计算出来的发送数据量的较小者。通常用 rwnd 来表示接收方的接收能力,用 cwnd 表示链路还能承载的数据能力,最终要发送的数据量是:

面试tcp ip问什么(面试需要知道的)(9)

在流量控制算法中,对端的接收能力是指对端还能接收多少数据,这个数值体现在对方发送给你的 TCP 报文的首部字段 winsize 中。比如对方说,它只能接收 100 个字节,那你就只能再给它发送不超过 100 字节的数据。如果你要发送的数据超过了 100 字节,抱歉,除去 100 字节剩下的部分就只能先暂存在本地的发送缓冲区中。

关于流量控制的经典算法,就是滑动窗口算法了,限于篇幅这里就不具体介绍了,你可以参考这篇文章《滑动窗口算法》。我们需要把更多的时间放在拥塞控制算法上。

4.1 拥塞控制算法

拥塞控制算法的目的,就是为了防止网络在拥塞的情况下,还在疯狂的向网络中发送大量数据。那么这里就有一个值得关心的问题:发送者,如何知道网络拥塞?

4.1.2 慢启动

在建立完连接后,发送方有办法知道网络的拥堵情况吗?显然不能,那怎样才能知道?想必你能猜出来,没错,只能试探。TCP 采取的策略就是试探,而且把这种方法取名为慢启动

如果在发送过程中,遇到了重复 ACK 或者超时的情况,需要减慢发送速度:

  • 连续收到 3 次对方重复的 ACK 确认
  • 这意味着对方极有可能没有收到数据,几乎可以认为丢包了。但这并不代表网络拥塞,甚至网络状况还不错呢。(稍后解释。)
  • 如果超时未收到 ACK,说明极有可能拥塞
  • 对方可能没收到报文
  • 对方收到报文,但 ACK 丢了

一定要严格区分,重复 ACK超时这两种情况,它影响了 TCP 拥塞算法做何种决策!!!

面试tcp ip问什么(面试需要知道的)(10)

图6 慢启动(一)

面试tcp ip问什么(面试需要知道的)(11)

图7 慢启动(二)

图 6 和图 7 是我抓取的到一段报文,可以看到一开始客户端发送的速度并不算很快,一次发送两个报文,经过一段时间后,就变成一次发送 10 多个报文。不过从抓取的数据包上看,并未出现丢包的情况,网络状况非常好。你也可以自己找一台机器进行实验,实验过程非常简单,写 4 行 Python 语句即可。

如果 TCP 在发送中途,遇到丢包或超时情况,那就必须用减慢发送速度,一次少发一些报文段。比如一次发送 16 个报文段时,出现了异常(三次重复 ACK 或超时),那下次发送的时候数量减半,一次发送 8 个。

关于慢启动的实验,这篇《慢启动》做的实验更加清晰且容易观察,你可以参考一下。

4.1.3 慢启动算法

最经典,最原始的慢启动算法是这样的:

在程序中,维护一个变量 cwnd,表示拥塞窗口大小,单位是字节。在最开始,cwnd 有一个初始值,RFC 2581 规定,它的大小不超过 2MSS。为了方便以后的描述,当我说 cwnd = 2 时,实际上是说 cwnd = 2MSS,后面的 MSS 就省略掉。(MSS 在后面会解释,表示最大报文段长度,一般在 1400 字节左右。)

为了方便描述这个算法,不妨约定 cwnd 初始值为 1(实际大多你看到的是 2)。

  • 首先发送方发送一个 cwnd = 1 的报文。
  • 发送方每收到一个确认,就把 cwnd 值加 1。

具体可以看图 8 的时序图。

面试tcp ip问什么(面试需要知道的)(12)

图8 慢启动

4.1.4 拥塞避免算法

为了防止慢启动过程中 cwnd 增长的过大,TCP 中还维护了另一个变量 ssthresh,单位为字节。它称之为慢启动门限,这是一个阈值,当 cwnd 超过这个值的时候,慢启动算法结束,进入拥塞避免算法!

这时候,TCP 发送 cwnd 个报文后,如果接收到了所有确认报文,cwnd 的值总和只是加 1,而不是加倍(也就是每收到一个确认报文,cwnd 加 1/cwnd)。这样,拥塞窗口 cwnd 就会按线性规律缓慢增长。

有文献将这个过程称为 “加法增大”

面试tcp ip问什么(面试需要知道的)(13)

图9 慢启动(中间出现超时)

4.1.5 拥塞检测过程

无论是在慢启动阶段,还是在拥塞避免阶段,只要发送方判断网络可能出现拥塞(依据就是没有按时收到确认,或者收到三次重复的 ACK),就要把 ssthresh 设置为出现拥塞时的 cwnd 值的一半)。

对于超时和收到三次重复 ACK,需要分别进行考虑,这两者之间是有区别的,而且需要严格区分。

a. 超时(图9)

如果计时器超时,出现拥塞的可能性就非常大(连重复的 ACK 都收不到),此时 TCP 反应强烈

  • 这时候把 ssthread设置为当前 cwnd 的值的一半.
  • cwnd 值再设置成 1,
  • 接下来重新从慢启动开始。

这样做的目的是要迅速减少主机发送到网络中的分组数,使得发生拥塞的中间设备有足够的时间把缓冲区中积压的分组处理完毕。参考图 9。

b. 连续收到三次重复的 ACK(图10)

初步可以判定网络没有拥塞,只是大概率丢失了一个报文。为什么能判定为没有拥塞呢?因为对方在收到失序报文的时候,就会立即返回一个 ACK(这种情况不受 Delay ACK 机制的影响,注意,是立即返回。)既然对方能一连串返回三个重复的 ACK,说明对方应该是连续收到三次的失序报文。你都能连续收到三次失序报文了,说明网络并不差。

失序报文:比如,接收方期望接收 100 号报文,但却收到了其它序号的报文(没有按照应该有的顺序收到)。

这个时候,发送方收到了三次重复 ACK,应该立即重传丢失的报文,而不是等待重传计时器超时。这个策略被称为快重传

发生快重传的时候,虽然网络可能没有拥塞,但是也要降低数据发送速率,只是 TCP 反应较弱,执行快恢复算法

  • 这时候把 ssthread设置为当前 cwnd 的值的一半。
  • cwnd 值设置成 ssthread(也有些实现设置成 ssthread 3)。
  • 进入拥塞避免阶段。

面试tcp ip问什么(面试需要知道的)(14)

图10 快恢复算法

下面是某个小伙伴制作的一个拥塞控制算法快恢复算法的流量曲线,哈哈。

面试tcp ip问什么(面试需要知道的)(15)

5. 三次握手是必须的吗?

我们从另一个角度来分析这个问题。假如使用 UDP 协议来实现可靠传输,应该怎么做呢?

最重要的是得解决可靠性问题,那可靠传输如何保证?

  • 超时重传
  • 确认机制

只要具备这两个机制,我们就能实现最基本、最简单的可靠传输。要想支持这两点,那么你所发送的所有报文都需要进行编号。接收方按照编号的顺序,重组报文,最终应用层就可以读取到有序的数据。

那么问题来了,接收方如何知道哪一包是第一包的报文呢?不妨约定第一包的编号为 0,如此我们就可以快乐的使用UDP 确认机制 超时重传来保证可靠的数据传输了。

这样看起来,我们也没有进行三次握手呢!照样可以实现可靠传输,那 TCP 的三次握手的意义在哪里呢?敲黑板了!!!

  • 协商第一包数据编号。这个第一包编号,称为 ISN (Initial Sequence Number,初始序号),一定要记住这个名词,很重要,默念几遍。刚刚我们约定的策略是,第一包数据是编号是 0,那黑客就可以利用这个漏洞,伪造 TCP 报文,篡改数据,有了 ISN 后,双方就可以通过 ISN 来协议第一包数据编号了。通常实现上,ISN 的初始值是一个随机数(看你怎么猜!)。
  • 协商 MSS 以及确认双方数据接收能力。

通俗的说,三次握手的目的是要建立一个信任关系,探测对方的老底,确认对方的能力,同时防止被黑客攻击。

6. MSS 与 MTU

MSS 全称为 Maximum Segment Size,即最大报文段长度。在前面也不止一次提到它,在第 7 节也提到三次握手其中一个目的也是为了协商 MSS 的大小。

一般来说,TCP 报文段携带的数据当然是越多越好。

如果 TCP 报文段传输的数据只有一个字节,在 IP 层传输的数据报大小就是 40 1 = 41 字节(至少 20 字节的 IP 头 20 字节的 TCP 头 1 字节数据)。这样网络的利用率就只有 1/41. 传输 nn 字节的数据利用率就是 n/(40 n)n/(40 n),显然 TCP 报文段传输的数据如果越大,网络利用率就越高。

但是实际上并非如此。因为网络传输数据时,数据是最终是要交付到链路层协议上的,也就是说最后要封装成“帧”。二型以太网(Ethernet Type 2)中规定,帧的大小不能超过 1518 个字节(14 字节的帧头 4 字节帧校验和 最多 1500 字节数据)。所以 IP 数据报的大小如果超过了 1500 字节,要想交付给链路层就必须进行“分片”。

“分片”指的是一个 IP 数据报太大,需要拆分成一个一个的小段,变成多个 IP 数据报。这种分片显然是不利的,有一定的开销。为了避免分片开销,我们希望 IP 数据报的大小不超过 1500 字节。除去 IP 数据报的首部 20 字节,也就是希望 TCP 报文段不超过 1480 字节。再减去 TCP 报文段首部 20 字节,也就是 TCP 携带的数据不超过 1460 字节。

实际上,链路层对这种帧数据长度的限制称为最大传输单元(Maximum Transmission Unit, MTU)。不同的链路层协议,对 MTU 的值也有所规定,通常这个值可以进行更改。使用命令 netstat -i 可以看到自己的网卡的 MTU 值。

面试tcp ip问什么(面试需要知道的)(16)

图11 查看网卡的 MTU

有些同学对 MSS 与 MTU 的概念仍然很模糊,分不清两者之间的关系。

  • MSS 是软件层的概念,它是由软件控制的
  • MTU 是硬件(比如网卡出口)的属性,是指二层链路层帧携带的数据最大大小。

再举个例子,MTU 的大小就好像一座桥的承重吨位,而桥就相当于网卡。事先给定 MSS,可以防止因为你货车载货过多,要进行分批运输。如果不指定 MSS,一旦你货车超载,吨位超过桥的承重能力,你就得把货拆分成几批运过去,运过去之后你还得组装,这是得不偿失的。

7. TIME_WAIT

面试tcp ip问什么(面试需要知道的)(17)

图12 TIME_WAIT

TIMEWAIT 是 TCP 协议中的一种状态,而且非常容易观察到。上面的实验利用 nc 命令发送数据,然后在客户端执行 netstat -ant 命令,就能看到 TIMEWAIT 状态了。

既然这个状态很容易观察到,说明 TCP 在这个状态下一定是停留了相当长的时间。

TIMEWAIT 状态,又称为 2MSL 等待状态。只有主动关闭一方才能进入 TIMEWAIT 状态。MSL(Maximum Segment Lifetime)表示报文段最大生存时间,它表示任何报文段被丢弃前在网络内的最长时间,实际上这个时间和 TTL 有关(TTL 是 IP 协议中的一个概念,表示能够经历的路由器的跳数,这个跳数是有限制的,最大值为 255)。

然而,MSL 却不用跳数,而是时间。不同系统中,MSL 定义的大小不一样,RFC 规定,MSL = 2 分钟,而实际实现中,通常是 30 秒、1 分钟。尽管 MSL 的单位是时间而不是跳数,我们仍然假设:具有最大跳数(255)的报文在网络中存在的时间不可能超过 MSL 秒。

当 TCP 协议进入 TIME_WAIT 状态时,必须要在这个状态停留 2 倍 MSL 的时间。那为什么要等待这个 2MSL 时间呢?目的是什么?

面试tcp ip问什么(面试需要知道的)(18)

图13 TCP 三次握手与四次挥手状态迁移

主要有两个原因:

  • 可以防止连接终止的最后一个 ACK 丢失。假设最后一个 ACK 在到达对端时恰好消失,此时对端已经等待了一个 RTT(报文段往返时间),于是进行重传最后一个 FIN,经过 0.5RTT 后到达对端。
  • 等待 2MSL 的第二个原因

假设在 ip1:port1 和 ip2:port2 建立了一个连接 A,发送完数据后关闭连接 A。如果没有 TIME_WAIT 状态,我们又立即在 ip1:port1 和 ip2:port2 建立了一个连接 B(虽然这种事情概率很小,但是仍然存在)。

很不幸的是,连接 A 它有一个重复的 TCP 段被连接 B 收到了,然而连接 B 并不知道这个 TCP 段是连接 A 中的旧报文,这会造成错误!

如果有了 TIMEWAIT 状态,等待 2MSL,就足以让连接 A 中重复的报文在网络中消逝;另一方面,TCP 协议规定,处于 TIMEWAIT 状态的端口,是无法建立新的连接的。这样就保证了每成功建立一个新连接时,旧连接中的重复 TCP 段都已消逝。

7.1 TIME_WAIT 影响

如果 TCP 处于 TIME_WAIT 状态,会进行 2MSL 时间的等待,在这个时间内,定义此连接的本地端口不能再次使用。比如有一个连接:

(LOCAL 192.168.80.130:5050, FOREIGN 192.168.166.107:40891) state: TIME_WAIT

那么对于 192.168.80.130 这个主机来说,在 2MSL 时间内,5050 端口都不能再次被使用。

一般来说,主动关闭一方都是客户端,客户端建立连接时的端口号都是由系统自动分配的,这并没什么影响,这个端口被占用了,那就再分配一个其它端口就是了。

但是对于服务器来说,它使用的端口是熟知端口(公开出去的端口),如果它是主动关闭一方,进入了 TIME_WAIT 状态,那么 2MSL 时间内,这个服务器都无法启动。如果再次启动,会提示 Address already in use 的错误。

实际上,现在的 Linux 提供了一种方法,它可以通过给 socket 指定选项 SO_REUSEADDR 允许端口重用。

需要注意的是,即使服务器或客户端绑定了处于 2MSL 的端口,RFC 规定它们也不能建立连接。可惜的是,大多系统实现并未遵守这个规定:

  • 如果服务器重用了处于 2MSL 端口,它仍然可以接收连接请求并连接成功(这违反了协议)
  • 如果客户端重用了处于 2MSL 端口,它建立连接时仍然会失败。(没有违反协议)
8. TCP 首部字段有哪些?

经过前面几个小时的实验和学习,接下来尝试着不看图,分析 TCP 首部字段。

这个问题,显然不是死记硬背。我们试着从 TCP 拥有的特性和相关的算法试着来分析,作为 TCP 协议,它至少拥有哪些字段。

  • 端口(共 4 字节 )

TCP 是一个端到端的协议,我们知道,要建立 TCP 连接,必须要知道对端的 IP 地址和端口。因此“端口”肯定是 TCP 首部必须的字段,但是 IP 地址不是,因为 IP 地址在三层的网络层。端口包含源端口和目的端口,因此这两个字段是 TCP 首部必须有的,即 source port & dest port。

  • 序号(4字节)

TCP 是一个面向字节流的协议,你发的数据是什么顺序,对方接收到的就是什么顺序。为了保证这一点,TCP 需要对发送的每个字节的数据进行编号,因此 TCP 首部一定有关于数据编号的字段。因为一个 TCP 数据包一次会携带若干字节的数据,不可能对每个字节进行编号,在实际协议中,TCP 只对第一个字节数据进行编号,剩下的可以根据第一字节编号进行推算。

关于数据包的长度。TCP 首部中不需要数据包的长度,因为这个长度在 IP 报文中已经携带了,TCP 数据包的长度可以根据 IP 报文首部中的长度进行计算。也就是拿 IP 报文中的长度减去 TCP 首部的长度,就是实际数据的长度。

  • 数据偏移(首部长度,4bit)

上面我们说到 TCP 首部长度,一般来说首部长度都是 20 字节。有时候,TCP 首部还可以携带一些额外的可选项(optional),这样首部就超过了 20 字节,很正常吗,作为软件开发者,我们都希望我们的协议非常具有弹性。为了能计算 TCP 携带的数据的起始位置,数据偏移字段是必不可少的。当然,你也可以叫数据偏移为首部长度完全没问题啊,这两者是一回事。另外需要注意的是,这个数据偏移的单位是 4 字节 。比如偏移是 5,说明首部长度就是 20 字节。

  • 确认号(4字节)

我们知道,TCP 是一个可靠的协议。所谓的可靠,是指 TCP 保证发送的报文要能被对方收到,而保证可靠的方法是基于超时重传算法确认机制。TCP 协议需要对方回复它收到哪些数据,利用之前的序号,就可以做到。因此,TCP 首部还需要一个确认号,来实现可靠传输。

  • 标志位(12bit)

TCP 报文有很多种类型,比如三次握手使用的同步包,四次挥手使用的结束包,确认机制使用的确认包,因此,TCP 首部一定有一些字段来描述 TCP 报文的类型。 TCP 首部中,使用一些特殊的比特位来标记报文的类型,如果比特位置 1,就说明它是该类型对应的包。目前有 9 种类型的报文(目前还剩下 3 bit 的保留位,以后可能还会新增新的标志位)。TCP 报文可以同时属于多种类型,比如可以同时是同步包确认包

目前常见的标志位有:SYN, ACK, PSH, FIN, RST, URG,这也是你在书上能找到的。还有NS, CWR, ECE,知道存在即可,如果你想更深入的了解,可以自行查阅资料。

  • 窗口大小(2字节)

了解过 TCP 的朋友,一定听说过滑动窗口算法,这个算法主要是用来做流量控制的。什么意思呢?实际上窗口大小是用来描述接收方接收能力的参数,比如我现在最多只能接收 1024 个字节,你发的时候就不要发超过 1024 字节的数据给我,否则我也装不下,没地方存储。因此 TCP 首部中还有 16 比特的字段来记录窗口大小。

  • 校验和(2字节)

这个字段即使你不知道,关系也不大。但是对于网络传输来说,校验和是一个十分常见且普遍使用的东西,TCP 首部有它也不奇怪。

  • 紧急指针(2字节)

这恐怕是目前存在感最低的字段了,现实中使用它的地方很少。在 socket 编程中,称其为 OOB(out-of-band data),意为连接之外的数据,但实际上它并不真提连接之外的数据,它还是和正常的数据一起被发送出去。”紧急指针“实际上并不紧急,当你发送紧急数据时,只是把 TCP 标志位的紧急标志位设置成 1,同时紧急指针指向对应的紧急数据编号,发送出去,接收方接收到带紧急标志位的数据时,做一些特殊处理而已。

下面来看一下全貌:

面试tcp ip问什么(面试需要知道的)(19)

最后切记,首部字段是不需要死记硬背的,一定要结合 TCP 自身的特性,来思考 TCP 要想实现这些功能,应当要包含哪些字段。

好,现在再回忆一下,TCP 首部应该有哪些字段?

9. 总结

TCP 内容真的是太多了,本文把一些重要且核心的内容挑了出来,等你掌握了这些基础后,再去深入学习更多的内容,应该没什么问题。再回顾一下我们的知识点:

  • 三次握手与四次挥手
  • 三次握手的目的
  • Delay ACK
  • Nagle 算法
  • 拥塞控制
  • MSS/MTU
  • TIME_WAIT
  • TCP 首部字段
  • tcpdump 工具的基本使用
  • 学会使用 python 进行简单的 socket 编程,用其来学习 tcp 协议

最后,还想说一点,TCP 是一个一直在发展的协议,它内部的算法不是一成不变的,尤其是拥塞控制算法,如今 Linux 内核集成的拥塞控制算法是 CUBIC 算法,最近又有非常火的 BBR 算法(值得你去细读一番)。

,