ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

TCP三次握手与四次挥手:原理、状态机与线上排障实战

TCP三次握手与四次挥手:原理、状态机与线上排障实战 TCP三次握手和四次挥手大概是整个TCP/IP协议栈里被讨论最多、也最容易被“背熟了但没吃透”的内容。很多人面试前能一字不差地说出SYN、SYN-ACK、ACK也能画出那几个经典的状态迁移框可真到了线上出问题——比如连接卡在CLOSE_WAIT、客户端报“Address already in use”、Docker映射端口失败——却很难把这些现象和握手挥手的机制对上号。这篇文章不打算让你再背一遍八股。我会从“为什么需要三次”“为什么断开要四次”这些根本问题出发把连接管理的设计逻辑、报文细节、状态机、故障排查经验串成一个系统性的知识体系。无论你是后端开发、运维工程师、网络爱好者还是在准备面试这套内容都能帮你从“背结论”进阶到“用结论”。1. 三次握手为什么不是两次也不是四次1.1 不可靠信道上的可靠性困境要理解三次握手先要回到TCP最底层的处境它是跑在IP协议之上的而IP协议只提供“尽力而为”的交付报文可能在网络中丢失、延迟、重复甚至乱序到达。TCP的一切设计本质上都是在和这种不可靠性对抗。三次握手要解决的核心问题是在两个还不知道对方状态的端点之间建立一条双方都确认“可用”的逻辑连接。这里至少包含三层含义第一双方的收发能力都正常第二双方能同步各自的初始序列号第三这个连接是“当前这次通信”的连接而不是历史遗留的过期报文。生活化的类比是这样两个人约好要开始长期通信但通信过程没有回执。发消息的人不知道对方在不在、有没有收到、会不会误解。三次握手相当于双方互相喊话确认“你能听到我吗”“我能听到你你能听到我吗”“我也能听到你。”只有听到这个确认链完成双方才敢放心发正式内容。1.2 两次握手的致命缺陷历史报文问题如果不考虑可靠性只考虑“建立连接”两次交互似乎就够了客户端发SYN服务器回SYNACK然后双方就进入连接状态。但这里隐藏着一个非常经典的故障场景叫做历史报文干扰。假设客户端发起一次连接发送了一个SYN报文但这个报文在网络中滞留了很久比如因为某个路由器拥塞。客户端等不到响应超时后重新发起连接这次连接很快建立、通信、关闭。然而之前那个滞留在网络中的旧SYN报文这时候才姗姗来迟地到达服务器。如果只有两次握手服务器收到这个SYN后会认为客户端想建立新连接于是返回SYNACK并分配连接资源、进入“半连接”状态。问题在于客户端此时根本不需要这个连接收到这个来源不明的SYNACK后只会觉得莫名其妙。更麻烦的是服务器已经把资源分配出去了这个资源可能一直等到超时才释放。如果攻击者故意反复发送这种无效SYN就能让服务器的半连接队列被打满导致正常用户无法建立连接——这就是SYN Flood攻击的基本原理。三次握手的机制可以解决这个问题客户端收到服务器的SYNACK后会检查确认号是否正确判断这是不是自己期望的连接。如果发现这个连接已经被自己放弃比如确认号不对客户端不会进入连接状态而是发送RST报文把这条“幽灵连接”拆掉。正是因为有了客户端的最终确认这一步服务器才敢确认“这是当前真实存在的连接”而不至于被历史报文骗走了资源。1.3 三次握手的同步收益序列号与窗口参数交换除了防历史报文三次握手还承担着一个容易被忽略的职能同步序列号。TCP的可靠性靠序列号Sequence Number保证接收方通过确认号Acknowledgment Number告诉发送方“我期望收到哪个字节”。如果两端没有同步好序列号后续的排序、去重、重传都无法工作。三次握手中客户端在SYN中携带自己的初始序列号x服务器在SYNACK中携带自己的初始序列号y并确认收到x最后客户端再确认收到y。到这里双方的序列号对齐数据字节流才能在这个安全边界上展开。握手过程中还同时交换了MSS最大报文段大小、窗口大小、是否支持SACK、时间戳等关键选项。这些参数在握手时报给对端双方根据对方能力协商出后续连接的传输策略。我以前排查过一个大文件传输慢的问题最后查下来就是握手时MSS协商得过小导致每个包的有效载荷很低吞吐上不去。从这个意义上说三次握手不只是“打招呼”它是连接参数的协商会场。2. 三次握手全流程拆解报文、状态与抓包对照2.1 三步报文的序列号与确认号推演三次握手的具体报文交互看起来简单但每一步的序列号和确认号变化值得仔细推演一遍因为很多人在这一块栽过跟头。第一步客户端向服务器发送SYN报文。SYN标志位为1序列号设为一个随机初始值x。这是客户端告诉服务器我想建立连接我的起始字节编号是x。注意SYN报文会消耗一个序列号所以服务器确认时要用x1。第二步服务器回应SYNACK报文。SYN标志位为1ACK标志位为1序列号设为服务器自己的随机初始值y确认号设为x1。这个x1表示“我已经收到了你那个编号为x的SYN并且准备好接收你下一个字节”。服务器同时也在同步自己的序列号所以必须带SYN。第三步客户端发送ACK报文。ACK标志位为1序列号设为x1确认号设为y1。注意这个纯ACK报文不携带实际业务数据因此不消耗序列号。到这一步双方都确认对方已收到自己的初始序列号连接进入ESTABLISHED状态。下面用一个具体的数字示例来演示步骤方向标志位序列号(seq)确认号(ack)1客户端→服务器SYN11000x无2服务器→客户端SYN1, ACK12000y1001x13客户端→服务器ACK11001x12001y1第三步完成之后双方都在内核里建好了连接对象。客户端状态从SYN_SENT变成ESTABLISHED服务器状态从SYN_RCVD变成ESTABLISHED。此后客户端发出的第一个数据字节的序列号就是1002服务器回包时确认号会基于这个继续累加。2.2 状态迁移与关键TCP选项三次握手中双方的TCP状态机变化是这样的客户端CLOSED → SYN_SENT发出SYN后→ ESTABLISHED收到SYNACK并发出ACK后。服务器CLOSED → LISTEN应用调用listen后→ SYN_RCVD收到SYN后→ ESTABLISHED收到ACK后。我强烈建议你把这些状态名背下来因为后面排障时看到连接卡在某个状态基本就能推断出问题在哪一步。例如客户端一直处于SYN_SENT说明SYN发出去后没收到任何响应可能是网络丢包或防火墙拦截服务器大量连接堆积在SYN_RCVD说明收到了SYN但等不到最后一个ACK大概率是同步队列被打满或客户端异常。握手过程中的TCP选项也很关键。现代TCP栈在SYN中都会携带MSS选项声明自己能够接收的最大报文段大小携带窗口缩放因子Window Scale让窗口字段能从16位扩展到更大的实际窗口携带SACK Permitted协商是否支持选择性确认。如果这些选项在握手时没能协商成功后续可能出现吞吐受限或丢包重传效率低的问题。2.3 握手异常队列打满、SYN Flood与syncookies服务器的握手过程并不是收到SYN就立刻建好完整连接而是分两步先在半连接队列SYN Queue也叫request_sock队列里放一个半连接条目等收到ACK后再把连接移动到全连接队列Accept Queue等待应用调用accept()取走。这两个队列的大小有限。如果连接请求太快或者最后一个ACK迟迟不来半连接队列会被打满这时候服务器只能丢弃新到的SYN或者通过syncookies机制绕过队列限制。Linux下的net.ipv4.tcp_syncookies就是干这个的——它把半连接信息编码进SYNACK的序列号里等ACK回来再重建连接以此抵抗SYN Flood。排查这类问题最实用的命令是ss -lnt看Recv-Q和Send-Q。Recv-Q堆积代表应用来不及accept全连接队列满了Send-Q堆积代表半连接队列满常见于SYN Flood或者客户端NAT环境下大量握手超时。这里踩过的坑是很多人觉得调大tcp_max_syn_backlog就能缓解实际上全连接队列上限还受listen backlog和net.core.somaxconn双重影响应用程序也要把listen的backlog参数调大三层要一起调才生效。3. 四次挥手为什么断开反而更麻烦3.1 全双工关闭的本质每个方向都要独立告别很多人第一次学到四次挥手时的反应是连接建立只要三次断开反倒要四次这也太不对称了。理解这个不对称关键要记住TCP连接是全双工的——数据的流动是双向的两个方向可以独立地关闭。想象一条双向隧道入口和出口分别由两端控制。A不想再发送数据了它需要告知B“我这边发完了”但B可能还有数据要发给A所以B不会因为收到这个消息就立刻关闭整个连接。只有当B也把自己这边的数据发完并且也告诉A“我也发完了”双方才能确认整条连接没有遗漏的数据。所以四次挥手一共包含四条报文主动关闭方发送FIN被动关闭方回复ACK被动关闭方再发送FIN主动关闭方最后回复ACK。如果中间被动关闭方恰好也没有数据要发了它可以把ACK和FIN合成一个报文这样四次挥手就“压缩”成了三次在抓包里偶尔能看到这种情形。3.2 挥手过程状态机拆解假设客户端主动发起关闭全程状态迁移如下步骤方向报文客户端状态服务器状态1客户端→服务器FIN1FIN_WAIT_1CLOSE_WAIT2服务器→客户端ACK1FIN_WAIT_2CLOSE_WAIT3服务器→客户端FIN1TIME_WAITLAST_ACK4客户端→服务器ACK1等2MSL后CLOSEDCLOSED这里最需要关注的是被动关闭方的CLOSE_WAIT状态。服务器收到FIN后kernel会回复ACK然后进入CLOSE_WAIT表示“对方已经关了他那边的门现在等我这边关门”。但kernel不知道应用层的业务逻辑什么时候结束它只能等应用调用close()来关闭socket。如果应用代码有bug没有及时关闭这个socket连接就会一直挂在CLOSE_WAIT上。我在实际运维中见过一个很典型的案例后端服务用Java NIO读数据超时后没有在finally里正确关闭channel导致大量连接堆积在CLOSE_WAIT。表面上看是“连接数暴涨”底层原因其实是四次挥手的第三步没有走完。排查方法很简单执行ss -ant | grep CLOSE_WAIT如果这个状态的数量持续高位基本可以断定是应用层泄漏。3.3 TIME_WAIT与2MSL代价最大的等待四次挥手里最容易被轻视的是主动关闭方最后进入的TIME_WAIT状态。按照TCP规范主动关闭方发送最后一个ACK后不能立刻关闭而是必须等待2MSL的时间。MSLMaximum Segment Lifetime是报文在网络中存活的最长时间Linux常见配置为30秒到2分钟不等所以TIME_WAIT通常会持续60秒到4分钟。TIME_WAIT存在有两个原因。第一保证最后一个ACK能到达对端。如果这个ACK丢失被动关闭方会超时重发FIN主动关闭方需要能再次响应。第二让网络中残留的旧报文自然消亡。如果某个旧连接的数据包还在网络上游荡而新连接恰好复用了相同的四元组旧报文就可能被误认为新连接的数据造成数据混乱。等待2MSL就是确保旧报文在时间上“过期失效”。TIME_WAIT的成本也正在于此。高并发短连接场景下主动关闭方会积累大量TIME_WAIT连接这些连接占用的本地端口在2MSL内无法快速复用。当端口池耗尽新连接就会失败报错通常是“Address already in use”。热搜词里Java客户端重连时报地址已在使用十有八九就是这个原因。解决办法有几个层次应用层优先做连接复用用长连接池减少频繁断开TCP层可以开启net.ipv4.tcp_tw_reuse让内核在安全前提下复用TIME_WAIT连接再不行就扩大本地端口范围net.ipv4.ip_local_port_range。但注意不要开tcp_tw_recycle它在NAT环境下有严重的兼容性问题Linux新版本内核也已经把它移除了。3.4 握手与挥手核心对比表把三次握手和四次挥手放在一张表里对比能帮你更清楚地看到它们各自的设计目标和风险点对比维度三次握手四次挥手目标建立连接、同步序列号、协商参数关闭连接、确认双方数据发送完毕报文数3个4个可合并为3个发起方客户端主动任意一方可主动关键状态SYN_SENT、SYN_RCVD、ESTABLISHEDFIN_WAIT_1/2、CLOSE_WAIT、LAST_ACK、TIME_WAIT失败后果连接无法建立连接泄漏、端口耗尽常见故障握手超时、半连接队列满CLOSE_WAIT堆积、TIME_WAIT过多优化方向syncookies、队列扩容长连接、tcp_tw_reuse、TIME_WAIT调参真正的大规模线上问题很多都集中在挥手这一侧。因为建立连接的流量模式简单统一而关闭连接时涉及两端应用层状态是否同步、是否及时释放资源容易出幺蛾子。4. 真实排障实录那些和握手挥手有关的高频故障4.1 场景一本地端口耗尽Java重连报“Address already in use”这是我在生产环境里真实遇到过的Case。应用是一个Java客户端每处理一条消息就新建TCP连接处理完就关闭吞吐一上来日志里开始不断报“java.net.BindException: Address already in use”。排查路径很简单先看本机TCP状态统计执行ss -ant | awk {print $1} | sort | uniq -c发现TIME_WAIT数量高达数万个。再查端口范围cat /proc/sys/net/ipv4/ip_local_port_range默认是32768到60999扣除TIME_WAIT占用可用端口很快枯竭。这正对应着四次挥手里的TIME_WAIT等待主动关闭方每个短连接都要停留2MSL。最终修复组合有三步第一步改造客户端为连接池复用长连接这是治本第二步在服务端开启tcp_tw_reuse允许安全复用TIME_WAIT这是缓解第三步把net.ipv4.ip_local_port_range扩大到1024到65535给短连接更多余量。注意顺序很重要如果一上来就改端口范围只是把问题往后推连接池才是根本解法。4.2 场景二Docker端口映射失败“ports are not available”热搜词里还有一条很典型的报错error response from daemon: exposing port tcp 0.0.0。这个错误在Docker启动容器时很常见但很多人不知道它和TCP连接管理有什么关系。Docker在宿主机上为容器映射端口时本质是要在宿主机上占用一个TCP监听端口。如果这个端口已经被其他进程占用或者内核里存在大量未清理的连接残留Docker会认为端口不可用。更深一层的原因是当容器频繁重启、服务不断建立和断开连接时宿主机上积累了大量的TIME_WAIT连接这些连接和待映射的端口形成冲突导致端口明明“空闲”却无法绑定。遇到这个情况我的排查习惯是先执行ss -tan | grep TIME_WAIT看看数量再确认目标端口有没有被占用。如果仅仅是TIME_WAIT干扰用上面说的tcp_tw_reuse加上合理设置容器网络模式比如用host网络基本能解决。如果端口确实被占用则看占用进程是不是僵尸残留必要时清理容器或调整端口规划。4.3 场景三CLOSE_WAIT堆积应用连接泄漏CLOSE_WAIT堆积是后端开发最容易踩的坑也是面试里很容易被问到的实战题。现象是服务运行一段时间后连接数持续上涨负载均衡器开始报后端健康检查失败重启服务后短暂恢复然后又开始堆积。原因在四次挥手里讲过了被动关闭方收到了对方的FIN内核回了ACK进入CLOSE_WAIT但应用层的socket没有被close。健康检查发送的探测请求也得不到正常响应服务被判定为不健康。排查的关键是定位哪个进程持有这些连接。用ss -antp能找到进程号再用jstack或gcore去看这个线程在干什么。常见的bug模式有处理完请求后没有关闭InputStream和socketHTTP客户端连接池中的连接没有正确返还框架的读写超时处理不当导致连接永远不释放。修复后CLOSE_WAIT数量应该像潮水一样快速消退。4.4 场景四TCP DUP ACK大量出现链路质量预警热搜词里的“tcp dup ack机制”是TCP可靠性体系里的另一个侧面但和挥手握手也有关联当接收方收到乱序报文或者检测到丢包时会重复发送ACK告诉发送方“我还等着某个编号的包”。发送方连续收到3个重复的ACK就会触发快速重传不需要等待超时。在抓包里看到大量TCP DUP ACK等于看到了一条链路的丢包和乱序信号。正常的轻微乱序会有少量DUP ACK但如果数量多到影响吞吐最好检查网卡丢包率、物理链路光衰、中间设备的缓冲队列。这里补充一个实操技巧tcpdump抓包时可以用-i any但判断网卡层面的丢包还是用ethtool -S eth0看rx_dropped和tx_dropped更直接。5. 概念辨析、误区纠偏与面试追问扩展5.1 高频误区与概念澄清表围绕三次握手和四次挥手网络上流传的误解非常多。我把常见误区整理成一张表逐个纠正误区事实三次握手就是三个包一定一发一收实际是三次交互中间那个包同时带SYN和ACK方向相反时不排除其他组合握手建立后双方就确定了所有可靠性能力握手还协商了MSS、窗口、SACK后续路径变化时可能重协商TIME_WAIT是服务器该担心的状态TIME_WAIT只会出现在主动关闭方谁主动关谁承担四次挥手后连接立即关闭主动方需要等2MSL被动方等收到最后一个ACK才关闭ACK报文都消耗序列号纯ACK不消耗序列号SYN和FIN各消耗一个序列号RST和FIN可以互相替代RST是异常关闭FIN是正常关闭语义不同处理方式也不同连接状态只有ESTABLISHED需要关注SYN_SENT、SYN_RCVD、CLOSE_WAIT、TIME_WAIT都是排障的向导5.2 更进一步的追问TFO、半关闭、同时打开面试官或深入学习的读者往往不满足于标准流程还会追问几个扩展场景。第一个是TCP Fast OpenTFO。标准握手需要一次RTT才能发数据TFO允许客户端在握手的同时带上应用数据节省一次往返。原理是服务器在之前的握手中给客户端一个Cookie下次客户端带着Cookie直接发数据。这个机制对短时延敏感的业务很有用但要在应用和内核两侧都开启。第二个是半关闭half-close。shutdown可以只关闭发送方向保留接收方向。这在一端不想再发数据但仍想接收对端剩余数据时很好用比如客户端发送完请求后调用shutdown(SHUT_WR)服务器知道客户端不会再发数据可以放心处理并关闭。第三个是同时打开。两台主机同时向对方发SYN理论上可行但实际很少见。这种情况下双方都会进入SYN_SENT然后收到对方的SYNACK后变成ESTABLISHED整个过程需要四次报文交互。这个知识点在面试里被问得不多但能讲清楚说明对TCP协议栈有更深入的理解。5.3 连接管理知识如何化为排障直觉顺着上面这些场景走一圈你会发现三次握手和四次挥手不只是面试题它其实是一张排障地图。看到SYN_SENT堆积第一反应是网络路径不通或者防火墙丢包看到SYN_RCVD大量想到半连接队列和syncookies看到ESTABLISHED数量异常却无流量怀疑应用层资源泄漏看到CLOSE_WAIT堆积确认应用没调用close看到TIME_WAIT过多结合业务判断是短连接太频繁还是端口池太小。这条直觉链一旦建立你面对线上网络问题时就不再是一头雾水而是能根据状态快速定位到具体环节。我个人这几年的体会是TCP连接管理机制就像一个放大镜把抽象的网络问题翻译成了具体的、可以操作的状态和参数。谁掌握这套翻译规则谁就能在网络排障中掌握主动权。最后补一句实操建议抓包永远是最好的学习工具用tcpdump抓一次localhost上到找个服务的握手挥手全流程自己亲手写一个简单的客户端和服务端观察双端的状态变化你会比看十篇文章都记得牢。
返回列表