网络3️⃣TCP-四挥
1、四次挥手
1.1、三握四挥
TCP 是面向连接的协议
-
通信之前必须先建立连接(aka. 三次握手)
- 客户端主动发起建立连接。
- 报文:SYN → SYN+ACK → ACK
-
通信结束后必须断开连接(aka. 四次挥手)
- 双方都可以主动断开连接,断连后将释放主机中的资源。
- 主动关闭连接的一方,才有
TIME_WAIT
状态。 - 报文:FIN → ACK → FIN → ACK
1.2、四挥
假设客户端主动断开连接
① 客户端 FIN
- 向服务端发送
FIN
报文(TCP 首部的FIN
标志位设1
)。 - 之后客户端进入
FIN_WAIT_1
状态。
② 服务端 ACK
- 收到客户端的
FIN
报文。 - 回复
ACK
报文(TCP 首部的ACK
标志位设1
)。 - 之后服务端进入
CLOSE_WAIT
状态。
服务端发送
ACK
报文后,可能还有数据待处理和发送。客户端收到服务端的
ACK
报文后,进入FIN_WAIT_2
状态。
③ 服务端 FIN
- 处理完数据后,向客户端发送
FIN
报文。 - 之后服务端进入
LAST_ACK
状态。
④ 客户端 ACK
- 收到服务端的
FIN
报文。 - 回复
ACK
报文。 - 之后客户端进入
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
状态(断开连接)。
- 重传次数:由 Linux 内核参数
② 服务端 ACK
服务端收到客户端的
FIN
报文(挥一),回复
ACK
报文(挥二),进入CLOSE_WAIT
状态。
报文丢失:
-
服务端:发送的 ACK 报文(挥二)丢失,
ACK
报文不会重传。 -
客户端:迟迟没有收到
ACK
报文(挥二),认为FIN
报文(挥一)丢失,触发超时重传。
③ 服务端 FIN
服务端发送 ACK 报文(挥二)后,进入
CLOSE_WAIT
状态。当服务器完成数据处理和发送时,会发送
SYN
报文(挥三),进入LAST_ACK
状态。
报文丢失:
-
客户端:没有收到服务器的
FIN
报文(挥三),不会回复ACK
报文(挥四)。 -
服务端:迟迟收不到
ACK
报文(挥四),触发超时重传。- 重传次数:由 Linux 内核参数
tcp_orphan_retries
决定 - 与客户端的重传次数是同一个参数。
- 重传次数:由 Linux 内核参数
④ 客户端 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_WAIT:
2MSL
可以确保两个方向上的数据包都被丢弃,之后出现的数据报一定是在新连接中产生的。
② 正常关闭被动方
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
过多,造成的影响是不同的。主要危害
- 客户端:占用端口资源。
- 端口资源是有限的,通常可用范围是
32768~61000
。 - 可通过
net.ipv4.ip_local_port_range
参数指定范围。
- 端口资源是有限的,通常可用范围是
- 服务端:占用系统资源。
- 服务器只监听一个端口,不会占满端口资源。
- TCP 连接过多会占用系统资源,如文件描述符、内存资源、CPU 资源、线程资源等。
4、服务端异常分析
4.1、出现大量 TIME_WAIT 状态
Hint:主动关闭连接方才有 TIME_WAIT 状态。
如果服务器出现大量
TIME_WAIT
状态的 TCP 连接,说明服务器主动断开了很多 TCP 连接。
可能原因:
- HTTP 没有使用长连接
- HTTP 长连接超时
- 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 Keep-Alive,此时 HTTP 请求 Header 中有
- 客户端开启,服务端禁用:服务端在发完 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 服务端工作流程
- 创建服务端 socket,bind 绑定端口、listen 监听端口
- 将服务端 socket 注册到 epoll
- epoll_wait 等待连接到来,连接到来时调用 accpet 获取已连接的 socket
- 将已连接的 socket 注册到 epoll
- epoll_wait 等待事件发生
- 对方连接关闭时,我方调用 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 函数之前,代码卡在某一个逻辑,比如发生死锁等等。