被印证的感觉,真爽!

网络 无线技术
有位读者在工作中抓到跟这个面试题场景类似的抓包图,我看了下,现象跟我之前啃 TCP 源码得出的结论是符合的。这种被印证的感觉真爽!

大家好,我是小林。

之前我在公众号解答了一位读者面试腾讯的面试题,问题如下:

针对这个问题,我也没办法用实验来验证我的结论,所以当时结论是基于啃 TCP 源码得出来的。

但是,就在昨天!

有位读者在工作中抓到跟这个面试题场景类似的抓包图,我看了下,现象跟我之前啃 TCP 源码得出的结论是符合的。

这种被印证的感觉真爽!

我觉得这个案例还是挺有意思的,因为很好的说明是 TCP 传输协议是按序接收的。

所以,先来回顾腾讯一面的这个问题,再来看看跟这个问题相似的抓包图。

回顾问题

这道鹅厂的网络题可能是提问的读者表述有问题。

因为如果 FIN 报文比数据包先抵达客户端,此时 FIN 报文其实是一个乱序的报文,此时客户端的 TCP 连接并不会从 FIN_WAIT_2 状态转换到 TIME_WAIT 状态。

因此,我们要关注到点是看「在 FIN_WAIT_2 状态下,是如何处理收到的乱序的 FIN 报文,然后 TCP 连接又是什么时候才进入到 TIME_WAIT 状态?」。

我这里先直接说结论:

在 FIN_WAIT_2 状态时,如果收到乱序的 FIN 报文,那么就被会加入到内核中的「乱序队列」,并不会进入到 TIME_WAIT 状态。

等再次收到前面被网络延迟的数据包时,会判断乱序队列有没有数据,然后会检测乱序队列中是否有可用的数据,如果能在乱序队列中找到与当前报文的序列号保持的顺序的报文,就会看该报文是否有 FIN 标志,如果发现有 FIN 标志,这时才会进入 TIME_WAIT 状态。

我也画了一张图,大家可以结合着图来理解。

神一般的抓包图

下图是昨天一位读者发给我的抓包图,图中的异常情况,跟前面这个问题的现象有点类似:

你可能会有疑问为什么 TCP 握手时,双方的 seq 都是 0 开始的?这个是抓包图做了优化,显示的是相对值,而不是真实值,显示相对值方便分析。

为了方便文字描述,我针对异常部分的报文进行编号如下:

图中端口号为 11710 的为客户端,端口号为 8080 的为服务端。另外,编号 4 是客户端发送的 http 请求,抓包图没有显示 TCP 信息,这里文字补充下:编号 4 数据报文 seq = 1,ack = 1,len = 27。

编号 6 是 FIN 报文,也就是服务端向客户端发送的 FIN 报文(第一次挥手),但是是一个乱序的 FIN 报文,因为从编号 4 报文中的 ack = 1 知道,客户端期望下一次收到的报文的序列号为 1,而当前收到的 FIN 报文的 seq = 177,这并不是客户端下一次期望收到的报文,所以是乱序的。

客户端收到乱序 FIN 报文后,并不会从 establish 转为 close_wait 状态,而是把这个乱序的 FIN 报文放到内核中的乱序队列。因为如果这时候就进入了 close_wait 状态,就会马上发送 FIN 报文了(第三次挥手),而不会有客户端后面发送的编号 8 和 9 报文的事情了。

编号 8 是应答报文,是客户端对编号 6 乱序 FIN 报文的应答报文,可以看到这个应答报文中 seq = 28,ack=1,因为并不是我期望的下一个报文,所以应答报文中 ack 还是为 1。

编号 7 是数据报文,也就是服务端向客户端发送的数据报文,该报文 seq = 1,ack=28,len=176,因为 seq 为 1,所以是客户端期望收到的报文。客户端收到该报文后,就回了编号 9 应答报文,此应答报文 seq = 28,ack = 178,其中 ack = 178 是告诉服务端:“你发的 seq = 178 之前(不包括seq=178)的报文,我都收到了,我下次期望收到的报文的 seq 为 178”。

客户端收到编号 7 数据报文时还会做一件事情,会检测「乱序队列」中是否有可用的数据,如果能在乱序队列中找到与当前收到报文的序列号「保持的顺序」的报文,就把处于乱序队列的报文移到可以被正常处理的数据队列。比如,这次的案例中,编号 7 报文 seq = 1,len=176 的数据范围是 1~176,而乱序队列中的 FIN 报文的 seq = 177,这两个报文的 seq 正好是保持顺序的,所以会把 FIN 报文从乱序队列中拿出来一起处理,然后发现有 FIN 标志,于是就会转换状态。

所以,客户端在应答完编号 7 数据报文后,就立马发送 FIN 报文了(第三次挥手),接着服务端应答了该报文,至此四次挥手结束。

这个抓包图跟前面这个腾讯的面试题有一点差异,差异在于:

  • 腾讯的面试题是在 FIN_WAIT_2 状态下收到乱序的 FIN 报文(第三次挥手);
  • 抓包图是在 establish 状态收到了乱序的 FIN 报文(第一次挥手);

上面这两种 TCP 状态,收到乱序的 FIN 报文,并不会立即转换状态,只会被内核放到一个乱序队列里。等收到一个序列号符合「接收方」期望收到的序列号的数据包时,会检测「乱序队列」中是否有可用的数据,如果能在乱序队列中找到与当前收到报文的序列号「保持的顺序」的报文,就会看该报文是否有 FIN 标志,如果发现有 FIN 标志,就会转换状态。

所以,从这里可以看到, TCP 传输协议是按序接收,如果收到一个乱序的报文时,并且在接收窗口范围内(序列号超过接收窗口范围外的报文就会被丢弃),就会缓存在内核中的乱序队列,不做其他处理。等收到能与乱序队列中报文的序列号保持顺序的报文,才会一起被处理。

TCP 层必须保证收到的字节数据是完整且有序的,所以如果序列号较低的 TCP 报文在网络传输中丢失了,即使序列号较高的 TCP 报文已经被接收了,应用层也无法从内核中读取到这部分数据。

举个例子,如下图:

图中发送方发送了很多个 packet,每个 packet 都有自己的序号,你可以认为是 TCP 的序列号,其中 packet 3 在网络中丢失了,即使 packet 4-6 被接收方收到后,由于内核中的 TCP 数据不是连续的,于是接收方的应用层就无法从内核中读取到,只有等到 packet 3 重传后,接收方的应用层才可以从内核中读取到数据。

责任编辑:赵宁宁 来源: 小林coding
相关推荐

2020-05-06 11:00:11

git命令GitHub

2021-06-01 09:29:43

ArthasJVM内存

2023-07-04 07:08:27

内存游戏容量

2020-02-26 17:32:57

WindowsWindows 10操作系统

2009-05-31 08:31:06

PalmWebOS移动OS

2009-05-12 16:11:00

求职面试招聘

2022-08-01 10:01:11

JavaScript语言代码库

2022-10-26 13:00:00

2013-04-15 09:52:13

程序员

2013-12-12 16:28:04

Lua脚本语言

2021-08-07 07:48:28

JDKjava JDK17

2010-01-04 14:37:46

Linux Ubunt

2023-03-29 09:00:00

2023-03-21 08:10:18

2023-02-22 17:56:55

MySQL数据存储

2020-11-23 11:30:00

IDEA技巧开发

2021-07-19 09:42:45

Spring Boot@ValueJava

2020-12-28 15:20:31

新华三集群路由器

2011-04-25 10:26:54

打印机

2021-02-02 13:31:34

新基建新华三路由器
点赞
收藏

51CTO技术栈公众号