面试需要知道的 TCP 知识

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

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

关于参考书:

  • TCP/IP 详解卷一(作者:W.Richard Stevens)
  • TCP/IP 协议簇(作者:Behrouz A.Forouzan)
  • 是的,本文几乎所有内容在上面的两本书你都能找到(除了实验之外),我当然不是抄书,那样有什么意义呢?本文的目的是梳理,就像老师划的重点一样,你才有复习的方向,特别适合那些时间紧迫,但是又来不及完整去看一遍书的同学。

    提示:

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

    1. 三次握手和四次挥手

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

    面试需要知道的 TCP 知识

    图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 系统上都是默认安装好的,它可以非常方便的抓取网卡上的数据包,并且可以根据你指定的参数进行过滤。在上面的实验中,各个参数含义如下:

    面试需要知道的 TCP 知识

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

    1.2 报文含义

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

    面试需要知道的 TCP 知识

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

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

    2. Delay ACK

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

    面试需要知道的 TCP 知识

    图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 知识

    一些观察到的现象:

  • 客户端发送 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。

    面试需要知道的 TCP 知识

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

    面试需要知道的 TCP 知识

    图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 知识

    图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 知识

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

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

    4.1 拥塞控制算法

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

    4.1.2 慢启动

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

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

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

    面试需要知道的 TCP 知识

    图6 慢启动(一)

    面试需要知道的 TCP 知识

    图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 知识

    图8 慢启动

    4.1.4 拥塞避免算法

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

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

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

    面试需要知道的 TCP 知识

    图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日

    相关推荐