TCP三次握手与四次挥手详解:从Wireshark抓包到实战排障

发布时间:2026/7/29 4:54:27

TCP三次握手与四次挥手详解:从Wireshark抓包到实战排障 1. 从一个真实的网络连接问题说起前几天一个刚入行的后端同事跑来找我说他们负责的服务间歇性地出现客户端连接失败错误日志里频繁出现“Connection refused”或者“Connection timeout”。他排查了防火墙、检查了服务端口监听状态甚至重启了服务问题依旧时隐时现。我让他抓个包看看几分钟后他发来一张Wireshark的截图指着里面一堆红色的“TCP Retransmission”和“TCP Dup ACK”标记一脸茫然地问我“这些是啥意思跟连接失败有关系吗”我一看就明白了这又是一个对TCP基础握手和挥手过程理解不透彻导致的典型排障困境。那些红色的标记正是TCP协议在通信异常时发出的“求救信号”。而理解这些信号的前提是必须吃透TCP连接建立与拆除的核心机制——三次握手和四次挥手。这不仅仅是教科书上的理论更是我们每天在线上系统里、在抓包分析中、在性能调优时无时无刻不在与之打交道的实战基础。很多人觉得这个概念老生常谈但据我观察能真正把每个报文、每个状态、每个异常场景都串起来讲清楚的人并不多。今天我就结合十多年踩坑的经验用最详细的实例图帮你把这块骨头啃透让你下次看到Wireshark里那些花花绿绿的包时心里门儿清。2. 为什么是“三次”握手—— 深入协议设计的博弈在展开细节之前我们必须先回答一个根本问题为什么是三次而不是两次或四次这个问题搞懂了整个握手过程就理解了八成。想象一下两个人打电话。A打给B。第一次A - BA说“喂听得到吗” 这是一个SYNSynchronize Sequence Numbers包A告诉B“我想和你建立连接我这边数据包的初始序号是X。”如果只有两次B听到后回一句“听到了你说吧。” 这是一个SYN-ACK包B对A说“你的请求我收到了ACK我同意建立连接我这边数据包的初始序号是Y。” 然后B就认为连接已经建立开始等待A发送数据。但问题来了A可能根本没收到B的回复网络丢包了。B会一直空等浪费资源。这就是“两次握手”无法解决的已失效连接请求问题。所以需要第三次A - BA必须再对B的回复进行一次确认“好的我也听到你的回复了那我们开始通话吧。” 这是一个ACK包。这样B在收到这个ACK后才能百分之百确定双方对“连接已建立”达成了共识。从状态机视角看这三次交互确保了通信双方Client和Server的发送能力和接收能力都得到了双向验证Client发送SYN验证了Client的发送能力、Server的接收能力如果Server没收到Client会超时重传。Server发送SYN-ACK验证了Server的发送和接收能力因为这是对Client SYN的回复、Client的接收能力如果Client没收到Server会超时重传。Client发送ACK最终确认了Client的接收能力和Server的发送能力。少于三次无法消除历史遗留的无效连接请求带来的资源浪费风险多于三次则显得冗余不符合协议设计的简洁高效原则。这就是“三次”背后的精妙之处它是在不可靠的IP网络上构建可靠通信的最小成本共识方案。2.1 核心字段拆解SYN、ACK、Seq、Ack在Wireshark里每一个TCP报文都像一张身份证关键信息都在几个标志位和数字字段里。SYN (Synchronize)同步序列号标志。当SYN1时表示这是一个连接请求或连接接受报文。在握手阶段SYN包会消耗一个序列号这意味着它需要被对方确认。ACK (Acknowledgment)确认标志。当ACK1时确认号Acknowledgment Number字段才有效。TCP规定在连接建立后所有传送的报文段都必须把ACK置1。Seq (Sequence Number)序列号。占4字节。TCP是面向字节流的在一个TCP连接中传送的每一个字节都会按顺序编号。Seq就是这个报文段所发送的第一个数据字节的序号。初始序列号ISN在握手时随机生成而非从0或1开始这是为了防止网络延迟导致的历史报文被误认为是新连接的数据。Ack (Acknowledgment Number)确认号。占4字节。是期望收到对方下一个报文段的第一个数据字节的序号。它代表的是“到Ack-1为止的所有数据我都已经收到了请你从Ack这个序号开始发”。因此Ack 对方上次发送的Seq 对方上次发送的数据长度Len 1。对于纯SYN或FIN包不携带数据其数据长度Len为0但它们依然消耗一个序列号所以对应的Ack需要1。注意很多人会混淆Seq和Ack的方向。记住每个TCP连接都有两个独立的字节流一个从A到B一个从B到A。A发送的Seq和B回复的Ack指向的是从A到B这个方向的数据流。反之亦然。3. 三次握手全流程与实战抓包分析现在我们结合一个真实的、最简单的HTTP连接建立过程用Wireshark抓包来一步步拆解。假设ClientIP: 192.168.1.100要访问ServerIP: 10.0.0.1的80端口。初始状态Client处于CLOSED状态然后主动发起连接进入SYN-SENT状态。Server在80端口监听处于LISTEN状态。3.1 第一次握手SYNClient生成一个随机初始序列号假设client_isn 1000然后构造一个TCP报文。设置SYN 1ACK 0。设置Seq client_isn 1000。因为这是第一个包没有需要确认的对方数据所以Ack 0实际上Wireshark可能会显示为相对值0。将报文发送给Server。在Wireshark中你看到的包大概是这样Transmission Control Protocol, Src Port: 54321, Dst Port: 80, Seq: 1000, Len: 0 Flags: 0x002 (SYN) ...0000 00.0. Reserved: Not set .... ..1. SYN: Set .... ...0 FIN: Not set这个包发出去后Client状态变为SYN-SENT。3.2 第二次握手SYN-ACKServer收到SYN包后如果同意建立连接则会分配连接资源如Socket缓冲区。生成自己的随机初始序列号假设server_isn 5000。构造回复报文。设置SYN 1,ACK 1。这表示“我同意建立连接SYN并且我收到了你刚才的SYN包ACK”。设置Seq server_isn 5000。设置Ack client_isn 1 1001。这个1001的意思是“你的Seq1000的SYN包我收到了我期望你下一个数据字节从1001开始发”。将报文发送给Client。Server状态变为SYN-RCVD。Wireshark中的包Transmission Control Protocol, Src Port: 80, Dst Port: 54321, Seq: 5000, Ack: 1001, Len: 0 Flags: 0x012 (SYN, ACK) ...0000 01.0. Reserved: Not set .... ..1. SYN: Set .... ...0 FIN: Not set Window size value: 65535 [Calculated window size: 65535] Acknowledgment number: 1001 (relative ack)3.3 第三次握手ACKClient收到SYN-ACK包后检查Ack是否为期待的client_isn 1即1001确认这是对自己SYN的合法回应。检查SYN标志确认Server也发起了连接。至此Client认为连接已建立状态变为ESTABLISHED。构造确认报文。设置SYN 0,ACK 1。设置Seq 1001。注意这里Seq不再是1000因为第一次握手的SYN消耗了序列号1000所以下一个可用的序号是1001。设置Ack server_isn 1 5001。表示“你的Seq5000的SYN包我收到了我期望你从5001开始发数据”。将此ACK包发送给Server。Server收到这个ACK包后检查Ack是否为期待的server_isn 1即5001。确认无误后Server状态也变为ESTABLISHED。至此三次握手完成双向通信通道正式建立。Wireshark中第三个包Transmission Control Protocol, Src Port: 54321, Dst Port: 80, Seq: 1001, Ack: 5001, Len: 0 Flags: 0x010 (ACK) ...0000 01.0. Reserved: Not set .... ..0. SYN: Not set .... ...0 FIN: Not set Acknowledgment number: 5001 (relative ack)实操心得在Wireshark中你可以使用过滤表达式tcp.stream eq 0来跟踪某一条完整的TCP流。观察握手阶段重点关注Seq和Ack数字的变化规律。一个健康的握手其Seq和Ack的增长是严格符合上述公式的。如果出现数字对不上或者标志位异常往往就是问题的起点。4. 连接拆除为什么挥手需要“四次”通信完毕需要断开连接。断开连接的过程同样是为了保证可靠性但比建立更复杂因为TCP连接是全双工的数据可以双向独立传输。因此每个方向都必须单独关闭。4.1 四次挥手流程详解假设Client主动发起关闭。初始状态双方均为ESTABLISHED。4.1.1 第一次挥手FINClient应用层调用close()或shutdown(SHUT_WR)表示“我没有数据要发给你了”。TCP协议栈会发送一个FIN报文。设置FIN 1,ACK 1通常ACK会置1因为可能还在确认之前的数据。Seq KK为Client最后发送的一个数据字节的序号1。Ack LL为Client期望收到的来自Server的下一个字节序号。Client发送FIN后进入FIN-WAIT-1状态。FIN报文消耗一个序列号。4.1.2 第二次挥手ACKServer收到FIN后TCP协议栈会立刻回复一个ACK报文进行确认。设置ACK 1。Seq L即上一次发送数据的序号1。Ack K 1。表示“你的FIN包SeqK我收到了”。发送ACK后Server进入CLOSE-WAIT状态。此时从Client到Server这个方向的连接就关闭了Client不能再发数据但Server可能还有数据要发给Client即“半关闭”状态。Client收到这个ACK后状态从FIN-WAIT-1变为FIN-WAIT-2。4.1.3 第三次挥手FIN当Server应用层也决定关闭连接处理完所有数据后它会发送自己的FIN报文。设置FIN 1,ACK 1。Seq L注意这个L可能比第二次挥手时的Seq要大因为Server在这期间可能又发送了一些数据。Ack K 1保持不变因为Client方向已关闭没有新数据过来。Server发送FIN后进入LAST-ACK状态。4.1.4 第四次挥手ACKClient收到Server的FIN后必须发送ACK进行确认。设置ACK 1。Seq K 1因为第一次挥手的FIN消耗了序号K。Ack L 1。表示“你的FIN包SeqL我收到了”。发送ACK后Client进入TIME-WAIT状态。等待2MSLMaximum Segment Lifetime报文最大生存时间通常为2分钟后状态才变为CLOSED。Server收到这个ACK后状态立刻变为CLOSED。至此四次挥手完成连接彻底释放。4.2 关键状态解析CLOSE-WAIT、TIME-WAIT 与排障这里有两个状态是线上问题的高发区必须深刻理解。CLOSE-WAIT当一端Server收到对端的FIN并回复ACK后就进入这个状态。这个状态的存在是为了等待本地的应用层处理完所有待发送的数据。如果应用层代码有Bug没有及时调用close()那么这个连接就会一直停留在CLOSE-WAIT状态成为“僵尸连接”占用系统资源如文件描述符。通过netstat -an | grep CLOSE_WAIT可以查看如果数量持续增长基本可以断定是应用层代码逻辑问题比如没有正确关闭Socket。TIME-WAIT主动关闭连接的一方Client在发送完最后一个ACK后会进入此状态持续2MSL。它有两个至关重要的目的可靠地终止TCP连接如果Client发送的最后一个ACK丢失了Server在超时后会重传它的FIN。处于TIME-WAIT状态的Client可以再次收到这个FIN并重传ACK从而保证连接能可靠地关闭。如果没有TIME-WAITClient直接关闭那么Server重传的FIN将得不到回应会一直重试无法正常关闭。让旧连接的报文在网络中消逝防止具有相同四元组源IP、源端口、目的IP、目的端口的新连接收到属于旧连接的延迟报文造成数据混乱。TIME-WAIT状态本身是TCP设计的一部分不是错误。但在高并发的短连接场景下如Web服务器大量主动关闭连接的服务器端会产生成千上万的TIME-WAIT连接可能耗尽端口资源。常见的优化手段包括开启Linux内核参数net.ipv4.tcp_tw_reuse和net.ipv4.tcp_tw_recycle后者在较新内核中已废弃需谨慎。设计长连接池减少连接的创建和销毁。修改应用逻辑让客户端主动关闭连接将TIME-WAIT转移到客户端但这需要架构支持。5. 从理论到实战Wireshark中的异常报文解读理解了正常流程我们就能快速定位异常。回到开头我同事遇到的问题那些红色的标记是什么意思TCP Retransmission (重传)发送方发送一个报文段后启动定时器。如果在定时器超时前没有收到确认就会重传这个报文段。在Wireshark中你会看到Seq和Ack数字相同的包被重复发送。常见原因网络拥塞、对端处理缓慢、中间链路丢包。持续的重传是网络质量差或对端服务异常的重要指标。TCP Dup ACK (重复确认)当接收方收到一个失序的报文段比如期望Seq1001但收到了Seq2001它会立即发送一个重复的ACK其Ack号指向它期望的那个缺失的字节序号即1001。发送方收到3个或以上重复的ACK即快速重传阈值后会立即重传缺失的报文段而不必等待超时。这是TCP快速重传机制。在Wireshark中你会看到一连串Ack号相同的包。TCP Out-Of-Order (乱序)网络报文可能经由不同路径到达导致接收顺序与发送顺序不一致。TCP协议栈会在缓冲区中对它们进行重组。少量的乱序是正常的大量的乱序可能意味着网络路径不稳定。TCP Previous segment not captured (前一个报文段未捕获)这通常不是网络问题而是Wireshark的提示意味着在抓包时可能漏掉了这个报文比如抓包点不在路径上或缓冲区满。需要结合其他信息判断。实战排障步骤定位流在Wireshark中使用过滤tcp.port [问题端口]或ip.addr [问题IP]找到有问题的TCP流。看握手检查三次握手是否完整完成SYN - SYN-ACK - ACK。如果只有SYN没有SYN-ACK可能是对端服务未监听、防火墙拦截或网络不通。如果SYN-ACK后没有ACK可能是客户端问题。看挥手检查连接结束是否有正常的四次挥手FIN - ACK - FIN - ACK。如果只有FIN没有ACK可能对端进程崩溃。如果大量连接停留在FIN-WAIT-1或FIN-WAIT-2需要结合应用日志分析。看传输在ESTABLISHED状态期间观察是否有大量的Retransmission和Dup ACK。计算重传比例。结合应用层的响应时间判断是网络问题还是服务端处理能力问题。跟序列号顺着Seq和Ack号看数据流是否连续。大的跳跃或长时间停顿都可能是问题点。6. 高级话题握手与挥手中的边界情况与内核参数在实际生产环境中纯粹的“三次握手”和“四次挥手”只是理想模型。操作系统内核提供了大量参数来调节TCP的行为以适应不同的网络环境和性能需求。握手阶段的优化SYN Flood攻击与防御攻击者伪造大量SYN包发送给服务器服务器回复SYN-ACK后进入SYN-RCVD状态并分配资源但攻击者不回复ACK导致服务器资源耗尽。防御机制包括SYN Cookiesnet.ipv4.tcp_syncookies 1。在SYN队列满时服务器在SYN-ACK中编码一个Cookie由Seq计算得出而不分配完整资源。只有收到携带正确Cookie的ACK时才分配资源。这可以有效抵御泛洪攻击。调整队列长度net.ipv4.tcp_max_syn_backlog控制半连接队列SYN-RCVD状态大小net.core.somaxconn控制全连接队列ESTABLISHED状态等待accept()大小。在高并发场景下需要调大。TCP Fast Open (TFO)允许在第一次SYN包中就携带数据减少一次RTT往返延迟特别适合HTTP等短连接场景。需要在客户端和服务端同时启用net.ipv4.tcp_fastopen。挥手阶段的调优TIME-WAIT的回收net.ipv4.tcp_tw_reuse 1允许将TIME-WAIT套接字重新用于新的TCP连接作为客户端时。这比tcp_tw_recycle更安全。net.ipv4.tcp_max_tw_buckets系统允许存在的TIME-WAIT套接字的最大数量。超过此数量时新的TIME-WAIT会被直接销毁并打印警告。这是一个“兜底”参数不能作为主要优化手段。FIN-WAIT-2 超时如果主动关闭方在FIN-WAIT-2状态一直收不到对端的FIN会一直等待。通过net.ipv4.tcp_fin_timeout可以设置这个超时时间默认60秒超时后连接被强制关闭。孤儿连接与 keepalive对于长时间空闲的连接中间的网络设备如NAT防火墙可能会因为超时而清除会话表导致连接“假死”。TCP的Keepalive机制net.ipv4.tcp_keepalive_time,net.ipv4.tcp_keepalive_intvl,net.ipv4.tcp_keepalive_probes可以定期发送探测报文来维持连接。但更佳实践是在应用层设计心跳协议。一个常见的坑tcp_tw_recycle与 NAT 的冲突在老版本的Linux优化指南中常会看到设置net.ipv4.tcp_tw_recycle 1来快速回收TIME-WAIT。但这个选项会启用一种称为“per-host”的PAWSProtection Against Wrapped Sequence numbers机制它会对每个来源IP的时间戳进行缓存和校验。在客户端位于NAT网关后如公司内网、云服务器的场景下同一个NAT后的多台机器对外呈现同一个源IP但它们各自的时间戳可能不同。这会导致服务器端因为时间戳混乱而丢弃某些连接请求造成部分用户连接失败。因此在存在NAT的网络环境中强烈建议不要开启tcp_tw_recycle。在Linux内核4.12之后这个参数已经被移除了。7. 编程中的注意事项Socket API 与状态对应作为开发者我们通过Socket API来操作TCP连接每一个API调用都对应着TCP状态机的变迁。connect()客户端调用触发三次握手。调用后本地进入SYN-SENT成功则进入ESTABLISHED。listen()服务器调用进入LISTEN状态。accept()从全连接队列中取出一个已完成的连接状态已是ESTABLISHED返回一个新的Socket文件描述符。close()/shutdown()close()将Socket的引用计数减1。当引用计数为0时会触发TCP的关闭流程发送FIN。如果只是调用close()连接进入FIN-WAIT-1这是正常的四次挥手起点。shutdown(int how)提供了更精细的控制。SHUT_RD关闭读通道。对端发来数据会被确认但丢弃本地不能再读。SHUT_WR关闭写通道。这是触发发送FIN报文的常用方式本地发送缓冲区数据发出后会紧跟一个FIN。调用后本地状态通常进入FIN-WAIT-1。SHUT_RDWR等同于先调用SHUT_RD再调用SHUT_WR。阻塞与非阻塞下的差异在阻塞模式下connect()、accept()、read()、write()等调用可能会一直等待直到完成或出错。在非阻塞模式下这些调用可能立即返回EINPROGRESS、EWOULDBLOCK等错误需要配合I/O多路复用如select、poll、epoll来使用。特别是在非阻塞Socket上调用close()可能不会等待数据发送完毕或四次挥手完成可能导致数据丢失或连接未正常关闭。更安全的做法是先调用shutdown(SHUT_WR)等待对端确认再调用close()。理解TCP状态与API的对应关系能帮助你在写网络程序时更准确地处理连接生命周期避免出现连接泄漏、资源未释放等棘手问题。下次当你看到CLOSE_WAIT堆积时你应该立刻想到是不是某个地方拿到了Socket却没有正确地调用close()。

相关新闻