【面试知识点精讲】10月27日 TCP常见面试题(下)

前两篇:

【面试知识点精讲】10月12日 TCP常见面试题(中) (zsxq.com)

【面试知识点精讲】10月8日 TCP常见面试题(上) (zsxq.com)


当四次挥手中某一次丢失,会发生什么?

1、第一次挥手丢失


第一次挥手是主动关闭方向被动关闭方发送的 FIN 报文,(以下都是以主动关闭方为客户端为例,但是不是说只有客户端能主动断开连接,TCP连接的双方都可以选择主动断开连接)客户端发送完该 FIN 报文后就会进入 FIN_WAIT_1 状态,等待服务端回复 ACK 报文。但是如果第一次挥手丢失服务端不会回复 ACK,客户端迟迟收不到 ACK 报文就会触发超时重传机制去重发 FIN 报文,等待两倍上次超时时间后如果还是收不到 ACK 确认报文,就会再重发,直到达到最大重发次数(由内核中 tcp_orphan_retries 参数控制)后会等待两倍上次超时时间,依旧收不到服务端传来的 ACK 报文(第二次挥手)的话,就会关闭连接进入 CLOSE 状态。


2、第二次挥手丢失


第二次挥手是服务端对客户端 FIN 报文的应答报文 ACK,如果该报文丢失,因为ACK报文不会重传,客户端收不到 FIN报文的确认报文,就会触发重传机制重传 FIN 报文,直到收到服务端的 ACK 或者一直收不到,达到最大重传次数后等待2倍上次超时时间断开连接。

当客户端收到第二次挥手,客户端会处于 FIN_WAIT_2状态,这个状态要等待第三次挥手(服务端发送的 FIN 报文)

  1. 对于用close函数关闭的连接,由于在这个状态无法再发送和接收数据,FIN_WAIT_2状态不可以持续太久,内核中 tcp_fin_timeout 控制了这个状态的最大持续时长(默认60秒)
  2. 如果客户端使用了 shutdown 函数关闭连接,指定只关闭发送而不关闭接收,那么客户端在FIN_WAIT_2状态可以接受数据,由于 tcp_fin_timeout 无法控制用 shutdown 关闭的连接,而客户端一直没收到第三次挥手,就会一直处于FIN_WAIT_2状态,一直死等而不会关闭连接,这点需要注意。


3、第三次挥手丢失


第三次挥手是服务端向客户端发送的 FIN 报文,告知客户端自己同意关闭连接了。当服务端收到客户端的 FIN 报文后就会自动回复 ACK 并进入 CLOSE_WAIT状态,这个状态表示等待应用进程调用 close 函数关闭连接(内核是没有权利代替应用关闭连接的),应用调用 close 函数后,服务端内核发出 FIN 报文随后进入 LAST_ACK 状态,等待客户端回复 ACK。当服务端迟迟收不到第三次挥手的 ACK,就会触发超时重传机制,和上面讲的一样,重传次数由 tcp_orphen_retrie 参数控制,如果一直收不到就会等到达到超时重传次数限制后再等待两倍上次超时时间,关闭连接进入 CLOSE状态。而客户端处于 FIN_WAIT_2 状态的时长是有限的,如果 tcp_fin_timeout 时间内还没收到服务端的 FIN 报文(第三次挥手),就会断开连接。


4、第四次挥手丢失


第四次挥手是客户端对服务端 FIN 报文的 ACK 报文,如果该报文丢失就会导致服务端收不到客户端对其 FIN 报文的确认,超时会重传 FIN 报文,客户端在进入 TIME_WAIT 状态时会开启 2MSL(Maximum Segment Lifetime报文段最大生存时间) 计时器,当计时器倒计时为0会断开连接,服务端重传 FIN 报文被客户端收到后会将计时器重置,并重传ACK,之后如果一直收不到 服务端重传的 FIN 报文,等待 2MSL 时长后客户端就会断开连接。服务端在收到 ACK 后断开连接,或达到最大重传次数等待 2 倍上次超时时间后断开连接。


为什么 需要 TIME_WAIT 状态


从四次挥手的示意图中我们能看到主动关闭连接的一方会有 TIME_WAIT状态

主动关闭方需要 TIME_WAIT 状态,主要有如下两个原因:

  1. 防止历史连接中的数据被后面相同四元组的连接错误地接收
  2. 保证被动关闭方能够被正确地关闭


1、防止历史连接中的数据被后面相同四元组的连接错误地接收


首先复习一下序列号(SEQ)和初始序列号(ISN)

  1. 序列号:TCP的头部字段,序列号是一个 32 位的无符号数,因此在到达 4G 之后循环回到0
  2. 初始序列号:TCP建立连接时客户端和服务器各自生成初始化序列号,是基于时钟生成的随机数,以保证每个连接拥有不容的初始序列号。序列号可视为 32 位的计数器,每 4 微妙+1,循环一次要 4.55 小时

从上述信息我们可以看出:序列号和初始化序列号不是无限递增的,会发生回绕位初始值的情况,这意味着无法根据序列号判断新老数据

这样就会出现如果 TCP 连接一端关闭连接前发送的报文被网络延迟了,而另一端以相同的四元组重新打开了新连接,前面被延迟的报文抵达,并且该报文的序列号正好在这一端的接收窗口内就会被接收,但是这个报文实际上是上一个连接残留的,这样就会出现数据错乱等严重问题。

为例防止上述问题的出现,TIME_WAIT状态会持续 2MSL 时长,这个时间足以让两个方向上的数据包都被丢弃,使得原来的连接的数据包在网络中都自然消失,再出现的数据包一定都是新建连接的。


2、保证被动关闭方能够被正常关闭


TIME_WAIT的最用是等待足够的时间确保最后的主动关闭方的 ACK 能被被动关闭方正确接收到,从而帮助其正确关闭。

假设没有 TIME_WAIT 状态,主动关闭方发送完最后一个 ACK 报文就会进入 CLOSE 状态,如果该报文丢失了,被动关闭方超时重传 FIN 报文,主动关闭方收到后会直接回 RST 报文,被动关闭方收到后会将其解释为一个错误,对于可靠的 TCP 协议而言不是一个优雅的终止方式。

于是主动关闭方有了 TIME_WAIT 状态,等待足够长的时间来确保被动关闭方能够收到 ACK(被动关闭方没收到 ACK 就会重传 FIN 报文,一去一来正好 2MSL,主动关闭方收到后会重置 TIME_WAIT 的等待时间为 2MSL,并重传ACK报文)


TIME_WAIT 过多有什么危害


注意:只有主动方才用 TIME_WAIT 状态,而服务端和客户端都可以选择主动关闭连接

两个危害:

  1. 占用系统资源-》针对服务端
  2. 占用端口资源-》针对客户端

如果服务端的 TIME_WAIT 状态太多,因为服务端只监听一个端口,所有不会导致占用过多的端口资源的,并且由于一个四元组唯一确定一个 TCP 连接,理论上服务端可以建立非常多的连接,但是 TCP 连接过多,特别是处于 TIME_WAIT 这种无法传输数据的状态,会占用文件描述符、内存、CPU、线程等系统资源。

如果客户端的 TIME_WAIT 状态过多,占用了所有的端口资源,就无法对目的id+目的端口一样的服务器发起连接,但是被使用的端口还可以对另外一个服务端发起连接,因为内核是通过四元组(源IP、源端口、目的IP、目的端口)定位一个连接,不会因为客户端的端口一样导致连接冲突。


如何优化 TIME_WAIT


主要三个方法,有利有弊

  1. 开启 Linux 内核参数:net.ipv4.tcp_tw_reuse = 1,需要打开对 TCP 时间戳的支持:net.ipv4.tcp_timestamps=1(默认即为 1),这样就可以复用处于 TIME_WAIT 的 socket 为新的连接所用。
  2. 这个功能只能给连接发起者用,当发起者调用connect()函数时,内核会随机找一个 TIME_WAIT 状态超过 1 秒的连接给新的连接复用
  3. 引入时间戳,重复的数据包会因为时间戳过期而被自然丢弃,前文提到的 2MSL 问题就不复存在了
  4. 内核中net.ipv4.tcp_max_tw_buckets参数,默认18000,当系统处于TIME_WAIT的连接超过这个值,系统会将之后的 TIME_WAIT 连接状态重置,这个方法很暴力。
  5. 程序中使用 SO_LINGER,通过设置socket选项来设置调用 close 关闭连接行为,在调用 close 后或直接发 RST 关闭连接(也就是说该 TCP 连接跳过了四次挥手直接关闭,自然也就没有了 TIME_WAIT 状态)

但是 TIME_WAIT 状态被设计本来就是为了避免发生乱七八糟的事。

如果服务端要避免过多的 TIME_WAIT 状态的连接,就永远不要主动断开连接,让客户端去断开,由分布在各处的客户端去承受 TIME_WAIT。


如果已经建立连接,但是客户端突然出现故障怎么办?


这就要引出 TCP 的一个重要机制:保活机制,在一定时间段如果没有任何连接相关的活动,TCP保活机制就会开始作用,每隔一个时间间隔发送一个探测报文(该报文包含的数据非常少),没得到回应并且发送探测报文的次数到达上限,就会认为当前的 TCP 连接已经死亡,系统内核会把错误信息通知给上层应用程序。

在 Linux 内核中有相关的测试参数:

  1. net.ipv4.tcp_keepalive_time=7200 保活时间,默认 7200 秒
  2. net.ipv4.tcp_keepalive_intvl=75 每次检测间隔,默认 75 秒
  3. net.ipv4.tcp_keepalive_probes=9 检测次数上限,默认9次

(以上均为默认值)

应用程序需要通过 socket 接口设置 SO_KEEPALIVE 选项开启 TCP 保活,如果开启了 TCP 保活,有如下几种情况:

  1. 对端程序正常工作,能够正常响应 TCP 保活的探测报文,这样 TCP 保活时间会被重置,等待下一次保活时间
  2. 对端程序崩溃重启,对端可以响应 TCP 保活的探测报文,但是由于没有该连接的优先信息,会回复一个 RST 报文,这样就能很快发现该 TCP 连接已被重置
  3. 对端程序崩溃或报文不可达,无法响应 TCP 保活的探测报文,连续几次达到保活探测次数后,TCP就会报告该 TCP 连接已死亡

前面提到 TCP 保活时间默认时 7200 秒(两个多小时),时间比较长,一般可以在应用层实现心跳机制:比如 web 服务软件中可以通过 keepalive_timeout 这样的参数来指定 HTTP 长连接的超时时间,比如 60 秒,web 服务软件就会启动一个定时器,如果客户端在客户端在完成一个 HTTP 请求后,在 60 秒内都没有再发起新的请求,该定时器到时后,就会触发回调函数来释放该连接


如果建立好连接,服务端的进程崩溃会发生什么?


结论:服务端会发送 FIN 报文,与客户端进行四次挥手

解释:因为 TCP 的连接信息是由内核维护的,服务端的进程崩溃后,内核要回收该进程的所有 TCP 连接资源,所以内核就会发送第一次挥手 FIN 报文,之后的挥手过程也是在内核完成的,不需要进程的参与。


总结:这种客户端/服务端进程崩溃或主机宕机的面试题


以客户端进程崩溃/主机宕机为例,客户端和服务端二者相同

  1. 如果客户端进程崩溃,其进程在发生崩溃时,内核会向服务端发送 FIN 报文,与其进行四次挥手
  2. 如果客户端主机宕机,则不会发生四次挥手,后续会发生什么主要看服务端会不会发送数据
  3. 如果服务端发送数据,由于客户端不存在,服务端收不到响应报文就会触发超时重传,当重传的总时间间隔达到一定阈值就会断开 TCP 连接
  4. 如果服务端一直不发数据,则看服务端有没有开启 TCP 保活机制
  5. 开启了,服务端在一定时间没有进行数据交互后会触发保活机制,如果探测到对方消亡,则会断开 TCP 连接
  6. 没开启,TCP 连接会一直存在,并且一直保持在 ESTABLISHED 状态

参考资料:小林coding:《图解计算机网络》、极客时间《趣谈网络协议》专栏

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
CarreyWu
下载 APP