提到TCP三次握手和四次挥手,我相信很多读者第一反应是:流程图我早背下来了。真正去了生产环境,服务器上积了一堆TIME_WAIT,客户端莫名其妙卡在SYN_SENT,或者服务端一直CLOSE_WAIT不掉——这时候再回头看教科书上的状态迁移图,才会发现自己其实根本没吃透这套机制。这篇文章不打算只是把SYN、ACK、FIN再念一遍,而是从“为什么非要这么设计”的角度,把报文细节、状态切换、抓包观察和常见故障串起来讲清楚。适合这几类人读:正在准备网络面试的开发者、被线上连接问题折腾过的运维或后端工程师、以及学完TCP理论但没实际抓过包的学生。我会尽量用人话解释里面的数字为什么是这个值、状态为什么停在那里,看完之后你可以直接拿抓包工具验证,也能拿这套思路去排查真实的连接异常。1. 为什么TCP连接需要一套“握手”“挥手”仪式:先理解它在解决什么问题1.1 TCP连接的本质:通信前先建立“字节序契约”TCP和UDP最大的区别,不是“可靠”两个字这么简单。UDP发一个数据报出去,接收方收没收到、顺序乱没乱,发送方并不在意。TCP要做到可靠传输,就必须让双方维护一套共同的编号体系:我发了多少字节、你收到了多少字节、下一个该发哪个字节,这些信息在通信之前得先对齐。所以TCP连接的本质,不是一条真的物理链路,而是通信双方在各自内存里建立的传输控制块(TCB)。里面记录了对方的IP、端口、当前收发序号、窗口大小、拥塞状态等一堆参数。三次握手做的事情,本质上就是让两端把这份状态同步好,尤其是初始序号(ISN)。你可以把两个进程想象成准备对暗号的两个人。双方都得知道对方从哪个编号开始数数,否则后面“确认收到第几个字节”就无从谈起。握手不是礼貌,是建立协同工作的基础。1.2 三次握手同时完成了哪三件事一次完整的TCP连接建立,必须解决三个问题:确认双方收发能力正常。任何一方发出的报文,另一方都能正确收到并回复,这说明链路可用。同步初始序号。两端各自告诉对方“我的第一个字节从序号x开始”,同时确认对方发来的初始序号。协商连接参数。比如最大报文段长度MSS、窗口缩放因子、是否支持SACK,这些会在握手报文里捎带协商。很多人以为握手就是交换三个包,其实这三个包里还藏了客户端和服务端各自的参数声明。抓包时你去看SYN包里的TCP Options,里面有MSS、窗口缩放、时间戳等字段,这些都是在这三步里搞定。正因为要做的事情不止“确认存在”,所以两步不够,三步正好。2. 三次握手逐段拆包:SYN、SYN-ACK、ACK 里每个数字都代表什么2.1 第一次握手:客户端发出SYN,亮出自己的初始序号客户端从CLOSED状态主动调用connect,发出一个SYN报文。这个报文的关键字段是:SYN标志位置1序列号seq = x,这里的x就是客户端随机生成的初始序号ISN不携带应用数据控制位SYN会占用一个序号为什么初始序号要随机,而不是固定从0开始?主要有两个原因。第一,防止上一次连接遗留的旧报文被误认为是当前连接的报文。如果序号固定,新旧连接的编号可能完全重合,旧数据就会“串门”。第二,防止攻击者猜测序号后伪造报文劫持连接。随机ISN是TCP安全的基础之一。这里有个初学者常忽略的点:SYN报文虽然不携带数据,但它会占用一个序号单位。所以客户端发出seq=x之后,下一次再发数据时seq会变成x+1。这个“控制报文也占序号”的规则,后面理解ACK值全靠它。发送SYN后,客户端进入SYN_SENT状态,等待服务端回复。2.2 第二次握手:服务端返回SYN-ACK,一次回应两件事服务端本来处于LISTEN状态,收到SYN后,如果愿意建立连接,会回复一个SYN-ACK报文。里面同时置两个标志位:SYN=1和ACK=1。语义是:SYN=1:服务端也亮出自己的初始序号yACK=1:确认收到客户端的SYNseq = y(服务端的ISN)ack = x+1这里ack=x+1是很多新手最容易懵的地方。为什么不是x?因为客户端发来的SYN报文虽然不承载业务数据,但它本身占用一个序号。服务端确认收到的是“序号为x的控制报文”,所以期待客户端下一个发出的字节序号是x+1。这个ack的完整含义是:“我已经收到你序号x及以前的所有字节,下一个请从x+1开始发。”服务端发出SYN-ACK后进入SYN_RECEIVED状态,此时连接还是半连接状态——服务端知道客户端想连,但还没确认客户端收到了自己的SYN。这个半连接会占用服务端内存资源,也是后面讲SYN Flood攻击和半连接队列的基础。2.3 第三次握手:客户端发送ACK,连接正式建立客户端收到SYN-ACK后,需要再回一个ACK报文:ACK=1seq = x+1ack = y+1这里ack=y+1同样是因为服务端的SYN占了一个序号。客户端的意思是:你的初始序号y我收到了,下一个请从y+1开始发。这个第三次握手有两个细节值得注意。第一,从第三包开始,客户端就可以携带真正的应用数据了。因为前两次交互已经让客户端确认服务端存在且收发正常,所以客户端在发送ACK时能捎带第一个HTTP请求等数据。但服务端在收到这个ACK之前,不会把连接交给应用层,因为应用层要等连接真正建立。第二,服务端收到第三个ACK后,状态从SYN_RECEIVED变为ESTABLISHED,连接才算完全建立。客户端其实在发出ACK后就已经进入ESTABLISHED状态。两边状态都到位,才开始传数据。2.4 为什么必须是三次:用“迟到的SYN”验证两次握手的问题这个问题几乎是面试必问,但真能讲清楚的人不多。我们设想一种场景:客户端第一次发了一个SYN,因为网络拥堵迟迟没到服务端。客户端等超时后重发了SYN,这次成功完成握手,数据传输完毕后关闭连接。可就在连接关闭之后,那个最初拥堵的旧SYN突然到达了服务端。如果只有两次握手,服务端收到这个迟到的SYN,会认为自己建立了一个新连接,于是回复SYN-ACK,然后服务端单方面把连接标记为ESTABLISHED,分配相关资源。但客户端根本没有发起这条新连接,收到这个莫名其妙的SYN-ACK后直接回一个RST重置。服务端直到收到RST才算明白,期间白白占用资源,可能还会干扰后续正常的连接请求。如果是三次握手,情况就不一样。客户端在第三次ACK里会带上自己本次连接的上下文。如果客户端根本没发起连接,或者这个SYN属于旧连接,客户端要么不回ACK,要么直接回RST,服务端就不会错误建立连接。更深层的原因可以这样理解:**两次握手只能保证服务端确认了客户端的发送能力,无法让客户端确认服务端收到了自己的确认信息。**三次握手通过“客户端主动再发一个ACK”这个动作,把“我知道你知道我知道”这个链条闭环,同时也给客户端一个机会,用RST主动终结无效的握手过程。顺带提一句,三次握手也决定了服务端资源分配的时机。服务端在收到第一个SYN后就要建立半连接、分配序号空间,如果迟迟收不到第三个ACK,半连接会一直挂着。这也是SYN Flood攻击的原理——攻击者发送海量SYN但不回ACK,把服务端半连接队列打满,正常的连接请求就无法处理。3. 四次挥手逐段拆包:全双工通道为什么各自说再见3.1 全双工决定的结论:两个方向需要独立关闭TCP连接是全双工的,也就是说客户端和服务端可以同时发送和接收数据。这和打电话不一样,更像一条双向车道,A方向的车流和B方向的车流互不干扰。既然两个方向是独立的,关闭时就不能指望一个FIN包同时关掉两条车道。假设客户端说“我发完了”,但服务端可能还有一车数据没发完。如果服务端收到FIN就把整个连接断开,那剩余数据就只能丢在路上。所以TCP的关闭逻辑是:**每一方主动关闭时,仅仅表示“我这边的数据发完了”,对方还能继续向我发数据。**只有双方都声明“我这边没有数据了”,连接才真正关闭。这就是为什么要四次挥手。3.2 挥手四步的报文细节和状态轨迹假设客户端主动发起关闭,完整过程如下:第一步:客户端发送FIN报文FIN=1seq = u(u是客户端最后一个已发送数据的序号加1)客户端进入FIN_WAIT_1状态FIN和SYN一样,虽然不携带数据,但会占用一个序号。客户端发完FIN后,表示“我这边没有更多数据要发了”。第二步:服务端回复ACKACK=1ack = u+1seq = v(服务端当前发送序号)服务端收到FIN后回复确认,进入CLOSE_WAIT状态。客户端收到这个ACK后,从FIN_WAIT_1进入FIN_WAIT_2。这个阶段非常关键:**客户端已经关闭了自己的发送方向,但接收方向仍然是打开的。**服务端如果还有数据要发,可以继续发,客户端照样能收。很多应用层协议在做优雅关闭时就会利用这个特性,比如HTTP服务端接到客户端的结束信号后,先把剩余响应发完,再调用close。第三步:服务端发送自己的FIN报文服务端把剩余数据全部发完之后,发送FIN:FIN=1seq = wack = u+1(这里的ack还是对着客户端那个FIN)服务端从CLOSE_WAIT进入LAST_ACK状态,表示“我等客户端确认我的FIN”。有很多连接会长时间停在CLOSE_WAIT,原因就是应用层代码里没有正确关闭socket,只关了自己的写方向,没把整个socket close掉,服务端自然发不出FIN。第四步:客户端发送最后一个ACKACK=1seq = u+1ack = w+1客户端收到FIN后回复ACK,进入TIME_WAIT状态,等待2MSL后关闭。服务端收到这个ACK后进入CLOSED状态,彻底断开。仔细观察这四个步骤:前两步是客户端单向关闭,后两步是服务端单向关闭,合起来正好四个报文。如果双方同时调用close,流程还会稍微不同,可能出现CLOSING状态,但总的交互原则不变。3.3 TIME_WAIT的两大使命:为什么主动关闭方最后还要等两分钟TIME_WAIT是这个流程里最容易被误解、也最容易被生产环境“嫌弃”的状态。TIME_WAIT状态出现在主动关闭连接的一方,它要做两件事。第一,**确保最后一个ACK能到达对端。**最后一个ACK如果丢了,服务端会发现自己迟迟收不到确认,于是重新发送FIN。客户端进入TIME_WAIT后不会立即关闭,而是继续等待,如果收到重发的FIN,就重新发送ACK,直到双方都能顺利进入CLOSED。如果没有TIME_WAIT,客户端直接关闭,最后一个ACK丢了之后,服务端重发FIN无人应答,服务端就会悬挂在LAST_ACK状态,白白占用资源。第二,**防止旧连接的延迟报文干扰新连接。**假设客户端刚关闭一个连接,立刻用相同的IP和端口四元组建立新连接。如果没有等MSL时间,旧连接中还在网络里游荡的延迟数据包,可能被新连接接收,造成数据错乱。TIME_WAIT的2MSL等待时间,足以保证旧连接的报文在网络中全部消失。这里的MSL是“报文最大生存时间”,不同系统取值不同。老的BSD实现常见30秒或60秒,2MSL就是1到2分钟。网上很多资料写“TIME_WAIT持续2分钟”,那是拿历史默认值当标准了,实际现代Linux的默认值可能更短,而且部分参数可以调整。面试时最好不要把“2分钟”说死,而要说“2倍MSL”。在生产环境里,TIME_WAIT多通常会出现在主动关闭方,尤其是短连接密集的服务。比如一个服务作为客户端频繁请求外部API,每次请求都新建连接、请求完立即关闭,这个服务上就会堆积大量TIME_WAIT。这本身是协议正常运行的结果,真正的问题在于连接创建和销毁的频率太高。4. 用抓包和状态关联看TCP连接:不再死背流程4.1 TCP头部控制位的实际含义抓包工具里最常见的几个控制位是SYN、ACK、FIN、RST,它们的组合决定了报文的作用。标志位含义用途SYN同步序号连接建立时使用,占用一个序号ACK确认确认已收到对方数据,本身不占用序号FIN结束关闭本方发送方向,占用一个序号RST重置异常断开连接,不占用序号这里可以记住一个通用规则:**SYN和FIN作为控制报文,各占一个序号;ACK和RST不占序号。**所以你会发现三次握手里,客户端发出seq=x后,第三包的seq变成x+1,而不是x;服务端发出seq=y后,第三次握手的ack变成y+1,原因都是SYN占了那一个序号。4.2 用抓包工具看一次完整的HTTP请求在Linux服务器上,可以用tcpdump直接观察握手过程。假设客户端IP是192.0.2.100,服务端是198.51.100.10:tcpdump -i eth0 tcp port 80 -nn -S-S参数用来显示绝对序号,否则看到的都是相对序号。抓包结果大致长这样:1 0.000000 192.0.2.100:50001 > 198.51.100.10:80 Flags [S], seq 1000 2 0.000123 198.51.100.10:80 > 192.0.2.100:50001 Flags [S.], seq 2000, ack 1001 3 0.000135 192.0.2.100:50001 > 198.51.100.10:80 Flags [.], seq 1001, ack 2001第一行Flags [S]就是SYN;第二行Flags [S.]是SYN-ACK,那个点表示ACK位;第三行Flags [.]就是纯ACK。配合前面讲的报文细节,这一眼就能看明白。实际使用Wireshark时,如果不开相对序号显示,你看到的seq可能是0、1、1这样的数字,这是Wireshark为了可读性做的归一化处理,不是真实的ISN。分析时要注意区分。4.3 用ss和netstat跟踪状态机没用过状态机排查问题的人,很可能会觉得状态图只是考试工具。实际排障时,状态机往往比业务日志更能说明问题。在Linux上最常用的命令是ss -ant,比netstat快,信息也更全。输出里的State列就是连接当前所处的状态。比如:LISTEN:服务端正在监听端口SYN_SENT:客户端已发出SYN,等待SYN-ACKSYN_RECEIVED:服务端已发出SYN-ACK,等待客户端ACKESTABLISHED:连接建立,可以传输数据FIN_WAIT_1:主动关闭方已发FIN,等ACKFIN_WAIT_2:主动关闭方已等ACK,等对方FINCLOSE_WAIT:被动关闭方已收到FIN,等应用层关闭LAST_ACK:被动关闭方已发FIN,等对方ACKTIME_WAIT:主动关闭方已发最后ACK,等待2MSL我自己的排查习惯是:看到大量CLOSE_WAIT,先怀疑应用层没关socket;看到大量TIME_WAIT,先确认服务作为客户端是不是频繁短连接;看到FIN_WAIT_2堆积,多半是对方应用层卡住一直不关闭。5. 真实环境里握手挥手的故障形态与排查方法5.1 半连接队列和全连接队列溢出:握手中途“消失”的包服务端收到SYN之后,并不会立即把连接交给应用层。不同阶段,连接会放在不同的队列里:半连接队列(SYN队列):服务端收到SYN后、收到第三次ACK前,连接挂在半连接队列里。全连接队列(accept队列):三次握手完成、连接进入ESTABLISHED后,等待应用层调用accept取走的连接放在全连接队列里。如果半连接队列满了,服务端可能直接丢弃SYN,客户端就会一直卡在SYN_SENT,反复重传。如果全连接队列满了,可能出现客户端已经完成握手、连接也显示ESTABLISHED,但应用层一直accept不到,请求迟迟没有响应。排查这类问题,可以先看ss -lnt输出里的Send-Q列,它表示全连接队列的最大长度,Recv-Q列可以对比当前等待accept的连接数。还可以通过netstat -s看SYN丢弃计数是否在增长。调整的方向通常是增大系统listen backlog、程序里listen参数调大、或者调整内核net.core.somaxconn。但要先搞清楚是哪一个队列溢出,别一上来就盲目调参。5.2 客户端卡在SYN_SENT和服务端卡在SYN_RECEIVED的排查路径客户端长时间停在SYN_SENT,可能是以下原因:目标IP不可达或端口没监听。可以先ping目标IP,再用telnet或nc测试端口。中间链路防火墙丢弃SYN包。很多云安全组或本机iptables规则会静默丢弃SYN,而不是回RST。服务端半连接队列满,不处理新的SYN。服务端长时间停在SYN_RECEIVED,说明客户端发来的第三个ACK没有到达服务端。常见原因包括:网络回程丢包,ACK在传输中被丢弃。服务端半连接队列处理不过来,把ACK当成无效包丢了。客户端主动关闭了连接,不再发送ACK。排查时最有效的手段是同时在客户端和服务端抓包,对比同一时间窗口内两边看到的报文是否一致。如果客户端发送了ACK而服务端没收到,问题就在中间链路或服务端收包逻辑;如果客户端根本没发ACK,问题在客户端侧。5.3 RST替代优雅FIN:哪些场景会让“四次挥手”瞬间结束四次挥手是优雅关闭,双方都留足了时间把数据发完。但实际网络里大量连接是被RST硬生生断开的。RST报文的语义是“这个连接已经不存在,请立刻停止发送”。收到RST的一方会丢弃连接上的所有缓冲数据,不会继续确认。几种常见情况:向一个不存在的端口发送数据,对端内核会直接回RST。比如服务端根本没监听这个端口。程序设置了SO_LINGER并清空缓冲区,关闭socket时内核不发FIN而直接发RST。连接已经被对端关闭,本端还在继续发送数据,对端收到后会回RST。生产环境里如果把优雅关闭和异常重置混为一谈,排查起来会绕很大圈子。我的经验是:看到RST先不要急着查应用日志,先确认是哪一侧发的RST。收到RST的一方通常是受害者,发出RST的一方往往才是问题所在。5.4 大量TIME_WAIT的场景:内核参数不是万能的服务主动关闭大量短连接时,TIME_WAIT会积压。最典型的是高并发的代理程序,每处理一个请求就新建上游连接,请求完立即关闭,TIME_WAIT数量很快飙升。网上很多文章教你调低MSL时间、开tcp_tw_reuse这些参数。我的看法是:这些参数有各自严格的适用条件,盲目修改可能引发连接错乱。比如NAT后面有大量客户端时,开启某些复用功能可能造成连接串扰。真正稳妥的做法是调整应用层设计:用连接池复用连接、打开HTTP keep-alive、减少不必要的连接新建和销毁。TIME_WAIT本身是协议可靠性的代价,你把它强行消掉,代价只是从“看得见的TIME_WAIT”变成了“极难排查的偶发异常”。6. 常被问错也常被记错的那些点:用序号规则把流程串起来6.1 几个流传很广但不准确的说法“握手必须三次,挥手必须四次。”不完全对。常规标准流程确实是三个报文和四个报文,但TCP允许在第二次握手时把SYN和ACK合在一个报文里,挥手时也可以把第二次ACK和第三次FIN合并发送。某些实现中四次挥手会变成三次。同时关闭的场景下状态流转也不一样。关键是理解每一步的语义,而不是把数字背死。“发送FIN之后就什么都不能发了。”错。FIN只代表本方的发送方向结束,接收方向仍然开着。对方在收到FIN之后、发出自己FIN之前,依然可以合法地发送数据。这就是半关闭状态存在的意义。“TIME_WAIT等待时间是2分钟。”历史上BSD衍生系统的MSL常见是30秒或60秒,但现代系统可以配置,并不统一。与其背一个固定数字,不如记住2MSL这个原则,再回答“具体时间取决于系统实现”。6.2 抓住seq和ack的核心规律,全流程都不会乱如果你不想背任何具体序号,只要记住下面三条规律,整个握手挥手都能推出来:**发送方的seq = 自己已经发送的字节数 + 1。**即将发送的新数据从哪个序号开始。**接收方的ack = 自己期望收到的下一个字节序号。**等于对方已发送字节数 + 1。SYN和FIN各占一个序号,所以ack的数值都要在对方seq基础上加1。拿三次握手验证:客户端发seq=x的SYN,服务端回ack=x+1;服务端发seq=y的SYN,客户端最后回ack=y+1。四次挥手完全同理:客户端FIN的seq=u,服务端ACK的ack=u+1;服务端FIN的seq=v,客户端ACK的ack=v+1。这套规则不仅能解释正常流程,还能帮你快速判断抓包结果是否合理。只要报文的ack值和对方上一个seq对不上,十有八九是中间有重传或丢包,值得深入排查。这篇文章写到这里,核心内容已经说得差不多了。最后分享一点个人的实际体会:真正让你对握手挥手产生深刻理解的,不是反复背流程图,而是亲手抓一次包,或者在生产环境里盯着状态列表看一次故障。我自己第一次把CLOSE_WAIT和代码里忘记close()对应起来的时候,才意识到教科书上的状态机图原来这么有用。如果遇到连接问题不知道从哪入手,先把TCP状态和抓包结果摆出来,再层层缩小范围,比盯着应用日志猜原因有效得多。希望这篇不只是帮你应付面试,更能成为你下次排障时的手边参考。
提到TCP三次握手和四次挥手,我相信很多读者第一反应是:流程图我早背下来了。真正去了生产环境,服务器上积了一堆TIME_WAIT,客户端莫名其妙卡在SYN_SENT,或者服务端一直CLOSE_WAIT不掉——这时候再回头看教科书上的状态迁移图,才会发现自己其实根本没吃透这套机制。这篇文章不打算只是把SYN、ACK、FIN再念一遍,而是从“为什么非要这么设计”的角度,把报文细节、状态切换、抓包观察和常见故障串起来讲清楚。适合这几类人读:正在准备网络面试的开发者、被线上连接问题折腾过的运维或后端工程师、以及学完TCP理论但没实际抓过包的学生。我会尽量用人话解释里面的数字为什么是这个值、状态为什么停在那里,看完之后你可以直接拿抓包工具验证,也能拿这套思路去排查真实的连接异常。1. 为什么TCP连接需要一套“握手”“挥手”仪式:先理解它在解决什么问题1.1 TCP连接的本质:通信前先建立“字节序契约”TCP和UDP最大的区别,不是“可靠”两个字这么简单。UDP发一个数据报出去,接收方收没收到、顺序乱没乱,发送方并不在意。TCP要做到可靠传输,就必须让双方维护一套共同的编号体系:我发了多少字节、你收到了多少字节、下一个该发哪个字节,这些信息在通信之前得先对齐。所以TCP连接的本质,不是一条真的物理链路,而是通信双方在各自内存里建立的传输控制块(TCB)。里面记录了对方的IP、端口、当前收发序号、窗口大小、拥塞状态等一堆参数。三次握手做的事情,本质上就是让两端把这份状态同步好,尤其是初始序号(ISN)。你可以把两个进程想象成准备对暗号的两个人。双方都得知道对方从哪个编号开始数数,否则后面“确认收到第几个字节”就无从谈起。握手不是礼貌,是建立协同工作的基础。1.2 三次握手同时完成了哪三件事一次完整的TCP连接建立,必须解决三个问题:确认双方收发能力正常。任何一方发出的报文,另一方都能正确收到并回复,这说明链路可用。同步初始序号。两端各自告诉对方“我的第一个字节从序号x开始”,同时确认对方发来的初始序号。协商连接参数。比如最大报文段长度MSS、窗口缩放因子、是否支持SACK,这些会在握手报文里捎带协商。很多人以为握手就是交换三个包,其实这三个包里还藏了客户端和服务端各自的参数声明。抓包时你去看SYN包里的TCP Options,里面有MSS、窗口缩放、时间戳等字段,这些都是在这三步里搞定。正因为要做的事情不止“确认存在”,所以两步不够,三步正好。2. 三次握手逐段拆包:SYN、SYN-ACK、ACK 里每个数字都代表什么2.1 第一次握手:客户端发出SYN,亮出自己的初始序号客户端从CLOSED状态主动调用connect,发出一个SYN报文。这个报文的关键字段是:SYN标志位置1序列号seq = x,这里的x就是客户端随机生成的初始序号ISN不携带应用数据控制位SYN会占用一个序号为什么初始序号要随机,而不是固定从0开始?主要有两个原因。第一,防止上一次连接遗留的旧报文被误认为是当前连接的报文。如果序号固定,新旧连接的编号可能完全重合,旧数据就会“串门”。第二,防止攻击者猜测序号后伪造报文劫持连接。随机ISN是TCP安全的基础之一。这里有个初学者常忽略的点:SYN报文虽然不携带数据,但它会占用一个序号单位。所以客户端发出seq=x之后,下一次再发数据时seq会变成x+1。这个“控制报文也占序号”的规则,后面理解ACK值全靠它。发送SYN后,客户端进入SYN_SENT状态,等待服务端回复。2.2 第二次握手:服务端返回SYN-ACK,一次回应两件事服务端本来处于LISTEN状态,收到SYN后,如果愿意建立连接,会回复一个SYN-ACK报文。里面同时置两个标志位:SYN=1和ACK=1。语义是:SYN=1:服务端也亮出自己的初始序号yACK=1:确认收到客户端的SYNseq = y(服务端的ISN)ack = x+1这里ack=x+1是很多新手最容易懵的地方。为什么不是x?因为客户端发来的SYN报文虽然不承载业务数据,但它本身占用一个序号。服务端确认收到的是“序号为x的控制报文”,所以期待客户端下一个发出的字节序号是x+1。这个ack的完整含义是:“我已经收到你序号x及以前的所有字节,下一个请从x+1开始发。”服务端发出SYN-ACK后进入SYN_RECEIVED状态,此时连接还是半连接状态——服务端知道客户端想连,但还没确认客户端收到了自己的SYN。这个半连接会占用服务端内存资源,也是后面讲SYN Flood攻击和半连接队列的基础。2.3 第三次握手:客户端发送ACK,连接正式建立客户端收到SYN-ACK后,需要再回一个ACK报文:ACK=1seq = x+1ack = y+1这里ack=y+1同样是因为服务端的SYN占了一个序号。客户端的意思是:你的初始序号y我收到了,下一个请从y+1开始发。这个第三次握手有两个细节值得注意。第一,从第三包开始,客户端就可以携带真正的应用数据了。因为前两次交互已经让客户端确认服务端存在且收发正常,所以客户端在发送ACK时能捎带第一个HTTP请求等数据。但服务端在收到这个ACK之前,不会把连接交给应用层,因为应用层要等连接真正建立。第二,服务端收到第三个ACK后,状态从SYN_RECEIVED变为ESTABLISHED,连接才算完全建立。客户端其实在发出ACK后就已经进入ESTABLISHED状态。两边状态都到位,才开始传数据。2.4 为什么必须是三次:用“迟到的SYN”验证两次握手的问题这个问题几乎是面试必问,但真能讲清楚的人不多。我们设想一种场景:客户端第一次发了一个SYN,因为网络拥堵迟迟没到服务端。客户端等超时后重发了SYN,这次成功完成握手,数据传输完毕后关闭连接。可就在连接关闭之后,那个最初拥堵的旧SYN突然到达了服务端。如果只有两次握手,服务端收到这个迟到的SYN,会认为自己建立了一个新连接,于是回复SYN-ACK,然后服务端单方面把连接标记为ESTABLISHED,分配相关资源。但客户端根本没有发起这条新连接,收到这个莫名其妙的SYN-ACK后直接回一个RST重置。服务端直到收到RST才算明白,期间白白占用资源,可能还会干扰后续正常的连接请求。如果是三次握手,情况就不一样。客户端在第三次ACK里会带上自己本次连接的上下文。如果客户端根本没发起连接,或者这个SYN属于旧连接,客户端要么不回ACK,要么直接回RST,服务端就不会错误建立连接。更深层的原因可以这样理解:**两次握手只能保证服务端确认了客户端的发送能力,无法让客户端确认服务端收到了自己的确认信息。**三次握手通过“客户端主动再发一个ACK”这个动作,把“我知道你知道我知道”这个链条闭环,同时也给客户端一个机会,用RST主动终结无效的握手过程。顺带提一句,三次握手也决定了服务端资源分配的时机。服务端在收到第一个SYN后就要建立半连接、分配序号空间,如果迟迟收不到第三个ACK,半连接会一直挂着。这也是SYN Flood攻击的原理——攻击者发送海量SYN但不回ACK,把服务端半连接队列打满,正常的连接请求就无法处理。3. 四次挥手逐段拆包:全双工通道为什么各自说再见3.1 全双工决定的结论:两个方向需要独立关闭TCP连接是全双工的,也就是说客户端和服务端可以同时发送和接收数据。这和打电话不一样,更像一条双向车道,A方向的车流和B方向的车流互不干扰。既然两个方向是独立的,关闭时就不能指望一个FIN包同时关掉两条车道。假设客户端说“我发完了”,但服务端可能还有一车数据没发完。如果服务端收到FIN就把整个连接断开,那剩余数据就只能丢在路上。所以TCP的关闭逻辑是:**每一方主动关闭时,仅仅表示“我这边的数据发完了”,对方还能继续向我发数据。**只有双方都声明“我这边没有数据了”,连接才真正关闭。这就是为什么要四次挥手。3.2 挥手四步的报文细节和状态轨迹假设客户端主动发起关闭,完整过程如下:第一步:客户端发送FIN报文FIN=1seq = u(u是客户端最后一个已发送数据的序号加1)客户端进入FIN_WAIT_1状态FIN和SYN一样,虽然不携带数据,但会占用一个序号。客户端发完FIN后,表示“我这边没有更多数据要发了”。第二步:服务端回复ACKACK=1ack = u+1seq = v(服务端当前发送序号)服务端收到FIN后回复确认,进入CLOSE_WAIT状态。客户端收到这个ACK后,从FIN_WAIT_1进入FIN_WAIT_2。这个阶段非常关键:**客户端已经关闭了自己的发送方向,但接收方向仍然是打开的。**服务端如果还有数据要发,可以继续发,客户端照样能收。很多应用层协议在做优雅关闭时就会利用这个特性,比如HTTP服务端接到客户端的结束信号后,先把剩余响应发完,再调用close。第三步:服务端发送自己的FIN报文服务端把剩余数据全部发完之后,发送FIN:FIN=1seq = wack = u+1(这里的ack还是对着客户端那个FIN)服务端从CLOSE_WAIT进入LAST_ACK状态,表示“我等客户端确认我的FIN”。有很多连接会长时间停在CLOSE_WAIT,原因就是应用层代码里没有正确关闭socket,只关了自己的写方向,没把整个socket close掉,服务端自然发不出FIN。第四步:客户端发送最后一个ACKACK=1seq = u+1ack = w+1客户端收到FIN后回复ACK,进入TIME_WAIT状态,等待2MSL后关闭。服务端收到这个ACK后进入CLOSED状态,彻底断开。仔细观察这四个步骤:前两步是客户端单向关闭,后两步是服务端单向关闭,合起来正好四个报文。如果双方同时调用close,流程还会稍微不同,可能出现CLOSING状态,但总的交互原则不变。3.3 TIME_WAIT的两大使命:为什么主动关闭方最后还要等两分钟TIME_WAIT是这个流程里最容易被误解、也最容易被生产环境“嫌弃”的状态。TIME_WAIT状态出现在主动关闭连接的一方,它要做两件事。第一,**确保最后一个ACK能到达对端。**最后一个ACK如果丢了,服务端会发现自己迟迟收不到确认,于是重新发送FIN。客户端进入TIME_WAIT后不会立即关闭,而是继续等待,如果收到重发的FIN,就重新发送ACK,直到双方都能顺利进入CLOSED。如果没有TIME_WAIT,客户端直接关闭,最后一个ACK丢了之后,服务端重发FIN无人应答,服务端就会悬挂在LAST_ACK状态,白白占用资源。第二,**防止旧连接的延迟报文干扰新连接。**假设客户端刚关闭一个连接,立刻用相同的IP和端口四元组建立新连接。如果没有等MSL时间,旧连接中还在网络里游荡的延迟数据包,可能被新连接接收,造成数据错乱。TIME_WAIT的2MSL等待时间,足以保证旧连接的报文在网络中全部消失。这里的MSL是“报文最大生存时间”,不同系统取值不同。老的BSD实现常见30秒或60秒,2MSL就是1到2分钟。网上很多资料写“TIME_WAIT持续2分钟”,那是拿历史默认值当标准了,实际现代Linux的默认值可能更短,而且部分参数可以调整。面试时最好不要把“2分钟”说死,而要说“2倍MSL”。在生产环境里,TIME_WAIT多通常会出现在主动关闭方,尤其是短连接密集的服务。比如一个服务作为客户端频繁请求外部API,每次请求都新建连接、请求完立即关闭,这个服务上就会堆积大量TIME_WAIT。这本身是协议正常运行的结果,真正的问题在于连接创建和销毁的频率太高。4. 用抓包和状态关联看TCP连接:不再死背流程4.1 TCP头部控制位的实际含义抓包工具里最常见的几个控制位是SYN、ACK、FIN、RST,它们的组合决定了报文的作用。标志位含义用途SYN同步序号连接建立时使用,占用一个序号ACK确认确认已收到对方数据,本身不占用序号FIN结束关闭本方发送方向,占用一个序号RST重置异常断开连接,不占用序号这里可以记住一个通用规则:**SYN和FIN作为控制报文,各占一个序号;ACK和RST不占序号。**所以你会发现三次握手里,客户端发出seq=x后,第三包的seq变成x+1,而不是x;服务端发出seq=y后,第三次握手的ack变成y+1,原因都是SYN占了那一个序号。4.2 用抓包工具看一次完整的HTTP请求在Linux服务器上,可以用tcpdump直接观察握手过程。假设客户端IP是192.0.2.100,服务端是198.51.100.10:tcpdump -i eth0 tcp port 80 -nn -S-S参数用来显示绝对序号,否则看到的都是相对序号。抓包结果大致长这样:1 0.000000 192.0.2.100:50001 > 198.51.100.10:80 Flags [S], seq 1000 2 0.000123 198.51.100.10:80 > 192.0.2.100:50001 Flags [S.], seq 2000, ack 1001 3 0.000135 192.0.2.100:50001 > 198.51.100.10:80 Flags [.], seq 1001, ack 2001第一行Flags [S]就是SYN;第二行Flags [S.]是SYN-ACK,那个点表示ACK位;第三行Flags [.]就是纯ACK。配合前面讲的报文细节,这一眼就能看明白。实际使用Wireshark时,如果不开相对序号显示,你看到的seq可能是0、1、1这样的数字,这是Wireshark为了可读性做的归一化处理,不是真实的ISN。分析时要注意区分。4.3 用ss和netstat跟踪状态机没用过状态机排查问题的人,很可能会觉得状态图只是考试工具。实际排障时,状态机往往比业务日志更能说明问题。在Linux上最常用的命令是ss -ant,比netstat快,信息也更全。输出里的State列就是连接当前所处的状态。比如:LISTEN:服务端正在监听端口SYN_SENT:客户端已发出SYN,等待SYN-ACKSYN_RECEIVED:服务端已发出SYN-ACK,等待客户端ACKESTABLISHED:连接建立,可以传输数据FIN_WAIT_1:主动关闭方已发FIN,等ACKFIN_WAIT_2:主动关闭方已等ACK,等对方FINCLOSE_WAIT:被动关闭方已收到FIN,等应用层关闭LAST_ACK:被动关闭方已发FIN,等对方ACKTIME_WAIT:主动关闭方已发最后ACK,等待2MSL我自己的排查习惯是:看到大量CLOSE_WAIT,先怀疑应用层没关socket;看到大量TIME_WAIT,先确认服务作为客户端是不是频繁短连接;看到FIN_WAIT_2堆积,多半是对方应用层卡住一直不关闭。5. 真实环境里握手挥手的故障形态与排查方法5.1 半连接队列和全连接队列溢出:握手中途“消失”的包服务端收到SYN之后,并不会立即把连接交给应用层。不同阶段,连接会放在不同的队列里:半连接队列(SYN队列):服务端收到SYN后、收到第三次ACK前,连接挂在半连接队列里。全连接队列(accept队列):三次握手完成、连接进入ESTABLISHED后,等待应用层调用accept取走的连接放在全连接队列里。如果半连接队列满了,服务端可能直接丢弃SYN,客户端就会一直卡在SYN_SENT,反复重传。如果全连接队列满了,可能出现客户端已经完成握手、连接也显示ESTABLISHED,但应用层一直accept不到,请求迟迟没有响应。排查这类问题,可以先看ss -lnt输出里的Send-Q列,它表示全连接队列的最大长度,Recv-Q列可以对比当前等待accept的连接数。还可以通过netstat -s看SYN丢弃计数是否在增长。调整的方向通常是增大系统listen backlog、程序里listen参数调大、或者调整内核net.core.somaxconn。但要先搞清楚是哪一个队列溢出,别一上来就盲目调参。5.2 客户端卡在SYN_SENT和服务端卡在SYN_RECEIVED的排查路径客户端长时间停在SYN_SENT,可能是以下原因:目标IP不可达或端口没监听。可以先ping目标IP,再用telnet或nc测试端口。中间链路防火墙丢弃SYN包。很多云安全组或本机iptables规则会静默丢弃SYN,而不是回RST。服务端半连接队列满,不处理新的SYN。服务端长时间停在SYN_RECEIVED,说明客户端发来的第三个ACK没有到达服务端。常见原因包括:网络回程丢包,ACK在传输中被丢弃。服务端半连接队列处理不过来,把ACK当成无效包丢了。客户端主动关闭了连接,不再发送ACK。排查时最有效的手段是同时在客户端和服务端抓包,对比同一时间窗口内两边看到的报文是否一致。如果客户端发送了ACK而服务端没收到,问题就在中间链路或服务端收包逻辑;如果客户端根本没发ACK,问题在客户端侧。5.3 RST替代优雅FIN:哪些场景会让“四次挥手”瞬间结束四次挥手是优雅关闭,双方都留足了时间把数据发完。但实际网络里大量连接是被RST硬生生断开的。RST报文的语义是“这个连接已经不存在,请立刻停止发送”。收到RST的一方会丢弃连接上的所有缓冲数据,不会继续确认。几种常见情况:向一个不存在的端口发送数据,对端内核会直接回RST。比如服务端根本没监听这个端口。程序设置了SO_LINGER并清空缓冲区,关闭socket时内核不发FIN而直接发RST。连接已经被对端关闭,本端还在继续发送数据,对端收到后会回RST。生产环境里如果把优雅关闭和异常重置混为一谈,排查起来会绕很大圈子。我的经验是:看到RST先不要急着查应用日志,先确认是哪一侧发的RST。收到RST的一方通常是受害者,发出RST的一方往往才是问题所在。5.4 大量TIME_WAIT的场景:内核参数不是万能的服务主动关闭大量短连接时,TIME_WAIT会积压。最典型的是高并发的代理程序,每处理一个请求就新建上游连接,请求完立即关闭,TIME_WAIT数量很快飙升。网上很多文章教你调低MSL时间、开tcp_tw_reuse这些参数。我的看法是:这些参数有各自严格的适用条件,盲目修改可能引发连接错乱。比如NAT后面有大量客户端时,开启某些复用功能可能造成连接串扰。真正稳妥的做法是调整应用层设计:用连接池复用连接、打开HTTP keep-alive、减少不必要的连接新建和销毁。TIME_WAIT本身是协议可靠性的代价,你把它强行消掉,代价只是从“看得见的TIME_WAIT”变成了“极难排查的偶发异常”。6. 常被问错也常被记错的那些点:用序号规则把流程串起来6.1 几个流传很广但不准确的说法“握手必须三次,挥手必须四次。”不完全对。常规标准流程确实是三个报文和四个报文,但TCP允许在第二次握手时把SYN和ACK合在一个报文里,挥手时也可以把第二次ACK和第三次FIN合并发送。某些实现中四次挥手会变成三次。同时关闭的场景下状态流转也不一样。关键是理解每一步的语义,而不是把数字背死。“发送FIN之后就什么都不能发了。”错。FIN只代表本方的发送方向结束,接收方向仍然开着。对方在收到FIN之后、发出自己FIN之前,依然可以合法地发送数据。这就是半关闭状态存在的意义。“TIME_WAIT等待时间是2分钟。”历史上BSD衍生系统的MSL常见是30秒或60秒,但现代系统可以配置,并不统一。与其背一个固定数字,不如记住2MSL这个原则,再回答“具体时间取决于系统实现”。6.2 抓住seq和ack的核心规律,全流程都不会乱如果你不想背任何具体序号,只要记住下面三条规律,整个握手挥手都能推出来:**发送方的seq = 自己已经发送的字节数 + 1。**即将发送的新数据从哪个序号开始。**接收方的ack = 自己期望收到的下一个字节序号。**等于对方已发送字节数 + 1。SYN和FIN各占一个序号,所以ack的数值都要在对方seq基础上加1。拿三次握手验证:客户端发seq=x的SYN,服务端回ack=x+1;服务端发seq=y的SYN,客户端最后回ack=y+1。四次挥手完全同理:客户端FIN的seq=u,服务端ACK的ack=u+1;服务端FIN的seq=v,客户端ACK的ack=v+1。这套规则不仅能解释正常流程,还能帮你快速判断抓包结果是否合理。只要报文的ack值和对方上一个seq对不上,十有八九是中间有重传或丢包,值得深入排查。这篇文章写到这里,核心内容已经说得差不多了。最后分享一点个人的实际体会:真正让你对握手挥手产生深刻理解的,不是反复背流程图,而是亲手抓一次包,或者在生产环境里盯着状态列表看一次故障。我自己第一次把CLOSE_WAIT和代码里忘记close()对应起来的时候,才意识到教科书上的状态机图原来这么有用。如果遇到连接问题不知道从哪入手,先把TCP状态和抓包结果摆出来,再层层缩小范围,比盯着应用日志猜原因有效得多。希望这篇不只是帮你应付面试,更能成为你下次排障时的手边参考。