☰
TCP三次握手与四次挥手:从报文细节到线上排查全解析
2026/10/14 4:58:57 网站建设 项目流程

还在背状态图?我把TCP握手挥手讲透给你

先问你一个问题:面试官问"TCP三次握手为什么不能是两次",你是直接回答"因为要防止历史重复连接"还是支支吾吾半天说不清?

TCP三次握手和四次挥手,几乎每个人都背过,但真到了面试场景、线上排查环境里,大多数人会瞬间露馅。原因很简单:大家都记住了那张状态迁移图,却没搞懂每个状态、每个报文背后的设计动机。这篇不打算给你画一张华丽的流程图然后讲一遍就完事,而是从报文细节、异常场景、面试官追问逻辑三个角度,把三次握手和四次挥手拆到最细,顺便把那些"拓展面试题"一并解决。

写这篇的原因也很实在:我见过太多候选人,能把SYN和ACK的缩写含义说清楚,但一被问到"netstat看到大量TIME_WAIT怎么办""握手包丢了怎么查""为什么服务端连接数上不去"就直接卡壳。如果你也在准备面试,或者正在线上环境里被TCP疑难杂症折磨,这篇值得你花十五分钟读透。

1. 三次握手:不只是"你好了我也好了"

三次握手的本质,是通信双方在不可靠的网络上,通过三次报文交换,同步彼此的初始序列号(ISN),并确认对方能收能发。很多人只记住了SYN、SYN+ACK、ACK这个顺序,却不清楚每个包里带着什么关键字段,更不理解为什么要用序列号来建立可靠连接。

1.1 报文层面看清三次握手的每个细节

一次标准的TCP连接建立,从连接发起方(通常是客户端)发出第一个SYN报文开始。假设客户端选择了一个初始序列号,比如1000,那么SYN报文里会带上seq=1000,并将SYN标志位置1。这里有个容易被忽略的点:SYN包本身要消耗一个序列号,因为它是一个"虚拟数据段",所以即使不带任何应用数据,对方回复ACK时确认号也要在此基础上加1。

服务端收到SYN后,如果同意建立连接,会返回SYN+ACK报文。这个包同时携带两个关键信息:自己的初始序列号,比如2000,以及ack=1001,也就是客户端序列号加1,表示"你的SYN我收到了,下次请从1001开始发"。SYN+ACK同样消耗服务端的一个序列号。

客户端收到SYN+ACK后,再发一个ACK报文,确认号ack=2001,表示"你的SYN我也收到了",同时这个ACK包可以不携带数据,但通常也会带上自己的序列号1001。到这里,连接正式建立,双方都确认了"我能发、你能收;你能发、我也能收"。

三次报文里每个字段都有明确含义,尤其是确认号的数值计算,面试时如果是手写或者看图分析,最容易在这里翻车。确认号永远等于"对方发送的序列号+对方发送的数据字节数+1",SYN和FIN都要当作占一个字节来算。

1.2 为什么必须是三次而不是两次

这是面试高频题,但大部分人答得不够完整。最核心的原因是:如果不做第三次握手,服务端无法确认客户端是否收到了自己的SYN+ACK报文,可能导致服务端白白保留一个半开连接,浪费资源。但更严谨的解释,要回到TCP的可靠性目标和历史重复报文问题。

设想一下:客户端发送的SYN报文在网络中滞留了很久,客户端等不及超时重传,又发了一个新的SYN。旧的SYN先到达服务端,服务端回了一个SYN+ACK。如果只有两次握手,服务端会认为连接已经建立,开始等待数据。但客户端这边,旧的SYN早就不是自己当前连接上下文里的请求了,它收到SYN+ACK后如果发现不是自己期望的,会直接丢弃或者回RST。问题是服务端并不知情,依然维持连接资源,这种"半连接"就是典型的资源浪费和安全隐患。

有了第三次握手,服务端只有在收到客户端对SYN+ACK的确认后,才会把连接状态切换为Established。如果客户端在第三次握手时携带的是对旧SYN的拒绝信息,服务端就能及时释放资源。所以三次握手的设计,本质是让服务端能够感知到客户端当前处于什么状态,避免历史连接造成的混乱。

提示:如果有面试官问你"能不能改为两次",你可以从"防历史重复SYN"和"同步双方序列号"两个角度回答。单纯说"因为要双方确认收发能力"其实是不够严谨的,因为一次握手就能让客户端确认自己发的能力,两次握手已经能让服务端确认收的能力了,关键问题在"过期报文的识别"。

1.3 第三次握手丢了会发生什么

线上最常见的问题就是"握手丢包"。比如客户端发的第三次ACK报文,因为网络拥塞丢失了。此时服务端已经收到SYN并发出SYN+ACK,等待ACK来建立连接。如果ACK一直不来,服务端的TCP协议栈会根据超时时间重传SYN+ACK(不是重传SYN),重传次数受tcp_synack_retries参数控制,Linux默认是5次,也就是大约1分钟。

同时,客户端因为已经完成了自己的状态切换,进入Established,开始发送应用数据。服务端还在Syn_Received状态,收到数据后它无法确认这是合法连接上的数据,会直接返回RST报文,把客户端刚建立的连接打断。所以第三次握手丢包的实际表现是:客户端认为连接已建立,但第一次数据交互就失败,出现Connection Reset的情况。

排查这类问题时,有两个方向可以走:

  • 在客户端和服务端分别抓包,看ACK是否到达、SYN+ACK是否有重传
  • 检查服务端是否有大量SYN_RECV状态的连接,如果大量堆积,大概率有攻击或者握手异常

2. 四次挥手:断开比建立更讲究

有句话说"三次握手容易,四次挥手难",难在主被动方的状态扭转、TIME_WAIT的存在价值、以及各种异常断开场景。

2.1 挥手的完整状态链

假设客户端主动发起断开连接。客户端先发一个FIN报文,携带seq=M,进入FIN_WAIT_1状态。服务端收到FIN后,返回ACK(确认号M+1),同时进入CLOSE_WAIT状态。注意,此刻服务端应用不一定没有数据要发,TCP并没有强制它在收到FIN后立刻也发FIN,它可以继续发剩余数据。这就解释了为什么挥手需要四次:TCP支持半关闭,一方结束发送后,另一方还能继续发送数据,因此FIN和ACK是分开的。

等服务端把剩余数据发完,应用层调用关闭接口,服务端再发FIN报文,携带seq=N,进入LAST_ACK状态。客户端收到这个FIN后,回复一个ACK,进入TIME_WAIT状态,而不是立刻关闭。服务端收到最后的ACK后,进入CLOSED状态。客户端要等2MSL(最长报文段寿命的两倍)后,才真正进入CLOSED。

很多人背状态图时总把FIN_WAIT_2和CLOSE_WAIT搞混。其实只要记住:谁主动发起断开,谁经历FIN_WAIT_1、FIN_WAIT_2;谁被动接收断开,谁经历CLOSE_WAIT。FIN_WAIT_2是主动方发送FIN并收到对方ACK后,等待对方发FIN的状态;CLOSE_WAIT是被动方收到FIN、回完ACK后,等待应用层关闭套接字的状态。

2.2 为什么最后要等2MSL而不是直接关闭

TIME_WAIT是四次挥手里最有含金量的设计。连接主动关闭方在发送最后一个ACK后,必须在这个状态停留2MSL的时间。MSL是报文在网络中的最长生存时间,RFC建议2分钟,Linux默认一般是30到60秒,看具体系统设置。

两个原因:

  • 保证最后的ACK能被对方收到。如果这个ACK丢了,服务端在LAST_ACK状态会超时重传FIN。如果客户端已经关闭,收到这个重传的FIN后会直接回RST,服务端收到RST后可能会把它当作错误来处理,但至少服务端能明确连接已异常。如果客户端保持在TIME_WAIT一段时间,就能在对方重传FIN时再次回复ACK,帮助对方正常关闭。
  • 让旧连接的报文在网络中自然消亡。假设客户端这次连接关闭后,立即用相同的四元组建立新连接,如果网络里还残留着旧连接的迟到数据包,新连接就可能收到"穿越"过来的数据,造成数据混淆。等2MSL后,所有旧报文都已消亡,新连接才是真正"干净"的。

2.3 大量TIME_WAIT和大量CLOSE_WAIT分别怎么处理

几乎所有做后端服务的人都遇到过netstat一查,一堆TIME_WAIT。这个状态本身不可怕,它说明系统在正常地关闭大量短连接。真正应该担心的是两个极端:

TIME_WAIT过多通常出现在高并发的短连接场景。如果系统的TIME_WAIT连接数超过几万,可能耗尽可用端口或内存。常用的优化思路包括:开启net.ipv4.tcp_tw_reuse(前提是连接方向是客户端,且依赖时间戳选项)、调整net.ipv4.tcp_fin_timeout、使用长连接替代短连接。但不要盲目开启tcp_tw_recycle,Linux 4.12之后这个选项已经被移除,因为它在NAT环境下会造成严重问题。

CLOSE_WAIT过多则是应用程序的锅,因为它表示对端已经关闭连接,但本地应用一直没有调用close()释放套接字。大量CLOSE_WAIT基本等于代码有bug,通常可以从三个方向排查:是否有异步线程持有socket一直没有释放、是否有连接池管理的连接没有被正确回收、是否有异常分支没有走到close逻辑。

状态出现原因主要风险首要处理方向
TIME_WAIT主动关闭方正常出现连接数过多时资源紧张长连接替代、tw_reuse配合时间戳
CLOSE_WAIT被动关闭方未释放socket文件描述符耗尽修代码,检查所有close路径
SYN_RECV收到SYN未完成握手可能有攻击或丢包检查半连接队列与丢包
FIN_WAIT_2主动方等对端FIN对方一直不关闭超时处理、应用逻辑检查

3. 异常场景与线上排查:握手刀锋上的故障

理论说完了,说点实战。如果连接建不起来,或者建起来之后异常断开,该怎么定位?这部分对做后端、运维、网络排查的人特别重要。

3.1 半连接队列与全连接队列:连接建立的中转站

服务端的TCP连接建立会经过两个队列:

  • 半连接队列(SYN队列):存储收到SYN但握手未完成的连接,对应内核参数net.ipv4.tcp_max_syn_backlog
  • 全连接队列(Accept队列):存储握手已完成后但等待应用层accept()的连接,对应参数net.ipv4.tcp_somaxconn和应用程序的backlog参数

二者的配合很直接:客户端SYN到达,服务端把连接放到半连接队列并回复SYN+ACK;客户端ACK到达,连接移入全连接队列;应用调用accept,从全连接队列取走连接。如果全连接队列满,服务端会丢弃新连接请求,具体行为取决于tcp_abort_on_overflow参数。

典型的排查命令是ss -lnt,看Send-Q列就是Accept队列的最大值,Recv-Q是当前等待accept的连接数。如果Recv-Q长期占满Send-Q,说明应用处理accept的速度跟不上连接建立的速度,优先优化应用,而不是盲目调大内核参数。

半连接队列溢出则更隐蔽,一般表现为客户端SYN发出后收不到SYN+ACK。可以通过netstat -s查看SYNs to LISTEN sockets dropped或者TCP: time wait sockets killed等统计,配合抓包确认SYN重传行为来定位。

3.2 实战案例:一个"对方端口无响应"的排查过程

有一次我遇到一个典型的线上问题:某个客户端到服务端的连接,约10%的概率建立失败,客户端日志报Connection timed out,服务端业务日志却完全没有异常。

排查步骤:

  • 第一步,在客户端和服务端同时抓包,发现客户端发SYN后,服务端确实收到了,但迟迟没有回SYN+ACK
  • 第二步,用netstat -s看连接统计,发现SYNs to LISTEN sockets dropped计数器一直在涨
  • 第三步,确认就是半连接队列满导致的丢SYN
  • 第四步,进一步查为什么队列会满:ss -lnt看到全连接队列并不满,但通过ss -s和cat /proc/net/somaxconn对比,发现半连接队列积压了大量来自同一IP的连接

原因其实是某个异常客户端在短时间内疯狂重连,且每次都停在半握手状态,占满了SYN队列。临时处理是加白名单过滤异常IP,同时调整tcp_max_syn_backlog和应用backlog,长期做法是修复客户端重连逻辑。

这个过程里有一个调试小技巧非常管用:ss -lnt里面的Recv-Q和Send-Q并不是永远都是0,这两个数值分别反映了全连接队列的当前积压情况和队列上限。如果Recv-Q持续为200而Send-Q为128,基本就是队列溢出的直接证据。

3.3 主动断开方收到RST:别再一个个猜了

RST是TCP协议里用来强制断开连接的标志。排在现象前三的触发原因如下:

  • 对端端口没有进程监听,内核直接回RST。这是最基础的场景,通常telnet就能复现
  • 连接已经关闭,但旧报文才到达,接收方以RST回应
  • 防火墙或中间设备主动发送RST进行干扰,常见于某些安全设备或代理的异常策略

排查时先抓包看RST方向。如果RST从客户端本地发出,多半是本地应用没走完四次挥手就直接close了;如果RST来自服务端,则要区分是服务端程序主动reset,还是服务端内核协议栈因为未知端口或序列号不匹配而reset。序列号不匹配这个原因经常被忽略,它往往出现在NAT设备老化连接后,网关重放了老序列号的报文,导致对端校验失败触发RST。

4. 拓展面试题:从握手挥手延伸出去的知识纵深

面试官不会只问一遍流程就收手,他们往往会把三次握手和四次挥手当成引子,串起一整个知识网络。这几个拓展题是我在实际面试中被问过、也问过别人的,基本每次都能看出候选人的深浅。

4.1 握手为什么可以捎带数据,挥手为什么通常不能

TCP规定SYN和FIN都要消耗序列号。三次握手的第三次ACK是可以携带应用数据的,因为连接此时已经建立。但理论上,服务端的SYN+ACK也可以捎带数据,前提是连接足够快。从协议角度来看,握手报文没有禁止携带载荷,写代码时可以这么做,但绝大多数实现会在握手完成前不发送应用数据。

挥手的问题在于:第一次FIN和ACK是分开发送的,因为被动关闭方可能还有数据要发。如果被动方在收到FIN后立即也发FIN,ACK和FIN可以合并成一个包,这样四次挥手实际上会退化为三次报文交换,但这不代表关闭过程可以跳过半关闭阶段。面试时能说出"可以将ACK和FIN合并为一次",比干巴巴背四次要好得多。

4.2 TCP的seq/ack到底怎么算:一道伪装成八股的必杀题

面试官最爱出这种题:客户端发送了一个SYN,初始seq=1000,服务端在SYN+ACK中返回ack=1001、seq=5000。之后客户端发了一个携带10字节数据的包,seq=1001。请问服务端收到后,回复的ack等于多少?

答案是1011。计算规则就是先看数据长度10,再加上seq,最后确认号表示"我期望你下一个字节的序号",所以是1001+10=1011。这里唯一的坑在于,SYN、FIN各占一个序列号,但普通数据包的首字节序列号就是前面报文的末字节序号加1(通常等于对方seq+已发送有效数据长度,因为SYN已经加过一个1了)。

这类题不仅考察计算,更考察你对"序列号空间"的理解。TCP的序列号是32位无符号整数,循环使用,所以现代TCP会加时间戳选项来区分相同序列号的重复报文。这也是tcp_tw_reuse能安全工作的前提之一。

4.3 假如服务器上是单端口,最多能建立多少TCP连接

这是一个极经典的拓展题,答案完全取决于你从哪个视角出发:

  • 客户端视角:连接数上限受本地端口数(约65536)和远端IP/端口组合限制,还要受文件描述符限制
  • 服务端视角:一个端口监听可以接收的连接数远不止65535,因为服务端区分连接的四元组包含客户端IP和端口,理论上限是客户端IP数乘以客户端端口数,实际上限是内存和文件描述符

通过这个问题的讨论,能很好展示你是否理解TCP四元组:源IP、源端口、目标IP、目标端口。来源端口是16位,意味着单个客户端的源端口数量最多65536个,但服务端的accept不受这个限制,因为每个客户端IP不同就很容易突破。这里再搭配ulimit -n、sysctl fs.file-max等参数,就能完整回答连接上限的工程问题。

4.4 HTTP和TCP的关系:请求/响应模型下的握手机制

很多人学了TCP但无法把HTTP映射上去。HTTP/1.1默认使用持久连接(Keep-Alive),也就是一个TCP连接上可以连续发送多个请求。但HTTP/1.1的队头阻塞问题也源于此:同一连接中,前一个请求未处理完,下一个请求无法响应。

HTTP/2则是通过多路复用,在一个TCP连接上并发处理多个消息流,彻底解决应用层的队头阻塞,但它依然不能突破TCP层的问题——如果TCP丢包,即使多个流中只有一条受到影响,所有流都要等待重传。这也是后来QUIC(基于UDP)出现的重要原因之一,它把可靠性控制上移到用户空间,规避了TCP层的队头阻塞。

4.5 握手与TLS握手的先后顺序

HTTPS连接建立时,TCP三次握手在前,TLS握手在后。整体流程是:TCP SYN/SYN+ACK/ACK完成后,客户端发起ClientHello,服务端返回ServerHello、证书和密钥交换参数,双方交换Finished后,TLS隧道建立,随后才传输HTTP数据。

这两个流程叠加后,一个HTTPS请求的RTT(往返时间)至少需要2个RTT:TCP握手的1个RTT,TLS握手(TLS 1.3简化后)的1个RTT。通过TCP Fast Open(TFO)可以节省TCP握手的数据传输延迟,但要权衡安全风险;通过TLS 1.3的会话恢复机制可以省掉第二次HTTPS连接的大部分握手。面试时能把这个链路讲清楚,已经是加分项了。

5. 系统内核参数调优:从原理到参数的落地映射

如果你只是把我上面说的东西听了,不知道内核参数怎么调,面试时在"你们线上怎么调优"这个问题上还是容易露怯。TCP握手和挥手相关的调优,是我日常用过的三组参数。

5.1 握手相关参数

net.ipv4.tcp_syn_retries控制客户端SYN重传次数,默认6,对应大约127秒。如果服务端探活、健康检查等场景不想等这么久,可以调小到2或3。需要注意的是,太小会让弱网下的访问更容易失败。

net.ipv4.tcp_synack_retries控制服务端对SYN+ACK的重传次数,默认5。如果客户端不走完握手就消失,服务端的SYN队列会一直被占用,适当调小这个值能降低半连接堆积风险。

net.core.somaxconn和应用的backlog参数共同决定Accept队列长度。很多应用框架(比如常见Web容器)默认backlog是128,在并发连接瞬间大量涌入时,很容易打满队列,建议应用层和内核层配合调整。

5.2 挥手相关参数

net.ipv4.tcp_fin_timeout控制FIN_WAIT_2的等待时间,默认60秒。如果被动关闭方迟迟不发送FIN,主动方不能无限等待。

net.ipv4.tcp_max_tw_buckets控制TIME_WAIT总数上限,超过后内核会回收多余连接,虽然不会报错,但连接复用成功率会下降,网络极端情况下可能出现建连异常。

net.ipv4.tcp_tw_reuse是使用频率极高的参数,但必须配合tcp_timestamps开启才能生效,且只在连接发起方(客户端角色)有意义,不能把它当作随便开的功能。一旦网络环境中存在NAT且时间戳不单调增加,复用TIME_WAIT连接可能会出大问题。

5.3 参数配置实例

下面的配置是我在一个高并发短连接服务上实际用过的模板,仅供参考,不要直接照抄到所有环境:

# /etc/sysctl.conf 片段 # 半连接队列上限,默认128,建议环境内存充足时调大 net.ipv4.tcp_max_syn_backlog = 8192 # 全局accept队列,和应用层backlog联动 net.core.somaxconn = 4096 # 服务端SYN+ACK重传次数,按网络质量调整 net.ipv4.tcp_synack_retries = 2 # 客户端SYN重传次数 net.ipv4.tcp_syn_retries = 3 # 主动关闭方FIN_WAIT_2超时 net.ipv4.tcp_fin_timeout = 30 # 快速复用处于TIME_WAIT的连接(仅客户端场景) net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_timestamps = 1 # TIME_WAIT最大桶数量,过小会牺牲连接复用成功率 net.ipv4.tcp_max_tw_buckets = 30000

注意:改完sysctl.conf后执行sysctl -p生效。线上变更务必先在灰度环境验证,因为参数之间互相影响,比如盲目调大backlog可能导致内存占用上升,而应用层accept不及时又会让队列积压更严重。

6. 面试实战:高分考官视角与应答框架

最后这部分是想说给准备面试的人听的。我发现很多候选人对TCP的知识是碎片化的,你问SYN队列他答得上来,你问TIME_WAIT他也答得上来,但你把二者串成一个场景问题,他就开始乱了。

6.1 一个把知识串起来的综合题

典型的综合题长这样:某Linux服务器上部署了服务端程序,监听8080端口。现在客户端请求偶尔超时,netstat显示大量SYN_RECV,服务器CPU和内存都很低,你如何排查?

好的回答路径应该是:

  • 先定位现象:SYN_RECV多说明握手没完成,问题出在服务端没回复SYN+ACK,或回复了但ACK没回来
  • 查队列:ss -lnt看Accept队列是否堆积;netstat -s | grep -i listen看是否有SYN dropped
  • 看网络层:抓包确认SYN是否收到、SYN+ACK是否发出,排查是否有防火墙、负载均衡设备干扰
  • 做流量分析:看是否为异常洪水,结合IP白名单/限流策略应对
  • 最后调整参数:合理设置tcp_max_syn_backlog、tcp_synack_retries,必要时启用SYN Cookies

这套链路下来,基本能把TCP握手全流程和内核实现、网络排查、系统优化全部覆盖,面试官想不给你高分都难。

6.2 我总结的答题顺序:流程→原理→场景→方案

关于"如何答得又全又不啰嗦",我在带团队时给候选人总结过一个四层答题法:

  • 第一层:先说流程和状态。让面试官知道你有基础,一两句带过即可
  • 第二层:讲设计动机。为什么三次、为什么四次、为什么2MSL,讲出设计里的"防错"和"容错"
  • 第三层:讲异常场景。连接失败、TIME_WAIT堆积、RST干扰,结合实际案例说明你做过实战排查
  • 第四层:讲调优方案。把内核参数和应用层策略落地,证明你可以从理论走到工程

很多候选人死在第三层和第四层之间。能背完整流程的人太多了,真正稀缺的是出了问题能24小时内拿出定位思路的人。

6.3 学习资料和个人建议

如果你是想系统掌握这块知识,我建议不要只看博客,至少去读一下RFC 793,里面对状态机的定义很核心,同时配合抓包工具把三次握手四次挥手实跑一遍,比看一百张图都管用。抓包时重点看Info里的[SYN]、[SYN, ACK]、[ACK]、[FIN, ACK]标记,以及Sequence Number的变化,很快你就会有肌肉记忆。

我个人最大的体会是,TCP这些状态和时序,不要在脑海里抽象理解,一定要结合崩溃的线上案例和被逼出来的排查经验。遇到一次诡异的RST,你比读十篇博客都记得牢。


最后再分享一个我自己的"土办法":平时在本地虚拟机里搭一个极简的TCP服务端,用Python的socket写几十行代码,再配合抓包工具手动模拟各种异常,比如发送乱序报文、直接发RST、模拟半开连接。把这些场景全部亲手跑一遍后,再回头去背状态图,所有信息都会自动串起来。学习网络协议真的没有捷径,但亲手造一次故障,比看任何文章都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询