TCP三次握手和四次挥手:面试能答不代表真懂
TCP三次握手和四次挥手面试能答不代表真懂摘要三次握手和四次挥手面试能答出来只是及格。TIME_WAIT出现在哪一端CLOSE_WAIT堆积说明什么tcp_tw_recycle为什么被移除了这些问题答得上来才算真正理解了TCP连接的生命周期。本文用抓包输出讲清楚每个状态的含义附带生产环境的排查方法和Java代码示例。关键词TCP、三次握手、四次挥手、TIME_WAIT、CLOSE_WAIT、连接排查分类网络 / Java / 后端开发面试答案只是起点三次握手面试标准答案“客户端发SYN服务端回SYNACK客户端再发ACK连接建立。”四次挥手“主动方发FIN被动方回ACK被动方发FIN主动方回ACK连接关闭。”及格了。但追几个问题为什么握手是三次不是两次为什么挥手是四次不是三次有没有可能变成三次TIME_WAIT在哪一端为什么要等2MSLCLOSE_WAIT堆积说明什么跟TIME_WAIT有什么区别tcp_tw_recycle为什么被Linux内核移除了能全答清楚的不多。先自己抓一次包理论讲一百遍不如自己看一遍。准备两台机器# 服务端nc-l8888# 客户端nc服务端IP8888客户端抓包tcpdump-ieth0host服务端IP and port8888-nn-S客户端输入任意内容回车然后CtrlC断开。你会看到完整的握手→数据传输→挥手过程。三次握手逐包拆解1 客户端.49152 服务端.8888: Flags [S], seq 1000000 2 服务端.8888 客户端.49152: Flags [S.], seq 2000000, ack 1000001 3 客户端.49152 服务端.8888: Flags [.], ack 2000001第1包客户端发SYN客户端进入SYN_SENT状态。这个包说的是“我想建连我的初始序列号是1000000。”注意seq1000000不是0也不是1是随机值。随机化是为了安全——防止TCP序列号预测攻击。如果你发现某个系统的TCP初始序列号是固定值或者规律递增的那它的连接可以被伪造。第2包服务端回SYNACK服务端从LISTEN进入SYN_RCVD状态。一个包做了两件事SYN我也要建连我的初始序列号是2000000。ACK你的序列号1000000我收到了我期望你下一个包从1000001开始。ack1000001 对端seq 1。这是TCP确认号的规则确认号 已收到的最后一个字节的序列号 1。第3包客户端发ACK客户端进入ESTABLISHED。服务端收到后也进入ESTABLISHED。连接建好。为什么是三次不是两次这是面试必问题。标准答案是防止历史重复连接但很多人不理解具体场景。看这个情况客户端发了SYNseq1000网络延迟这个包绕了很久。 客户端等不到回复重发SYNseq1500这次服务端收到并完成握手正常通信后关闭。 这时第一个延迟的SYNseq1000终于到了服务端。如果是两次握手服务端收到迟到的SYN → 发SYNACK → 认为连接建立了 → 开始等数据 客户端根本不知道有这个连接 → 不会发数据 服务端的资源白白浪费如果是三次握手服务端收到迟到的SYN → 发SYNACK 客户端收到SYNACK → 发现ack1001不是我期望的 → 发RST拒绝 服务端收到RST → 不建连资源不浪费第三次握手给了客户端一个否决权可以拒绝无效的历史连接。数据传输连接建好后双方互发数据。确认机制客户端 服务端: seq 1000001, len 6 发了6字节 服务端 客户端: seq 2000001, ack 1000007 确认收到下一个要1000007ack1000007 10000016表示1000007之前的所有字节我都收到了。TCP的确认是累积确认——不需要逐包确认效率更高。四次挥手逐包拆解4 客户端 服务端: Flags [F.], seq 1000007, ack 2000001 5 服务端 客户端: Flags [.], ack 1000008 6 服务端 客户端: Flags [F.], seq 2000001, ack 1000008 7 客户端 服务端: Flags [.], ack 2000002第4包客户端发FIN客户端说我没有数据要发了。进入FIN_WAIT_1。注意FIN只表示我不再发数据不是说我不收了。这是半关闭half-close的概念。TCP是全双工的两个方向的数据流独立控制。第5包服务端回ACK服务端确认知道你要关了。进入CLOSE_WAIT。客户端收到ACK后进入FIN_WAIT_2。第6包服务端发FIN服务端也说我也没有数据要发了。进入LAST_ACK。第7包客户端回ACK客户端确认。进入TIME_WAIT等2MSL后彻底关闭。服务端收到ACK后直接CLOSED。为什么是四次不是三次因为TCP是全双工两个方向的数据流独立。握手时服务端收到SYN的同时就决定同意连接SYN和ACK可以合并成一个包。挥手时服务端收到FIN只能先ACK确认我知道你要关了但服务端可能还有数据没发完不能立刻发FIN。所以ACK和FIN通常分成两个包。收到FIN → 立刻ACK确认对方要关→ 继续发剩余数据 → 发完后发FIN我也关了不过如果服务端收到FIN时恰好没有剩余数据ACK和FIN可以合并成一个包这时候四次挥手就变成了三次。抓包中确实偶尔能看到这种情况。状态机全景客户端 服务端 | | | ---- SYN(seqx) ---- | | SYN_SENT LISTEN | | | | --- SYNACK(seqy,ackx1) ---- | | SYN_RCVD | | ---- ACK(acky1) --- | | ESTABLISHED ESTABLISHED | | | | 数据传输 | | | | ---- FIN ---- | | FIN_WAIT_1 CLOSE_WAIT | | --- ACK ---- | | FIN_WAIT_2 | | | | --- FIN ---- | | LAST_ACK | | ---- ACK ---- | | TIME_WAIT (CLOSED) | | (等60秒) | | (CLOSED) |TIME_WAIT和CLOSE_WAIT生产环境真正在意的两个状态基础知识讲完了下面是生产环境真正会遇到的问题。TIME_WAIT出现在主动关闭方存在的两个原因确保最后一个ACK能送达。如果最后的ACK丢了服务端会重发FIN客户端需要在TIME_WAIT状态下重新回ACK。如果已经CLOSED了就没人回了。防止旧连接的延迟包干扰新连接。如果没有TIME_WAIT新连接复用了相同的四元组源IP、源端口、目的IP、目的端口旧连接的迟到的包会被误认为新连接的包。持续时间Linux上TIME_WAIT的超时是60秒硬编码在内核里MSL30秒2MSL60秒。注意net.ipv4.tcp_fin_timeout这个参数控制的是FIN_WAIT_2的超时时间默认60秒不是TIME_WAIT的超时。TIME_WAIT的60秒是写死在内核代码里的不能通过sysctl调整。# 查看TIME_WAIT数量ss-ant|grepTIME_WAIT|wc-l大量TIME_WAIT的处理# sysctl.conf# 允许复用TIME_WAIT状态的socket仅对出站连接生效net.ipv4.tw_reuse1# 扩大可用端口范围net.ipv4.ip_local_port_range102465535tcp_tw_reuse是安全的它只对出站连接生效而且会检查时间戳确保不会复用到还活着的旧连接。tcp_tw_recycle不要用。这个参数在NAT环境下会导致严重问题——同一个NAT出口后面的多台机器时间戳不一致会导致合法的SYN包被丢弃。Linux 4.12已经把这个参数移除了。如果你的内核文档里还看得到它别碰。更根本的解决办法用长连接替代短连接。TIME_WAIT堆积的元凶是大量短连接。如果是自己写的服务用连接池复用连接如果是调用外部服务HTTP用Connection: keep-alive。CLOSE_WAIT出现在被动关闭方CLOSE_WAIT是一个短暂的过渡状态——收到FIN后进入CLOSE_WAIT发完剩余数据发自己的FIN就到LAST_ACK了。但如果你的服务出现大量CLOSE_WAIT堆积说明你的代码有问题。CLOSE_WAIT意味着你的程序收到了对端的FIN对端要关连接但你的程序没有调用close()。CLOSE_WAIT不会自动消失。它不像TIME_WAIT有60秒超时CLOSE_WAIT会一直持续到你的程序主动close。一旦堆积只会越来越多直到文件描述符耗尽进程崩掉。# 查看CLOSE_WAIT数量ss-ant|grepCLOSE_WAIT|wc-l# 找到对应的进程ss-antp|grepCLOSE_WAIT|head-5CLOSE-WAIT 0 0 10.0.1.50:45000 10.0.1.60:6379 users:((java,pid12345,fd89))同一个进程大量CLOSE_WAIT大概率是连接泄漏。常见原因// 连接泄漏的典型代码Connectionconnpool.getConnection();Resultresultconn.query(sql);// 如果这里抛异常pool.release(conn);// 这行就不会执行连接就泄漏了// 正确写法try-finally确保归还Connectionconnpool.getConnection();try{Resultresultconn.query(sql);returnresult;}finally{pool.release(conn);// 无论异常与否都归还}这种泄漏不直接报错表现是运行一段时间后连接池耗尽、新请求超时。重启就好了过几天又出现。如果你的Java服务有这种症状第一件事查CLOSE_WAIT。各状态速查表状态在谁身上意味着大量堆积说明SYN_SENT主动连接方发了SYN等回复对方没响应查防火墙或端口SYN_RCVD被动连接方收到SYN等ACK可能是SYN Flood攻击ESTABLISHED双方连接已建立正常FIN_WAIT_1主动关闭方已发FIN等ACK对方没回ACK网络问题FIN_WAIT_2主动关闭方已收到ACK等对方FIN对方程序没close()CLOSE_WAIT被动关闭方收到对方FIN自己还没关自己的代码没closeLAST_ACK被动关闭方已发FIN等最后ACK正常过渡极少堆积TIME_WAIT主动关闭方等60秒后彻底关闭短连接太多用长连接解决一句话CLOSE_WAIT是代码bug的信号TIME_WAIT是架构设计的信号。前者查代码后者改架构。排查实战连接状态分布ss-ant|awk{print $1}|sort|uniq-c|sort-rn5000 ESTABLISHED 2000 TIME_WAIT 500 CLOSE_WAIT 50 SYN_SENTTIME_WAIT 2000不一定有问题短暂存在是正常的。CLOSE_WAIT 500就有问题了。CLOSE_WAIT对应哪个进程ss-antp|grepCLOSE_WAIT|awk{print $6}|sort|uniq-c|sort-rn480 users:((java,pid12345,fd89)) 20 users:((python,pid6789,fd12))java进程pid12345占了绝大多数。然后用jstack看这个进程在干什么。SYN_SENT堆积ss-ant|grepSYN_SENT|wc-l如果SYN_SENT大量堆积说明你的服务器在主动建立连接但对方没回应。可能是下游服务挂了、防火墙拦了、或者端口没开。# 看SYN_SENT都连着谁ss-ant|grepSYN_SENT|awk{print $5}|sort|uniq-c|sort-rn45 10.0.1.60:3306 5 10.0.1.70:637910.0.1.60:3306MySQL有45个连接卡在SYN_SENT。大概率MySQL挂了或者被防火墙拦了。给Java开发的几个建议连接池配置// Apache HttpClient 4.xPoolingHttpClientConnectionManagercmnewPoolingHttpClientConnectionManager();cm.setMaxTotal(200);// 总连接数cm.setDefaultMaxPerRoute(50);// 每个目标的最大连接数// 超时配置三个都要设RequestConfigconfigRequestConfig.custom().setConnectTimeout(3000)// TCP建连超时.setSocketTimeout(5000)// 等待响应超时.setConnectionRequestTimeout(2000)// 从连接池获取连接超时.build();ConnectionRequestTimeout很多人漏了。不设的话连接池满了请求会无限卡住排查起来很迷惑——日志里看起来像是请求没发出去实际是卡在等连接。长连接保活# Spring Boot配置server:tomcat:connection-timeout:20s# 空闲连接超时keep-alive-timeout:60s# Keep-Alive超时max-keep-alive-requests:100# 单连接最大请求数定期检查连接状态建议在监控系统里加上TCP状态监控#!/bin/bash# tcp_state_monitor.sh - 每分钟执行一次CLOSE_WAIT$(ss-ant|grep-cCLOSE_WAIT)TIME_WAIT$(ss-ant|grep-cTIME_WAIT)if[$CLOSE_WAIT-gt100];thenecho[ALERT] CLOSE_WAIT$CLOSE_WAIT可能有连接泄漏fiecho$(date)CLOSE_WAIT$CLOSE_WAITTIME_WAIT$TIME_WAIT/var/log/tcp_state.logCLOSE_WAIT超过阈值就告警早发现早修复。最后面试能答出三次握手四次挥手只是及格。生产环境需要理解的是每个状态的含义知道什么情况下会堆积堆积了怎么排查和解决。CLOSE_WAIT是代码bug的信号——查连接泄漏。TIME_WAIT是架构设计的信号——用长连接替代短连接。SYN_SENT堆积是下游出了问题——查目标服务和防火墙。这些不是运维的专属知识。做后端开发的线上出问题排查方向搞反了——明明是网络连接的问题却一直在查代码逻辑白白浪费时间。# 排查问题之前先看一眼连接状态ss-ant|awk{print $1}|sort|uniq-c|sort-rn一行命令十秒钟可能省掉你半天的排查时间。