网络3️⃣TCP-四挥

1、四次挥手

1.1、三握四挥

TCP 是面向连接的协议

  • 通信之前必须先建立连接(aka. 三次握手

    • 客户端主动发起建立连接。
    • 报文:SYN → SYN+ACK → ACK
  • 通信结束后必须断开连接(aka. 四次挥手

    • 双方都可以主动断开连接,断连后将释放主机中的资源。
    • 主动关闭连接的一方,才有 TIME_WAIT 状态。
    • 报文:FIN → ACK → FIN → ACK

    客户端主动关闭连接 —— TCP 四次挥手

1.2、四挥

假设客户端主动断开连接

① 客户端 FIN

  1. 向服务端发送 FIN 报文(TCP 首部的 FIN 标志位设 1)。
  2. 之后客户端进入 FIN_WAIT_1 状态

② 服务端 ACK

  1. 收到客户端的 FIN 报文。
  2. 回复 ACK 报文(TCP 首部的 ACK 标志位设 1)。
  3. 之后服务端进入 CLOSE_WAIT 状态

服务端发送 ACK 报文后,可能还有数据待处理和发送。

客户端收到服务端的 ACK 报文后,进入 FIN_WAIT_2 状态

③ 服务端 FIN

  1. 处理完数据后,向客户端发送 FIN 报文。
  2. 之后服务端进入 LAST_ACK 状态

④ 客户端 ACK

  1. 收到服务端的 FIN 报文。
  2. 回复 ACK 报文。
  3. 之后客户端进入 TIME_WAIT 状态(经过 2MSL 后进入 CLOSE 状态,关闭连接)。

服务端收到客户端的 ACK 报文后,进入 CLOSE 状态关闭连接)。

2、四挥分析

2.1、为什么是四次

假设客户端主动断开连接

  • 客户端:发送 FIN 报文,代表客户端不再发送数据,但还能接收数据。
  • 服务端
    • 回复 ACK 报文后,可能还有数据待处理和发送
    • (数据处理完成,不再发送数据时)发送 FIN 报文给客户端,代表现在同意关闭连接
  • 客户端:回复 ACK 报文,正式关闭连接

服务端通常需要等待数据的处理,因此 ACK 和 FIN 通常会分开发送。

特定情况下,TCP 四次挥手可以变成三次

2.2、挥手丢失的影响

假设客户端主动关闭连接

① 客户端 FIN

客户端发送 FIN 报文(挥一)后,进入 FIN_WAIT_1 状态。

报文丢失

  • 服务端:无法收到客户端 FIN 报文(挥一),不会响应 ACK 报文(挥二)

  • 客户端:迟迟收不到 ACK 报文(挥二),触发超时重传

    • 重传次数:由 Linux 内核参数 tcp_orphan_retries 决定。
    • 超过重传次数后,会再等待一段时间(上一次超时时间的 2 倍),如果仍未收到 ACK 则进入 close 状态(断开连接)。

② 服务端 ACK

服务端收到客户端的 FIN 报文(挥一),

回复 ACK 报文(挥二),进入 CLOSE_WAIT 状态。

报文丢失

  • 服务端:发送的 ACK 报文(挥二)丢失,ACK 报文不会重传

  • 客户端:迟迟没有收到 ACK 报文(挥二),认为 FIN 报文(挥一)丢失,触发超时重传

③ 服务端 FIN

服务端发送 ACK 报文(挥二)后,进入 CLOSE_WAIT 状态。

当服务器完成数据处理和发送时,会发送 SYN 报文(挥三),进入 LAST_ACK 状态。

报文丢失

  • 客户端:没有收到服务器的 FIN 报文(挥三),不会回复 ACK 报文(挥四)

  • 服务端:迟迟收不到 ACK 报文(挥四),触发超时重传

    • 重传次数:由 Linux 内核参数 tcp_orphan_retries 决定
    • 与客户端的重传次数是同一个参数。

④ 客户端 ACK

客户端收到服务端的 FIN 报文(挥三),

回复 ACK 报文(挥四),进入 TIME_WAIT 状态(持续 2MSL 后关闭连接)。

报文丢失

  • 客户端:发送的 ACK 报文(挥四)丢失,ACK 报文不会重传

  • 服务端:迟迟没有收到 ACK 报文(挥四),触发超时重传

3、TIME_WAIT

3.1、为什么是 2MSL

MSL 和 TTL

含义

MSL TTL
全称 最大报文生存时间(Maximum Segment Lifetime) 生存时间(Time To Live)
含义 任何报文在网络上存在的最长时间 IP 数据报可以经过的最大路由数
何时丢弃 超过 MSL 的报文将被丢弃 每经过一个路由器减 1,值为 0 时将被丢弃,并发送 ICMP 报文通知源主机
本质 时间 经过路由跳数
  • TCP 报文是基于 IP 协议的,TTL 字段位于 IP 首部中。
  • 为了确保报文自然消亡,MSL 应当不小于 TTL 消耗为 0 的时间(否则报文会在数据报转发途中被丢弃)。
  • 取值
    • 通常 TTL = 64,Linux 中的 MSL = 30s
    • Linux 认为报文经过 64 跳的时间不会超过 30 秒,超过了就认为报文已经消失在网络中。

TIME_WAIT = 2MSL

取值 2MSL 的原因

  • 网络中可能存在来自发送方的数据包,数据包被接收方处理后又会回复响应。
  • 一来一回需要等待 2MSL 的时间

2MSL 计时:从客户端接收 FIN 报文,发送 ACK 报文后开始计时。

  • 如果客户端的 ACK 报文丢失,服务端会重传 FIN。
  • 当客户端接收到服务端重传的 FIN 报文,将重置 2MSL 定时器

3.2、TIME_WAIT 作用

主动关闭连接的一方,才会有 TIME-WAIT 状态。

作用

  • 防止历史连接中的数据(历史报文),被相同四元组的连接接收。
  • 保证能够正确关闭被动关闭连接的一方

① 历史报文

防止历史报文被相同四元组的连接接收。

  • 序列号(SEQ)和初始序列号(ISN不是无限递增的。
    • SEQ:TCP 首部字段。是一个 32 位无符号数,达到最大值后会从 0 开始
    • ISN:TCP 握手时生成。ISN 随机算法的计数器每 4ms 加一,每 4.55h 循环一次
  • 没有 TIME_WAIT 的后果:假设场景如下,。、、。
    • 服务器在关闭连接前发送的某个报文(在后来属于历史报文),发送网络堵塞。
    • 关闭连接后,客户端以相同的四元组建立新的 TCP 连接。
    • 假设此时历史报文到达客户端,并且恰好在客户端接收窗口内。
    • i.e 客户端成功接收历史报文,产生数据错乱。
  • 加入 TIME_WAIT2MSL 可以确保两个方向上的数据包都被丢弃,之后出现的数据报一定是在新连接中产生的。

② 正常关闭被动方

RFC 793: TIME-WAIT - represents waiting for enough time to pass to be sure the remote TCP received the acknowledgment of its connection termination request.

含义:等待足够的时间,以确保最后的 ACK 能让被动关闭方接收,从而帮助其正常关闭。

假如客户端(主动关闭方)的最后一个 ACK 报文(挥四)丢失,会触发服务端的超时重传 FIN 报文(挥三)。

  • 没有 TIME_WAIT 的后果
    • 客户端发送最后一个 ACK 报文(挥四)后,直接进入 CLOSE 状态。
    • 客户端接收到服务端重传的 FIN 后,会回复 RST 报文。
    • 服务端会将 RST 解释为一个错误(Connection reset by peer),中断 TCP 连接(但并不优雅)。
  • 加入 TIME_WAIT
    • 如果客户端的 ACK 报文(挥四)丢失,触发服务端重传 FIN 报文(挥三)。
    • ACK 报文(挥四)和 FIN 报文(挥三)加起来正好 2MSL

客户端再次收到 FIN 报文后,会重置 2MSL 定时器。

3.3、TIME_WAIT 过多的危害

客户端和服务端 TIME_WAIT 过多,造成的影响是不同的。主要危害

  1. 客户端占用端口资源
    • 端口资源是有限的,通常可用范围是 32768~61000
    • 可通过 net.ipv4.ip_local_port_range 参数指定范围。
  2. 服务端占用系统资源
    • 服务器只监听一个端口,不会占满端口资源。
    • TCP 连接过多会占用系统资源,如文件描述符、内存资源、CPU 资源、线程资源等。

4、服务端异常分析

4.1、出现大量 TIME_WAIT 状态

Hint主动关闭连接方才有 TIME_WAIT 状态

如果服务器出现大量TIME_WAIT 状态的 TCP 连接,说明服务器主动断开了很多 TCP 连接

可能原因

  1. HTTP 没有使用长连接
  2. HTTP 长连接超时
  3. HTTP 长连接的请求数量达到上限

HTTP 长连接(Keep-Alive)

开启长连接后, TCP 连接不会中断,直到客户端或服务器提出断开连接。

① 没有使用长连接

HTTP/1.0 默认关闭长连接(i.e. 短连接)

手动开启长连接

  • 如果浏览器要开启 Keep-Alive,必须在请求的 Header 中添加 Connection: Keep-Alive
  • 服务器收到请求后响应时,也需要在响应的 Header 中添加 Connection: Keep-Alive

HTTP/1.1 起默认开启长连接

  • 一旦客户端和服务端达成协议,长连接就建立完成。
  • 如果要关闭 Keep-Alive,需要在 HTTP 请求或响应的 Header 中添加 Connection:close

无论任何一方禁用 Keep-Alive,通常都是由服务器主动关闭连接。

此时服务端就会出现 TIME_WAIT 状态的连接。

分析:双方 Keep-Alive 的开启状态。

  • 客户端禁用,服务端开启:说明客户端不需要重用连接了。
    • 客户端禁用 HTTP Keep-Alive,此时 HTTP 请求 Header 中有 Connection:close 信息。
    • 服务端在发完 HTTP 响应后,主动关闭连接。
  • 客户端开启,服务端禁用:服务端在发完 HTTP 响应后,主动关闭连接。

② 长连接超时

在 HTTP 长连接中,如果客户端发送一个 HTTP 请求后不再发送新的请求,此连接会一直占用资源。

为了避免资源浪费,Web 服务端通常会设置 HTTP 长连接超时时间。

示例:Nginx 的 keepalive_timeout 参数,假设取值 60s。

  • Nginx 会启动一个定时器。
  • 如果客户端发送一个 HTTP 请求后,在 60 秒内都没有再发起新的请求,Nginx 会触发回调函数来关闭连接。
  • 此时服务端上就会出现 TIME_WAIT 状态的连接。

③ 长连接请求数量达到上限

Web 服务端通常会设置一条 HTTP 长连接上最大能处理的请求数量。

当超过最大限制时,就会主动关闭连接。

示例:Nginx 的 keepalive_requests 参数,默认值 100。

  • 当一个 HTTP 长连接建立之后,Nginx 会为其设置一个计数器,记录此连接上已经接收并处理的请求数量
  • 当计数器达到参数值时,Nginx 会主动关闭长连接。
  • 此时服务端上就会出现 TIME_WAIT 状态的连接。

对策:将最大请求数量的参数值调大。

4.2、出现大量 CLOSE_WAIT 状态

Hint被动关闭连接方才有 CLOSE_WAIT 状态

如果服务器出现大量 CLOSE_WAIT 状态的 TCP 连接,说明服务端程序没有调用 close() 关闭连接。

没有调用 close(),无法发出 FIN 报文,状态无法从 CLOSE_WAIT 转为 LAST_ACK

TCP 服务端工作流程

  1. 创建服务端 socket,bind 绑定端口、listen 监听端口
  2. 将服务端 socket 注册到 epoll
  3. epoll_wait 等待连接到来,连接到来时调用 accpet 获取已连接的 socket
  4. 将已连接的 socket 注册到 epoll
  5. epoll_wait 等待事件发生
  6. 对方连接关闭时,我方调用 close

没有调用 close() 的原因

针对具体代码分析,可能原因如下。

  • 第 2 步没有做:没有将服务端 socket 注册到 epoll。
    • 有新连接到来时,服务端无法感知此事件,也无法获取到已连接的 socket,服务端自然就没机会对 socket 调用 close()。
    • 不过这种原因发生的概率比较小,这种属于明显的代码逻辑 bug,在前期 read view 阶段就能发现的了。
  • 第 3 步没有做:有新连接到来时没有调用 accpet 获取该连接的 socket。
    • 导致有大量的客户端主动断开了连接,而服务端没机会对这些 socket 调用 close()。
    • 发生这种情况可能是因为服务端在执行 accpet 函数之前,代码卡在某一个逻辑或者提前抛出了异常。
  • 第 4 步没有做:通过 accpet 获取已连接的 socket 后,没有将其注册到 epoll。
    • 导致后续收到 FIN 报文的时候,服务端没办法感知这个事件,那服务端就没机会调用 close()。
    • 发生这种情况可能是因为服务端在将已连接的 socket 注册到 epoll 之前,代码卡在某一个逻辑或者提前抛出了异常。
  • 第 6 步没有做:当发现客户端关闭连接后,服务端没有执行 close 函数。
    • 可能是因为代码漏处理。
    • 或者是在执行 close 函数之前,代码卡在某一个逻辑,比如发生死锁等等。
posted @ 2023-05-09 01:12  Jaywee  阅读(30)  评论(0编辑  收藏  举报

👇