尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

TCP三次握手与四次挥手:从协议原理到网络排查实战

发布时间:2026/9/24 19:26:32

资讯中心
01
ARTICLE

TCP三次握手与四次挥手:从协议原理到网络排查实战

TCP三次握手与四次挥手:从协议原理到网络排查实战
搞网络排查的时间长了你会发现一个特别有意思的现象很多人能把TCP三次握手的口号背得滚瓜烂熟但真正出了问题比如线上服务突然大量出现connection reset by peer或者端口明明没被占用却提示bind: only one usage of each socket address又或者用Wireshark抓了一堆包根本不知道从哪里看起——这时候能把三次握手、四次挥手真正用起来的人反而没几个。这篇文章我想换个讲法不把三次握手和四次挥手当纯理论来背而是结合我这些年实际操作中的经验把这两件事拆开揉碎了讲清楚。包括每个标志位背后的含义、每一步如果失败会怎样、用Wireshark怎么抓怎么筛、以及开发中常见的TCP异常到底怎么排查。不管你是刚入门的学生还是已经在写socket服务但平时没系统梳理过TCP细节的开发者这篇都值得花点时间看看。1. 三次握手不只是SYN、SYNACK、ACK1.1 三个报文段的真实样貌先把最经典的流程摆出来。客户端要和服务端建立TCP连接双方交换三个报文段客户端 服务端 | SYN (seqx) | |-----------------------------| | SYNACK (seqy, ackx1) | |-----------------------------| | ACK (seqx1, acky1) | |-----------------------------|第一步客户端发送一个SYN报文携带一个初始序列号seqx。这个序列号不是随便来的它跟TCP的可靠性机制直接挂钩——后续每个字节的数据都会基于这个初始序列号进行编号。第二步服务端收到SYN之后如果同意建立连接会回复一个SYNACK报文。这个报文同时做了两件事一是确认收到了客户端的SYN所以ackx1二是自己也发起一个同步请求所以携带自己的初始序列号seqy。第三步客户端再回一个ACK确认收到服务端的SYNACKacky1。到这里双方都确认了彼此的收发能力正常连接正式建立。注意一个细节第三步的ACK报文本身不携带应用层数据但它已经可以携带数据了。如果发送端着急完全可以在第三次握手的同时把第一个数据包一起发出去。这也解释了为什么实际抓包中有些连接的第四、第五个包就已经是应用层数据了。1.2 为什么必须是三次两次行不行经常有人问为什么不能两次握手就建立连接我一般用“双方都需要确认对方收发能力正常”来解释。第一次握手后服务端知道客户端的发送能力正常但客户端不知道服务端的情况。第二次握手后客户端知道服务端的收发能力都正常了同时自己的发送能力也再次被确认。但服务端此时还不知道客户端的接收能力是否正常因为SYNACK发出去之后客户端还没回应。如果只握手两次就会出现一个经典的坑服务端收到了一个历史遗留的旧SYN立刻分配资源并建立连接但客户端其实根本没有发新的连接请求。这个旧SYN可能是网络中的延迟重传报文端口恰好对上了服务端就白白浪费了资源还可能造成数据错乱。三次握手的关键作用就在这里客户端收到SYNACK后会检查这个确认号是否和自己发出的序列号匹配不匹配就直接发RST终止。这样服务端就能及时释放掉误建连接不会长期被无效连接占着资源。1.3 握手背后的两个队列半连接队列和全连接队列三次握手不是简单的“一发一收”就完事服务端内核里还藏着两个队列。客户端SYN到达后服务端内核会创建一个SYN_RECV状态的连接放进半连接队列也叫SYN队列然后回复SYNACK。此时连接还没有真正建立服务端还不能给应用层交付这个连接。当第三次握手的ACK到达服务端把连接从半连接队列移入全连接队列也叫Accept队列状态变成ESTABLISHED。应用层调用accept()才能从这个队列里取出连接。这两个队列都有长度限制。如果半连接队列满了新来的SYN会被直接丢弃客户端就会一直重传SYN直到超时。如果全连接队列满了即使握手完成了应用层也来不及接受内核可能丢弃ACK或发送RST表现就是客户端明明完成了三次握手但发数据时立刻报错。我调过不止一次这种接口协议栈看起来正常但应用层并发一高就出现大量Connection reset by peer查到最后基本都是全连接队列被打满。Linux下可以用ss -lnt看Send-Q如果全连接队列的当前使用数经常顶着上限说明应用层消费连接的速度跟不上内核接受连接的速度这时候要做的不是调大队列而是优化业务代码里accept()之后的处理逻辑。2. 四次挥手断开一个连接为什么更复杂2.1 挥手流程逐段看TCP连接是全双工的数据可以同时在两个方向上传送所以断开的时候两个方向要分别关闭这就产生了四次挥手。主动关闭方 被动关闭方 | FIN (sequ) | |-----------------------------| | ACK (acku1) | |-----------------------------| | FIN (seqv) | |-----------------------------| | ACK (ackv1) | |-----------------------------|第一步主动关闭方发送FIN表示“我的数据发完了我不会再往这个方向发数据了”。第二步被动关闭方回复ACK表示“收到你的FIN”。但此时被动关闭方可能还有数据没发完所以应用层不会马上关闭连接处于CLOSE_WAIT状态。第三步被动关闭方把剩余数据发完后也发送FIN表示“我这个方向也关闭了”。第四步主动关闭方回复ACK确认收到对方的FIN。主动关闭方进入TIME_WAIT状态等待2MSL最大报文段生存时间后连接才真正消失。看起来很简单但实际工程里最容易出问题的恰恰是第二步和第四步。2.2 主动关闭方为什么要等2MSLTIME_WAIT这玩意儿很多人只是知道“要等”但不知道为什么等。两个原因一是保证最后一次ACK能送到对方。如果这个ACK丢了被动关闭方超时会重发FIN主动关闭方需要能再次回复ACK。如果主动关闭方直接关闭连接收到重发的FIN后无从回应对方会一直停在LAST_ACK状态浪费资源。二是让网络上残留的旧数据包自然消亡。如果连接马上关闭端口立刻被复用新连接可能收到旧连接在网络中滞留的数据包造成数据混乱。等待2MSL能确保一个报文在网络中的最大存活时间内不会再出现。2MSL指的是报文段在两个方向各经过一次最大生命期的总和。Linux的默认值是60秒也就是一个MSL是30秒2MSL就是60秒。你可以在配置里看到相关的sysctl参数不过一般不需要动它。实际开发中TIME_WAIT多会带来一个直接问题主动关闭方大量端口被占用。高并发的短连接场景下服务端主动断开连接后端口要等60秒才能复用新连接数量一多就可能出现Cannot assign requested address。解决思路有两个层面一个是开启net.ipv4.tcp_tw_reuse让内核在安全条件下复用TIME_WAIT连接另一个更推荐的是改造业务逻辑尽量用长连接、连接池减少频繁主动断开。2.3 CLOSE_WAIT的产生与危害如果说TIME_WAIT是主动关闭方的烦恼那CLOSE_WAIT就是被动关闭方的典型问题。收到对方的FIN后如果应用层迟迟不调用close()去关闭自己的socket连接就会一直停在CLOSE_WAIT。注意ACK是内核帮你回的应用层可能根本不知道对方已经关闭了所以应用层需要主动感知到读事件返回0然后调用close()才能继续走完挥手流程。我排查过的一个案例某服务的连接数在监控里持续上涨最后稳定在一个很高的数值不再下降。用ss -tan一看密密麻麻全是CLOSE_WAIT。追代码发现业务线程在读取数据时遇到EOF没有正确处理直接跳出了循环但忘了释放socket资源。这类bug在写业务代码时特别容易漏尤其当连接读取逻辑封装在深层代码里时异常分支很难被测试覆盖到。排查CLOSE_WAIT的通用思路很直接先确认是哪个进程的连接异常再定位它收到了FIN之后为什么不关闭本地socket。多数情况不是网络问题而是代码问题。还有一个高频场景值得注意被动关闭方在收到FIN后如果还有数据要发送它会正常发送之后再发FIN。这段时间虽然连接没有完全关闭但主动关闭方已经不能再发新数据了。如果被动关闭方因为业务bug一直不发送FIN主动关闭方会一直等下去最终表现为连接“半死不活”。这类问题单纯抓包能看到完整的两次挥手机器上却是大量FIN_WAIT_2状态的连接。遇到这种最稳妥的办法是结合应用日志和tcpdump一起分析。3. 在Wireshark里把三次握手和四次挥手抓出来3.1 最简单的出手姿势很多人在Wireshark里看TCP包看到的是一大堆灰色、黑色、绿色的行完全不知道哪个包属于哪一次握手。其实筛选很简单因为三次握手和四次挥手都是靠TCP头里的标志位来区分的。抓到包之后如果只想看特定主机的连接先在过滤器里输入ip.addr 192.168.1.100 tcp.port 8080然后在显示过滤器里用标志位筛选只显示SYN包包含SYNACKtcp.flags.syn 1只显示纯SYN包不包含ACK的SYNtcp.flags.syn 1 tcp.flags.ack 0显示SYNACK包tcp.flags.syn 1 tcp.flags.ack 1只显示FIN包tcp.flags.fin 1只显示RST包tcp.flags.reset 1只看三次握手tcp.flags.syn 1 || (tcp.flags.ack 1 tcp.seq_raw 1)最后这个筛选其实不太严谨我自己更常用的方式是先找到连接的第一个SYN包然后右键菜单选Follow - TCP Stream在弹出的流内容里直接看开头那几个带[SYN]、[SYN, ACK]、[ACK]标注的包再结合序号和确认号判断。3.2 实例解读一次完整的建连和断连我用一个典型的本地连接来描述抓包后你会看到什么。假设客户端IP是192.168.1.10服务端IP是192.168.1.20服务端口8080No. Time Source Destination Protocol Length Info 1 0.000000 192.168.1.10 192.168.1.20 TCP 74 53540 - 8080 [SYN] Seq0 Win64240 Len0 MSS1460 2 0.000123 192.168.1.20 192.168.1.10 TCP 74 8080 - 53540 [SYN, ACK] Seq0 Ack1 Win65535 Len0 MSS1460 3 0.000221 192.168.1.10 192.168.1.20 TCP 66 53540 - 8080 [ACK] Seq1 Ack1 Win64240 Len0第一行Seq0是相对序列号Wireshark默认显示相对值。第三行的Seq1和Ack1分别是第一行Seq0加1、第二行Seq0加1的结果。也就是说握手包交换的序列号确认逻辑用相对值看会更加直观。如果这个连接随后正常关闭你会在数据交换结束后看到No. Time Source Destination Protocol Length Info 20 0.050312 192.168.1.10 192.168.1.20 TCP 66 53540 - 8080 [FIN, ACK] Seq120 Ack80 Len0 21 0.050398 192.168.1.20 192.168.1.10 TCP 66 8080 - 53540 [ACK] Seq80 Ack121 Len0 22 0.061203 192.168.1.20 192.168.1.10 TCP 66 8080 - 53540 [FIN, ACK] Seq80 Ack121 Len0 23 0.061312 192.168.1.10 192.168.1.20 TCP 66 53540 - 8080 [ACK] Seq121 Ack81 Len0第20个包是主动关闭方发的FINACK。第21行是被动方的确认。第22行是被动方处理完剩余数据后发的FINACK。第23行是主动方最后的确认。如果你看到第21行之后伴随着RST而不是FIN说明被动方异常终止了连接。常用的判断是正常挥手一定是FIN开头不正常的是RST开头。但要注意很多TCP实现允许在FIN上携带ACK标志因为关闭前的最后一个数据包往往同时需要确认之前的序号所以[FIN, ACK]是正常现象。3.3 抓包时容易踩的坑第一个坑是没抓到第一次握手。如果你打开Wireshark的时机太晚很可能只看到SYNACK而看不到最开始的SYN。解决办法是在捕获选项里设置Limit each packet to 64 bytes之类的参数没必要过滤范围反而要放大别只盯着自己应用的端口先把整个网卡的流量抓下来再慢慢过滤。第二个坑是抓包工具本身把校验和算错了。在Windows或者虚拟机上网卡可能开启了校验和卸载功能导致Wireshark显示一堆[Checksum Offload]的警告。这通常不影响分析但如果你在做底层协议开发最好在虚拟机设置里关闭TCP校验和卸载否则排查起来非常折磨人。第三个坑是机器上回环流量的抓取。自己写socket程序测试时抓本地回环接口的包很多新手发现抓不到。这主要是因为Wireshark默认可能选择了错误的接口或者系统对回环流量的处理路径特殊。在Linux上回环接口名称通常是lo在Windows上名字可能叫Npcap Loopback Adapter选对接口就行。4. 实战中的TCP异常与排查套路4.1 高频错误对照速查表排查TCP问题最怕看到一个错误就去网上搜搜完也不知道原理。我整理了一份自己在工作中反复用到的对照表先看懂错误属于哪个阶段再对症下药。报错信息常见阶段可能原因优先排查方向bind: only one usage of each socket address启动监听端口被占用netstat -ano/ss -lntp看谁占了端口Connection refused建立连接服务端未监听或防火墙丢包确认端口监听状态、本机防火墙Connection timed out建立连接SYN被丢弃或网络不通抓包看SYN有没有发出、有没有回包Connection reset by peer数据传输对端发送了RST抓包定位RST来源重点看对端堆积和异常关闭Cannot assign requested address发起连接本地临时端口耗尽ss -s看TIME_WAIT数量调优客户端连接复用Broken pipe发送数据对端已关闭后继续写代码层处理写错误及时关闭socketNo route to host建立连接路由不通或对端主机不可达ping网关、traceroute排查链路有一类错误特别常见就是报错信息里带端口号比如error: listen tcp 127.0.0.1:11434: bind: only one usage of each socket address。看到这个说明你的程序想在127.0.0.1:11434这个地址上监听但内核说这个端口已经被占用了。排查这类问题Linux上先执行ss -lntp | grep 11434Windows上执行netstat -ano | findstr 11434找到占用端口的进程PID后再查是哪个程序ps -ef | grep PID很多时候是上一个服务没退出或者另一个进程占了同一个端口。也有一种情况是端口被短暂留在了TIME_WAIT状态不过TIME_WAIT一般不影响新进程绑定监听端口除非你设置了SO_REUSEADDR且没有正确处理重启场景。4.2 从“对端RST”反推连接问题Connection reset by peer是后端开发里最高频的TCP报错之一。它到底什么意思简单说就是对方给你回了RST包直接把连接掐断了。RST是怎么产生的最常见的几种第一种对端的socket已经关闭但你的数据还在路上。比如服务端close()之后内核收到了一个迟到的数据包就会回RST。第二种对端的接收缓冲区已满内核拒绝接收新数据也可能会回RST。第三种对端收到一个序列号完全不在接收窗口内的包比如旧连接的数据窜到新连接上内核直接丢RST。第四种你向一个没有监听的端口发连接请求服务端内核会回RST而不是FIN携带着Connection refused的语义。排查的时候别光看自己这一端。直接在服务端抓包tcpdump -i eth0 -nn tcp port 8080 -w /tmp/tcp8080.pcap然后用Wireshark打开这个文件找到报错时间点附近的包看RST是从哪里发出来的。如果RST来自对端且在这之前没有任何FIN说明对端应用层有逻辑直接放弃了连接比如调用SO_LINGER设置为0再close()或者进程被强杀。如果RST来自你自己的主机那问题就出在你的内核或应用层。4.3 编程中常见的连接状态与处理很多人写服务端代码时对连接状态的变化没有感知。Java NIO、Python asyncio、C socket底层都是同一套TCP状态机。客户端主动断开后服务端第一次read()会返回0或者异常这是正常的EOF。熟悉这个特征后请确保你的代码在EOF分支一定执行资源清理否则CLOSE_WAIT会不断累积。还有一个高频隐患是半关闭。TCP支持shutdown(fd, SHUT_WR)这样的操作我关闭发送方向但保留接收方向。这在一些自定义协议里有用比如客户端发完请求后主动关闭发送方向服务端读完请求就知道数据完了回完响应再关闭。如果不理解半关闭遇到对方只发FIN不发数据的情况容易手足无措。处理RST的方式也值得注意。默认情况下进程收到RST后如果继续调send()会得到EPIPE/SIGPIPE。在Unix/Linux下SIGPIPE默认会终止进程很多服务器莫名挂掉就是这个原因。生产代码里最好显式忽略SIGPIPE统一走错误处理逻辑signal(SIGPIPE, SIG_IGN);这样错误就能以返回值的形式暴露出来而不是让进程直接死掉。5. TCP在开发中的落地细节5.1 一个最小可用的服务端骨架手工实现一个能够正确走完三次握手和四次挥手的服务端并不需要太多代码但要注意几个关键点。下面是一个C语言风格的socket服务端骨架省略了错误处理细节重点看调用顺序int listen_fd socket(AF_INET, SOCK_STREAM, 0); int opt 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt)); struct sockaddr_in addr; addr.sin_family AF_INET; addr.sin_addr.s_addr htonl(INADDR_ANY); addr.sin_port htons(8080); bind(listen_fd, (struct sockaddr*)addr, sizeof(addr)); listen(listen_fd, 128); while (1) { int conn_fd accept(listen_fd, NULL, NULL); // 每来一个连接建议丢给线程/协程处理 }listen()的第二个参数是已连接队列长度也就是前面说的全连接队列上限。这个值不是越大越好太大会导致应用层消费不及时连接长时间堆积在内核里。通常根据并发模型来定线程池模型下128左右够用事件驱动模型下可以适当调大。客户端那边核心流程是int fd socket(AF_INET, SOCK_STREAM, 0); connect(fd, (struct sockaddr*)server_addr, sizeof(server_addr)); send(fd, data, len, 0); recv(fd, buf, sizeof(buf), 0); close(fd);connect()返回成功说明三次握手已经完成但TCP的细节比这个更丰富。比如connect()超时时间通常由内核参数tcp_syn_retries决定默认大概在127秒左右。想在业务层控制超时得用非阻塞socket配合select/poll/epoll来自己做超时控制这也是为什么很多高级网络库都封装了一套自己的连接超时逻辑。5.2 长连接、超时与保活连接建立之后真正影响线上稳定性的往往是连接管理和保活策略。关于连接池。短连接频繁建立和断开会让服务端累积大量TIME_WAIT状态也增加握手开销。生产中强烈建议使用连接池。连接池的要点不是简单的“复用连接”而是能正确处理连接断开后的重建否则复用了一个被服务端关闭的连接第一次发送就报Broken pipe。关于TCP KeepAlive。TCP自带的保活机制默认是关闭的即使开启默认也需要2小时无数据才探测。对大多数应用来说这个时间太长所以应用层通常需要自己的心跳。心跳包的本质是“应用层的保活包”它同时也检测应用逻辑是否正常而不只是网络通不通。关于读超时和写超时。服务端处理慢客户端时读超时是很关键的。如果客户端建立了连接但不发数据服务端可能被大量“半开连接”占用资源。设置合理的读超时超时就直接关闭能避免很多资源泄漏问题。设置方式通常是先获取socket的SO_RCVTIMEO、SO_SNDTIMEO选项或者在使用非阻塞模型时由事件循环来管理超时。关于粘包和拆包。TCP是字节流协议没有消息边界。三次握手只是让连接建立它不帮你分隔数据。所以应用层必须定义消息格式最简单的例子是每个消息前4字节存长度。这个和四次挥手其实也有关系如果你在消息还没完全读完时就关闭连接对方可能只收到半条消息处理逻辑如果没做好容错就会出现各种诡异的业务异常。5.3 握手/挥手与丢包的组合场景有些网络问题只有在“握手阶段丢包”和“挥手阶段丢包”时才会显现。比如客户端发SYN后如果服务端的SYNACK在网络中丢了客户端会超时重传SYN这很正常但如果你在抓包里看到多个SYN连续发出来说明网络丢包率已经开始影响端到端通信了。再看挥手阶段主动关闭方的最后一个ACK如果丢了被动方会重发FIN此时主动方因为还在TIME_WAIT可以正常回复ACK。如果主动方已经退出TIME_WAIT且端口被新连接复用被动方收到新的SYN后会根据TCP协议重置这个连接给客户端一个RST。这类问题在负载均衡后面特别常见因为LB和后端服务器之间的连接断开时机不由你完全控制必要时需要在LB和后端同时开放tcp_tw_reuse和tcp_tw_recycle但tcp_tw_recycle在NAT环境下会引发更隐蔽的问题现在内核版本很多已经移除了这个选项更推荐的还是从连接复用模型上解决。还有一个值得单独说的现象叫“同时关闭”。双方同时发送FIN连接会进入CLOSING状态然后各自回ACK最后进入TIME_WAIT。这种场景在实际网络里不常见但压力测试或者某些协议设计中会发生。不了解这个状态的人看到抓包里的CLOSING会以为出了什么严重错误其实这是协议正常工作的一部分。6. 最后分享一点排查经验如果你只从这篇文章里带走一样东西我想是这句话TCP的握手和挥手不是为了让你背八股文而是给你提供一张“排查地图”。线上出现连接问题第一件事不是重启服务而是先想清楚这个问题发生在建立连接的哪一步、数据传输的哪一步、还是断开连接的哪一步。然后顺着这个思路去抓包、看状态、查代码。抓包的时候关注的是标志位和序列号看状态的时候关注的是SYN_RECV、ESTABLISHED、CLOSE_WAIT、TIME_WAIT这几种典型状态查代码的时候重点看数据读完之后有没有正确处理EOF、出错之后有没有可靠地关闭socket。我实际踩过最深刻的一个坑是服务端代码里忘记处理半关闭。客户端调用shutdown(SHUT_WR)发完数据服务端却一直在傻等新数据两边互相等了几分钟直到超时才断开。如果当时我熟悉TCP半关闭语义早就能从报文特征里判断出问题根本不用翻半天的业务日志。技术的价值从来不是“知道”而是“用上”。多抓几次包多把异常状态和协议状态机对应起来比背一百遍流程图都有用。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。