TCP状态图
网络连接断开状态图
TCP建立关闭连接状态图
MSL:Max segment lifetime最大段存活时间。
主动关闭连接的一方处于TIME_WAIT状态。
实际测试中某个连接关闭后,处于FIN_WAIT2,若对方一直未发送FIN,则过一段时间后此连接主动断开。具体时间待考证。
网络状态
可用如下命令查看网络状态:
#netstat -n | awk '/^tcp/ {++S[$NF]} END { for(a in S) print(a,S[a])}'
LAST_ACK 14 SYN_RECV 348 ESTABLISHED 70 FIN_WAIT1 229 FIN_WAIT2 30 CLOSING 33 TIME_WAIT 18122
CLOSED:无连接
LISTEN:服务器在等待进入呼叫
SYN_RECV:一个连接请求已经到达,等待确认ACK
SYN_SENT:应用已经开始,打开一个连接
ESTABLISHED:正常数据传输状态
FIN_WAIT1:应用主动关闭连接
FIN_WAIT2:对端接收到FIN请求后回复ACK
CLOSING:两边同时尝试关闭
TIME_WAIT:对端关闭连接
LAST_ACK:等待最后的连接关闭请求FIN的回复ACK
TIMET_WAIT
time_wait问题参考:TCP 连接的 TIME_WAIT 问题 解决TIME_WAIT过多造成的问题
MSL在RFC1122中规定为两分钟,但是各个操作系统的实现不同,在linux上一般配置MSL为2分钟。实际应用中常用的是30秒,1分钟和2分钟等。
处于TIME_WAIT状态的连接端必须等待2*MSL后才可关闭:如果出错,对方发送错误信息。若无等待,重新建立连接后,可能收到上次的错误信息(本次建立连接快,上次错误信息还未收到)。此时无法判断错误信息是本次连接错误还是上次的。我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。
主动关闭连接的一方在发送完最后一个ACK后就处于TIME_WAIT状态,停留2MSL(max segment lifetime)时间,这是TCP/IP必不可少的,也就是“解决”不了的。
TIME_WAIT 并不会占用很大资源的,除非受到攻击。
影响
每一个 time_wait 状态,都会占用一个「本地端口」,上限为 65535
(16 bit,2 Byte);
当大量的连接处于 time_wait
时,新建立 TCP 连接会出错,address already in use : connect 异常。
问题分析
大量的 TIME_WAIT
状态 TCP 连接存在,其本质原因是什么?
1)大量的短连接存在;
2)特别是 HTTP 请求中,如果 connection
头部取值被设置为 close
时,基本都由「服务端」发起主动关闭连接
3)TCP 四次挥手
关闭连接机制中,为了保证 ACK 重发
和丢弃延迟数据
,设置 time_wait
为 2 倍的 MSL
(报文最大存活时间)
解决方案
解决上述 time_wait
状态大量存在,导致新连接创建失败的问题,一般解决办法:
1)客户端,HTTP 请求的头部,connection 设置为 keep-alive,保持存活一段时间:现在的浏览器,一般都这么进行了
2)服务器端
允许 time_wait
状态的 socket 被重用
缩减 time_wait
时间,设置为 1 MSL
(即,2 mins)
如发现系统存在大量TIME_WAIT状态的连接,服务端通过调整内核参数解决:
vim /etc/sysctl.conf
编辑文件,加入以下内容:
net.ipv4.tcp_syncookies = 1 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_tw_recycle = 1 net.ipv4.tcp_fin_timeout = 30
net.ipv4.tcp_syncookies = 1 表示开启SYN cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭; net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭; net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。 net.ipv4.tcp_fin_timeout 修改系統默认的 TIMEOUT 时间
然后执行 /sbin/sysctl -p 让参数生效。
SYN攻击
半开连接(half-open connection)
什么是半开连接?
当客户端与服务器建立起正常的TCP连接后,如果客户主机掉线(网线断开)、电源掉电、或系统崩溃,服务器进程将永远不会知道(通过我们常用的select,epoll监测不到断开或错误事件),如果不主动处理或重启系统的话对于服务端来说会一直维持着这个连接,任凭服务端进程如何望穿秋水,也永远再等不到客户端的任何回应。这种情况就是半开连接,浪费了服务器端可用的文件描述符。
客户端connect()成功返回而服务端没有正确处理(没有accept(),服务端直接关闭连接了)则客户端也会处于half-open connection。
如何处理?
熟悉套接字通用选项的朋友一定已经有了想法。TCP套接字不是有个保持存活选项SO_KEEPALIVE嘛,如果在两个小时之内在该套接字的任何一个方向上都没数据交换,TCP就自动给对端发送一个保持存活探测分节,如果此TCP探测分节的响应为RST,说明对端已经崩溃且已经重新启动,该套接字的待处理错误被置为ECONNRESET,套接字本身则被关闭。如果没有对此TCP探测分节的任何响应,该套接字的处理错误就被置为ETIMEOUT,套接字本身则被关闭。
确实,这个选项确实可以处理我们前面遇到的TCP半开连接的问题,但是默认两小时间隔探测的实时性是不是差了些呢?当然,我们可以通过修改内核参数改小时间间隔,完美了吧?但是必须注意的是大多数内核是基于整个内核维护这些时间参数的,而不是基于每个套接字维护的,因此如果把无活动周期从两小时改为(比如)2分钟,那将影响到该主机上所有开启了此选项的套接字。我想大家都不会愿意承担服务器端的这种不确定性吧。另外,心跳除了说明应用程序还活着(进程存在,网络畅通),更重要的是表明应用程序能正常工作。而SO_KEEPALIVE由操作系统负责探查,即便是进程死锁或有其他异常,操作系统也会正常收发TCP keepalive消息,而对方无法得知这一异常。
没关系,其实我们可以在应用层模拟SO_KEEPALIVE的方式,用心跳包来模拟保活探测分节。由于服务器通常要承担成千上万的并发连接,所以肯定是由客户端在应用层进行心跳来模拟保活探测分节,客户端多次收不到服务器的响应时可终止此TCP连接,而服务端可监测客户端的心跳包,若在一定时间间隔内未收到任何来自客户端的心跳包则可以终止此TCP连接,这样就有效避免了TCP半开连接的情况。
参考:
5.TCP协议格式
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步