面试需要知道的 TCP 知识

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

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

关于参考书:

提示:

  • 在学习之前,请确保自己有 >2 小时的连续空闲时间,走马观花,很难消化哦!
  • 准备一台 Linux 服务器,如果没有云主机,那就自己安装一个 Linux 虚拟机来做服务器吧。
  • 准备好本地 Linux 客户机,虚拟机创建一个就行。
  • 接下来,我们拿起大刀—— tcpdump,开始 TCP 之旅吧。

    1. 三次握手和四次挥手

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

    图1 三次握手与四次挥手

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

  • Step.1 打开三个终端。
  • Step.2 下面窗口输入 sudo tcpdump -# -S -n -i lo0 tcp and host 127.0.0.1 and port 8000 启动抓包程序。
  • Step.3 左上窗口输入 nc -l 8000 表示在端口 8000 启动一个 TCP 服务程序。
  • Step.4 右上窗口输入 nc localhost 8000 表示向 localhost:8000 这个地址发起 TCP 连接请求。
  • 不出意外,你就能在下方的窗口里看到抓取到的三次握手的 文了。接下来,你顺便可以观察一下四次挥手的过程。步骤如下:

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

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

    1.1 tcpdump

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

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

    1.2 文含义

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

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

  • 在 tcpdump 中,标志位都放在 [] 中,比如 [S.],其中 S 就表示 SYN,F 表示 FIN,而 . 表示 ACK。
  • 可以看出,除了第一包握手 文外,其它所有 文都带有 ACK 标志。
  • 带有 ACK 标志的 文,表示收到了 ack-1 文,并且接下来期望收到对方序 为 ack 的 文。
  • 第 4 包是一个重传 文,很容易发现,这一包啥也没干,就是把 ACK 重传了一遍。
  • 由于在实验里,我们先退出的客户端,因此是客户端先断开连接,因此客户端发送 FIN 到服务器端(对应的第 5 包)。
  • 关于三次握手,待会在第 5 节,还有更重要的内容!这里只是让你先适应一下 tcpdump 工具,以及放松下心情。

    2. Delay ACK

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

    图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 文。同样这里我们用表格记录一些关键信息。

    一些观察到的现象:

  • 客户端发送 a(第 4 包)到服务器后,服务器立即返回了 ACK(第 5 包) 到客户端,没有经过任何等待。
  • 客户端发送 b(第 6 包)到服务器后,服务器没有立即返回 ACK,而是等待了约 40ms 才返回 ACK(第 7 包)到客户端。
  • 第 8、9、10、11 包也是一样。
  • 这里你需要关注的问题是,服务器接收到数据后,为什么有时候没有立即返回 ACK,而要等待 40ms 呢? 很好,我希望你能看到这个现象,这不是巧合,而是 TCP 的特性,是一种机制,它就是 Delay ACK,即延迟 ACK。

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

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

  • 累积确认:服务器极有可能在这 40ms 里又收到了客户端发送过来的多个 TCP 文段,40ms 后就可以对这些 文进行累积确认,也就是只返回一个 ACK 文就行。这样就能减少 络中 ACK 文的数量。
  • 捎带确认:40ms 里,服务器也可能会返回数据给客户端,如果服务器有数据返回给客户端,那不如把这个 ACK 连同数据一起返回给客户端吧,等一下下是值得的。就好比你要出门和朋友吃饭,但是你女朋友可能也想和你一起去,然而你女朋友要化妆什么的,速度非常慢,于是你想了个策略,你等她 30 分钟,如果 30 分钟内她搞定了,你就带她一起去,如果她搞不定,你就不带她去了。(什么?程序员不可能有女朋友,其实男朋友也可以的^_^)。
  • 好,接下来再谈谈 echo 是怎么把 Delay ACK 机制打开的。非常简单,你使用 man 手册查阅 man 7 tcp,就能看到一些关于 TCP 机制的文档,其中有一项是 TCPQUICKACK。

    图3 TCPQUICKACK

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

    
    

    另外,你需要在每次 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 是实验的结果。具体代码我就不贴进来,你自己敲一遍,记得更加清楚。

    图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 次 文。

    图5 关闭 Nagle

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

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

    3.4 Nagle 算法的影响

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

  • 客户端发送 4 字节出去,经过 41ms 后(服务器的 Delay ACK 40ms + 络延时 1ms),收到了服务器返回的 ACK。
  • 客户端继续发送剩余的 396 字节。
  • 这中间几乎有 40ms 的时间几乎浪费在了 Delay ACK 上,这简直不能忍啊!如果关闭 Nagle 算法,程序性能会有极大的提升,因为不必等待服务器的 ACK 返回,剩下的 396 字节就能直接发送出去。

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

    4. 流量控制与拥塞控制

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

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

  • 相同点
  • 它们都是为了控制发送数据量的大小。
  • 不同点
  • 流量控制,是根据接收者能力情况,来控制发送数据量。
  • 拥塞控制,是根据 络的拥塞状态,来控制发送数据量。
  • 最后实际要发送的数据量,取决于流量控制和拥塞控制计算出来的发送数据量的较小者。通常用 rwnd 来表示接收方的接收能力,用 cwnd 表示链路还能承载的数据能力,最终要发送的数据量是:

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

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

    4.1 拥塞控制算法

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

    4.1.2 慢启动

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

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

  • 连续收到 3 次对方重复的 ACK 确认
  • 这意味着对方极有可能没有收到数据,几乎可以认为丢包了。但这并不代表 络拥塞,甚至 络状况还不错呢。(稍后解释。)
  • 如果超时未收到 ACK,说明极有可能拥塞
  • 对方可能没收到 文
  • 对方收到 文,但 ACK 丢了
  • 一定要严格区分,重复 ACK超时这两种情况,它影响了 TCP 拥塞算法做何种决策!!!

    图6 慢启动(一)

    图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 的时序图。

    图8 慢启动

    4.1.4 拥塞避免算法

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

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

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

    图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 的值的一半。
  • 声明:本站部分文章及图片源自用户投稿,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

  • 上一篇 2020年10月19日
    下一篇 2020年10月19日

    相关推荐