news 2026/10/21 15:57:41

TCP三次握手与四次挥手:从抓包分析到参数调优的实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TCP三次握手与四次挥手:从抓包分析到参数调优的实战指南

1. 为什么面试官总爱问三次握手和四次挥手

如果你面过网络相关的岗位,不管是后端开发、运维还是测试,大概率都被问过这个问题。很多人背得滚瓜烂熟,但真到了线上抓包排查问题的时候,却连一个SYN丢包都看不出来。我自己刚入行那会儿也是这样,能把流程图画得漂漂亮亮,但遇到连接建立慢、连接数暴涨这类问题就完全懵了。

这篇文章不打算给你背八股文,而是想从实际排查问题的角度,把三次握手和四次挥手这件事讲透。我会先讲清楚每个报文到底在干什么,再讲为什么必须是三次而不是两次、四次而不是三次,然后落到真实的抓包分析、参数调优和常见故障上。适合已经知道个大概、但想真正搞明白底层逻辑的开发者,也适合需要排查网络问题的运维同学。

核心关键词就几个:SYN、ACK、FIN、半连接队列、TIME_WAIT、抓包分析。这些词你肯定都见过,但它们在真实系统里怎么表现、怎么影响你的服务,才是真正值钱的部分。

2. 三次握手:每个报文到底在解决什么问题

2.1 从"两个人在对讲机里确认通话"说起

三次握手的本质,是通信双方在正式传数据之前,先确认三件事:我的发送能力正常、你的接收能力正常、你的发送能力也正常。这就像两个人用对讲机通话,A先喊一句"能听到吗",B回一句"能听到,你能听到我吗",A再回一句"能听到"。到这一步,双方才敢确认这条链路是双向通的。

对应到TCP协议里,这三个报文分别是:

  • 第一次:客户端发送SYN=1, seq=x,意思是"我想跟你建立连接,我的初始序列号是x"
  • 第二次:服务端回复SYN=1, ACK=1, seq=y, ack=x+1,意思是"我收到了,我也同意建立连接,我的初始序列号是y"
  • 第三次:客户端回复ACK=1, seq=x+1, ack=y+1,意思是"我收到你的确认了,连接建立成功"

这里有个细节很多人忽略:序列号不是从0或1开始的,而是随机生成的。为什么?因为如果每次都用固定初始序列号,攻击者很容易伪造报文插入到连接里。随机化初始序列号(ISN)是TCP安全性的第一道防线,虽然现在有更复杂的攻击手段,但这个设计思路一直保留着。

2.2 为什么是三次,两次到底差在哪

这是面试必问的点,但很多人答不到点子上。常见的回答是"两次的话服务端不知道客户端有没有收到自己的SYN+ACK",这个说法对,但不够深。

真正的核心在于:两次握手无法防止历史重复连接请求造成的资源浪费。设想一个场景:客户端之前发过一个SYN,因为网络延迟卡在某个节点,客户端超时后重发了一个新的SYN并完成了通信,之后关闭了连接。这时候那个卡住的旧SYN突然到达服务端,服务端以为是新请求,回复SYN+ACK并直接进入ESTABLISHED状态,分配资源等待数据。但客户端根本不认这个连接,服务端的资源就白白浪费了。

三次握手能解决这个问题,因为客户端在第三步会检查这个ACK对应的连接是不是自己当前想要的。如果是历史连接,客户端会发RST拒绝,服务端就不会傻等。所以三次握手的第三次数,本质上是给客户端一个最终否决权。

提示:理解这一点之后,你就能明白为什么SYN Flood攻击有效——攻击者大量发送SYN但不完成第三次握手,服务端的半连接队列被占满,正常用户就进不来了。

2.3 半连接队列和全连接队列:连接建立的真实瓶颈

光看报文流程是不够的,真正影响服务性能的是内核里的两个队列。

半连接队列(SYN Queue):服务端收到SYN、回复SYN+ACK之后,连接处于SYN_RCVD状态,这时候连接被放进半连接队列。等收到客户端的ACK,才移到全连接队列。

全连接队列(Accept Queue):连接已经完成三次握手,处于ESTABLISHED状态,等待应用层调用accept()取走。如果应用层处理慢,这个队列会堆积。

这两个队列都有大小限制,满了之后新连接会被丢弃或拒绝。相关参数:

参数作用默认值参考
net.ipv4.tcp_max_syn_backlog半连接队列最大长度受内存影响,通常1024或更大
net.core.somaxconn全连接队列最大长度通常128或4096
listen()的backlog参数应用层指定的队列长度取somaxconn和backlog的较小值

这里有个实战经验:很多人只调了somaxconn,忘了应用层listen的backlog。比如Nginx的listen指令后面可以跟backlog参数,如果不设,默认值可能很小。我见过一个服务QPS不高但偶尔大量连接超时,最后查出来就是全连接队列溢出,内核直接丢ACK,客户端只能重传。

查看队列溢出情况可以用:

# 查看因全连接队列满而丢弃的连接数 netstat -s | grep -i "listen" # 或 ss -lnt # 输出里的Send-Q就是全连接队列上限,Recv-Q是当前等待accept的数量

如果Recv-Q经常接近Send-Q,说明应用层accept太慢,要么加worker,要么调大队列。

3. 四次挥手:为什么比握手多一次

3.1 挥手流程拆解与"半关闭"状态

四次挥手比三次握手多一次,根本原因是TCP连接是全双工的,两个方向要分别关闭。握手的时候,服务端可以把SYN和ACK合并成一个报文发出去;但挥手的时候,被动关闭方收到FIN后,可能还有数据没发完,不能立刻回FIN,只能先回一个ACK,等数据发完了再单独发FIN。

完整流程:

  • 第一次:主动关闭方发送FIN=1, seq=u,表示"我没数据要发了,但还能收"
  • 第二次:被动关闭方回复ACK=1, ack=u+1,表示"我知道你要关了"
  • 第三次:被动关闭方数据发完后,发送FIN=1, seq=w, ack=u+1,表示"我也没数据要发了"
  • 第四次:主动关闭方回复ACK=1, ack=w+1,表示"收到,你可以关了"

第二次和第三次之间,连接处于**半关闭(Half-Close)**状态。这个状态很关键,它允许被动关闭方继续发送剩余数据。比如HTTP场景里,服务端处理完请求返回响应,如果客户端先关了,服务端还能把响应发完。

3.2 TIME_WAIT:那个让人又爱又恨的状态

主动关闭方发完最后一个ACK后,不会立刻进入CLOSED,而是进入TIME_WAIT,等待2MSL(Maximum Segment Lifetime,报文最大生存时间)后才真正关闭。Linux上2MSL默认是60秒。

为什么要等?两个原因:

第一,保证最后一个ACK能到达对方。如果这个ACK丢了,被动关闭方会重传FIN,这时候主动关闭方还在TIME_WAIT状态,能重新回ACK。如果直接关了,对方重传的FIN就没人理,会一直重传到超时。

第二,让本次连接的所有报文在网络中消散。如果不等待,新连接可能复用相同的四元组(源IP、源端口、目的IP、目的端口),旧连接的延迟报文可能被新连接误收,造成数据错乱。

TIME_WAIT本身是正常状态,但问题在于主动关闭方通常是服务端(比如Nginx主动关闭客户端连接),高并发下会积累大量TIME_WAIT,占用端口资源。相关调优参数:

# 允许TIME_WAIT状态的socket被新连接复用(谨慎使用) net.ipv4.tcp_tw_reuse = 1 # 快速回收TIME_WAIT(NAT环境下可能导致问题,新内核已移除) net.ipv4.tcp_tw_recycle = 0 # TIME_WAIT最大数量,超过后直接关闭 net.ipv4.tcp_max_tw_buckets = 262144

注意:tcp_tw_recycle在较新的内核里已经被移除,因为它和NAT设备配合时会导致连接失败。如果你的内核还有这个参数,建议保持关闭。

3.3 谁先关闭连接,决定了谁背TIME_WAIT的锅

这是一个非常实用的经验:TIME_WAIT出现在主动关闭方。所以如果你的服务是主动关闭连接的一方,TIME_WAIT就会堆在你这边。

在HTTP/1.1里,默认是keep-alive,连接复用,谁先关取决于配置。Nginx有个keepalive_timeout,超时后Nginx主动关闭连接,TIME_WAIT就在Nginx上。如果后端服务主动关,TIME_WAIT就在后端。

我遇到过一个案例:某服务QPS很高,但端口耗尽,查下来是服务端主动关闭连接导致大量TIME_WAIT。解决方案有两个方向:一是让客户端主动关闭(调整keepalive策略),二是开启tcp_tw_reuse让端口能复用。最终我们选择了调整连接复用策略,因为改内核参数有风险。

4. 抓包实战:用tcpdump看清握手和挥手的每个细节

4.1 抓包命令与过滤表达式

光看理论不够,真正理解要靠抓包。最常用的工具是tcpdump,配合Wireshark分析。

# 抓取指定端口的TCP包,-S显示绝对序列号,-nn不解析域名和端口名 tcpdump -i eth0 -S -nn 'tcp port 8080' -w handshake.pcap # 只抓SYN和FIN包,快速看连接建立和关闭 tcpdump -i eth0 -nn 'tcp[tcpflags] & (tcp-syn|tcp-fin) != 0' # 抓取特定主机的包 tcpdump -i eth0 -nn host 192.168.1.100 and tcp port 80

抓完之后用Wireshark打开,过滤表达式很关键:

  • tcp.flags.syn == 1只看SYN包
  • tcp.flags.fin == 1只看FIN包
  • tcp.analysis.retransmission看重传
  • tcp.stream eq 0看某一条完整的流

4.2 从抓包结果里读出"故事"

一次正常的三次握手在Wireshark里是这样的:

No. Time Source Destination Info 1 0.000000 10.0.0.1 10.0.0.2 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 2 0.000123 10.0.0.2 10.0.0.1 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 MSS=1460 3 0.000200 10.0.0.1 10.0.0.2 [ACK] Seq=1 Ack=1 Win=64240 Len=0

注意几个细节:MSS(最大报文段长度)在SYN包里协商,双方各报自己的MSS,取较小值。Win是接收窗口,告诉对方自己还能收多少数据。这些信息在排查性能问题时非常有用。

如果看到这样的序列:

1 [SYN] Seq=0 2 [SYN, ACK] Seq=0 Ack=1 3 [SYN] Seq=0 <- 客户端重传SYN 4 [SYN, ACK] Seq=0 Ack=1 5 [ACK] Seq=1 Ack=1

说明第三次握手的ACK丢了或者延迟了,客户端以为服务端没收到,重传了SYN。这种情况如果频繁出现,可能是网络质量差或者中间设备丢包。

4.3 常见异常抓包特征对照表

现象抓包特征可能原因
连接建立慢SYN后很久才出现SYN+ACK服务端半连接队列满、网络延迟
连接被拒绝SYN后收到RST端口未监听、防火墙拒绝
大量重传同一序列号的包重复出现网络丢包、MTU不匹配
TIME_WAIT过多大量FIN后跟ACK,无后续主动关闭方集中,端口复用不足
连接卡在半关闭FIN后长时间无第二个FIN被动关闭方应用层未关闭socket

这张表是我自己排查问题时总结的,基本上看到抓包特征就能定位到大概方向。

5. 那些年我踩过的握手挥手相关的坑

5.1 半连接队列溢出导致的新连接超时

有一次线上服务突然大量超时,但CPU和内存都很正常。抓包发现客户端发了SYN,服务端有时候回SYN+ACK,有时候不回。查netstat -s发现:

SYNs to LISTEN sockets dropped

这个计数在涨,说明半连接队列满了。原因是当时有个促销活动,瞬时并发连接数暴涨,而tcp_max_syn_backlog还是默认的1024。调大之后问题缓解。

但这里有个坑:调大backlog之后,还要确认内核的somaxconn也够大,因为半连接队列的实际大小受多个参数影响。另外,如果服务端开启了tcp_syncookies,在队列满的时候会用cookie机制处理,不直接丢弃,但cookie机制有性能开销,不能完全依赖它。

5.2 客户端端口耗尽与TIME_WAIT的关系

另一个坑是客户端端口耗尽。有一台机器作为客户端频繁调用下游服务,短连接模式,结果报"Cannot assign requested address"。查下来是TIME_WAIT状态的连接太多,把本地端口占满了。

Linux默认本地端口范围是net.ipv4.ip_local_port_range,通常是32768到60999,大概28000个端口。如果每秒建立几千个短连接,60秒的TIME_WAIT就能把端口耗尽。

解决方案有几个:改用长连接、开启tcp_tw_reuse、扩大端口范围。我们最终改成了连接池,因为长连接不仅解决端口问题,还省去了握手开销,一举两得。

5.3 服务端主动关闭引发的连锁反应

还有一个比较隐蔽的坑:服务端配置了较短的keepalive_timeout,导致服务端频繁主动关闭连接。这本身没问题,但关闭时如果客户端正好在发数据,就会收到RST,客户端可能报"Connection reset by peer"。

这种问题的排查思路是:先确认谁主动关闭(看抓包谁先发FIN),再检查keepalive配置是否合理。一般来说,让客户端主动关闭更安全,因为客户端能更好地处理关闭时机。但具体要看业务场景,没有绝对的对错。

6. 参数调优:让握手挥手不再成为瓶颈

6.1 服务端调优清单

针对高并发服务端,我通常会检查这几个参数:

# 半连接队列 net.ipv4.tcp_max_syn_backlog = 8192 # 全连接队列 net.core.somaxconn = 8192 # SYN+ACK重传次数,默认5次,可适当降低加快失败感知 net.ipv4.tcp_synack_retries = 2 # 开启syncookies,队列满时兜底 net.ipv4.tcp_syncookies = 1 # TIME_WAIT相关 net.ipv4.tcp_tw_reuse = 1 net.ipv4.tcp_max_tw_buckets = 262144 # FIN_WAIT_2超时,防止对方不关连接 net.ipv4.tcp_fin_timeout = 30

这些参数不是越大越好,要根据实际连接数和内存来定。比如tcp_max_tw_buckets设太大,TIME_WAIT积累过多会占内存;设太小,超过后连接直接被关闭,可能影响正常通信。

6.2 应用层能做的事比内核更多

内核参数是兜底,真正解决问题往往要靠应用层。几个实用建议:

使用连接池。不管是数据库连接、HTTP调用还是Redis,连接池都能大幅减少握手挥手次数。这是性价比最高的优化。

合理设置keepalive。长连接不是越长越好,要根据业务特点设置。比如内部服务调用可以设长一点,面向用户的接口要考虑负载均衡器的超时时间。

监控队列指标。ss -lnt看Recv-Q和Send-Q,netstat -s看溢出计数,这些指标要纳入监控,提前发现问题。

考虑使用TCP Fast Open。TFO允许在SYN包里携带数据,减少一次往返。但需要客户端和服务端都支持,且中间设备不能干扰。实际部署中兼容性一般,要谨慎评估。

6.3 调优前后的验证方法

调完参数不能拍脑袋说好了,要有验证。我通常这样做:

  1. 用ss -s看整体socket统计,对比调优前后
  2. 用netstat -s看各计数器变化,特别是drop和overflow
  3. 压测工具模拟高并发,观察连接建立成功率和延迟
  4. 抓包确认握手流程正常,没有异常重传

只有数据证明有效,调优才算完成。我见过太多人改完参数就不管了,结果问题换个形式又出现。

7. 几个容易被问倒的延伸问题

7.1 SYN Flood攻击的原理和防御

SYN Flood就是利用三次握手的机制:攻击者大量发送SYN但不完成第三次握手,服务端半连接队列被占满,正常用户无法建立连接。防御手段包括:

  • SYN Cookies:不立即分配资源,而是把连接信息编码到序列号里,收到ACK再验证。开启tcp_syncookies即可。
  • 缩短SYN+ACK重传超时:让半连接更快释放。
  • 限制单IP连接数:用iptables或应用层限流。
  • 部署专业防护设备:在流量入口做清洗。

SYN Cookies虽然好用,但它会丢失一些TCP选项信息(比如窗口缩放),在极端情况下可能影响性能。所以它是兜底手段,不能替代正常的队列调优。

7.2 为什么有时候抓包看不到三次握手

如果你在服务端抓包,但看不到完整的SYN、SYN+ACK、ACK,可能有几个原因:

  • 抓包点不对:比如在容器里抓,但流量走的是宿主机网卡。
  • 连接复用了:长连接已经建立,后续请求不会再有握手。
  • 中间有代理:代理可能自己完成了握手,和后端是另一条连接。
  • 抓包过滤太严:只抓了特定端口或协议,漏掉了握手包。

排查这类问题,我一般先用tcpdump -i any抓所有接口,确认流量路径,再逐步缩小范围。

7.3 四次挥手能不能变成三次

理论上可以,如果被动关闭方收到FIN后没有数据要发,可以把ACK和FIN合并成一个包,变成三次挥手。但实际中很少见,因为大多数场景下被动关闭方总有一些收尾工作要做。

另外,TCP还有同时关闭的情况:双方同时发FIN,各自回ACK,流程和四次挥手类似,但状态转换不同。这种情况更少见,但理解它有助于你真正掌握TCP状态机。

8. 我个人的排查工具箱和习惯

最后分享几个我平时排查握手挥手问题的习惯,都是踩坑踩出来的。

第一,先看状态分布。ss -s和netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a, S[a]}'能快速告诉你当前连接处于哪些状态。如果TIME_WAIT特别多,就往主动关闭方向查;如果SYN_RECV多,就往半连接队列查。

第二,抓包要抓全。不要只抓一个方向,-i any或者同时抓两端,才能看清完整交互。我吃过只抓服务端的亏,以为是客户端没发ACK,结果发现是服务端的SYN+ACK根本没出去。

第三,改参数前先记录基线。调优不是玄学,要有数据支撑。我习惯把调优前的ss -s、netstat -s、连接成功率、P99延迟都记下来,改完再对比。没有对比就没有发言权。

第四,理解业务比理解协议更重要。同样是TIME_WAIT多,短连接服务和高频交易系统的处理方式完全不同。协议是死的,业务是活的,脱离业务谈调优都是耍流氓。

这些经验不一定适用于所有场景,但至少能让你在遇到问题时有个清晰的排查方向,而不是对着满屏的报文发呆。TCP这东西,看再多不如抓一次包,抓一次包不如真正解决一次线上问题。希望这篇内容能帮你少走点弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 15:57:05

轻量插件串起知识管理全流程:采集、加工、检索到输出

先聊个真实场景。周五下午&#xff0c;方法论文档收了三份&#xff0c;代码片段攒了十几条&#xff0c;邮箱里还躺着两篇长文没读。周一要交方案&#xff0c;你想从资料库里找一条半年前记过的思路&#xff0c;翻了半天目录&#xff0c;愣是没找到。这种时刻你就会意识到&#…

作者头像 李华
网站建设 2026/10/21 15:56:57

开题报告与答辩PPT双效工具:Paperzz硕士开题实操指南

开题季又到了。每年这个时候&#xff0c;实验室里都是一片兵荒马乱的景象&#xff0c;左手开题报告&#xff0c;右手答辩PPT&#xff0c;导师催、格式严、内容杂&#xff0c;两头夹击之下&#xff0c;熬夜熬夜再熬夜几乎是硕士生开题的标配。我见过太多同学晚上十点坐在工位前&…

作者头像 李华
网站建设 2026/10/21 15:55:40

LiteLLM统一接入层:一个接口搞定所有大模型API

常年跟大模型 API 打交道的人&#xff0c;多半经历过这种崩溃瞬间&#xff1a;早上还在用 A 厂商的接口&#xff0c;下午客户说预算超标要换 B 厂商&#xff0c;你硬着头皮把刚写的调用代码里里外外改一遍&#xff0c;换完发现错误码格式不同、流式解析逻辑要重写、连 token 计…

作者头像 李华
网站建设 2026/10/21 15:54:26

STM32F103 USART2奇偶校验配置实战:工业RS-485通信硬性约束解析

简介&#xff1a;本资源是一份面向嵌入式初学者与STM32开发者的实操型工程代码包&#xff0c;聚焦STM32F103C8T6单片机串口2&#xff08;USART2&#xff09;的奇偶校验通信实现&#xff0c;解决工业通信中数据可靠性增强的关键需求&#xff0c;适用于传感器数据校验、调试通信容…

作者头像 李华
网站建设 2026/10/21 15:54:06

边缘设备实时图像增强与目标检测:从数据到NPU部署的实战解析

最近接手了一个内部代号叫“rea”的视觉处理项目&#xff0c;从需求评审到最终落地部署前后折腾了将近两个月。趁热打铁把整个过程梳理一篇&#xff0c;给后面做类似图像处理、目标识别场景的朋友一个参考。特别是那些打算从零搭一套轻量级检测流程、又不想一上来就堆大模型的团…

作者头像 李华