大家好,我是小林。
之前我在公众号解答了一位读者面试腾讯的面试题,问题如下:
针对这个问题,我也没办法用实验来验证我的结论,所以当时结论是基于啃 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 重传后,接收方的应用层才可以从内核中读取到数据。