1. 从一个抓包现场说起为什么三次握手值得反复讲很多人第一次接触TCP三次握手都是在教科书上看到那三行经典的箭头图SYN、SYNACK、ACK。背下来不难但真到了排查线上问题的时候比如连接建立慢、偶发连接超时、服务端半连接队列被打满光靠背那三行是解决不了问题的。我自己就遇到过好几次类似的情况最后定位下来根子都在握手阶段的某个细节上。这篇文章想做的事情很明确把TCP三次握手从背概念拉到能上手验证的层面。我会先讲清楚它到底解决了什么问题、为什么非得是三次而不是两次或四次然后带你用抓包工具实际看一遍握手过程再深入到半连接队列、SYN Flood防护、握手超时参数这些工程里真正会踩的坑。不管你是刚学网络的新手还是已经工作几年但没系统梳理过这块的开发者看完应该都能有收获。需要提前说明的是握手本身是协议规范层面的东西但不同操作系统在实现细节上差异很大比如Linux和Windows对半连接队列的处理、重传次数的默认值都不一样。文中涉及具体参数的地方我会以Linux为主来说明因为这是服务端最常见的部署环境其他系统会做对比提示。2. 三次握手到底在解决什么问题2.1 先搞清楚TCP要保证什么要理解三次握手得先知道TCP这个协议给自己定了哪些目标。它要提供的是面向连接的、可靠的、基于字节流的传输服务。这三个词拆开看每一个都对握手提出了要求。面向连接意味着通信双方在传数据之前必须先建立一个双方都认可的连接状态。这个状态不是单方面说了算的A认为连接建立了B也必须认为连接建立了否则A发出去的数据B可能根本不认。可靠意味着数据不能丢、不能乱序、不能重复而要做到这一点双方必须同步一个起始的序列号后续所有数据的确认、重传都基于这个序列号展开。基于字节流则意味着TCP不关心你上层发的是多大的块它只认字节所以序列号的同步尤其关键。把这三点合起来看握手阶段的核心任务其实就两个确认双方的收发能力都正常以及同步双方的初始序列号ISN。三次握手的设计本质上就是为了用最少的报文数同时完成这两件事。2.2 为什么两次不够四次又多余这是面试里被问烂的问题但真正理解的人不多。我用一个生活化的类比来说你打电话给朋友想确认我能听到你你也能听到我。第一次你说喂能听到吗——这时候你只知道自己的话筒是好的不知道对方能不能收到也不知道对方的听筒和话筒是否正常。第二次朋友回能听到你呢——这时候朋友确认了自己的听筒正常收到了你的话也确认了自己的话筒正常能回话同时你确认了自己的话筒正常你的话被收到了和自己的听筒正常收到了回复。但注意朋友还不知道你能不能收到他这句回复。第三次你说我也能听到——这时候朋友才确认自己的话筒真的把声音传过去了。如果只有两次朋友那边就无法确认我发出去的话对方到底收到没有。放到TCP里就是服务端无法确认客户端收到了自己的SYNACK也就无法确认客户端具备正常的接收能力更无法确认客户端已经同步了自己的序列号。那为什么不用四次因为第二次和第三次可以合并。服务端在回复SYNACK的时候既确认了客户端的SYNACK部分又发出了自己的SYN一个报文干了两件事。客户端收到后只需要再发一个ACK确认服务端的SYN即可。所以理论上的四次被压缩成了三次这是协议设计上的精简。这里有个容易混淆的点三次握手完成的是连接建立但连接建立后双方并没有立刻开始传数据。客户端发完最后一个ACK之后如果马上有数据要发会直接捎带在这个ACK里这叫捎带确认如果没有数据就等应用层触发。2.3 初始序列号为什么不能从0开始很多人会想序列号从0开始多简单为什么非要搞个随机数这里涉及两个层面的考虑。第一是安全性。如果ISN可预测攻击者就能伪造出看起来合法的报文插入到连接里甚至劫持整个会话。随机化ISN让这种伪造变得困难。第二是避免旧连接的残留报文干扰新连接。网络里可能存在上一次连接延迟送达的报文如果新连接的ISN和旧连接一样这些旧报文就可能被误认为是新连接的数据。随机化ISN大大降低了这种碰撞概率。现代操作系统生成ISN的方式通常是基于一个全局时钟加上连接四元组的哈希保证既随机又在一定时间内单调递增。RFC 793里建议ISN每4微秒加1这样大约4.55小时会回绕一次这个周期足够长能有效避免新旧连接序列号重叠。3. 抓包实测把三次握手看得明明白白3.1 环境准备与工具选择光讲理论没意思我们直接抓一次真实的握手看看。工具方面我推荐用tcpdump配合Wiresharktcpdump负责在服务器或客户端上抓包存成pcap文件Wireshark负责图形化分析。如果你只是想快速看一眼直接用Wireshark的图形界面抓也行但生产环境通常没有图形界面所以tcpdump是必备技能。假设我在一台Linux机器上想抓一次访问某个HTTP服务的握手过程。命令大概是这样sudo tcpdump -i eth0 -nn -s 0 -w handshake.pcap tcp port 80 and host 192.168.1.100参数逐个解释一下这些细节很多人抓包时容易忽略-i eth0指定网卡不指定的话tcpdump会选一个默认的可能抓不到你要的流量。-nn不解析主机名和端口名直接显示IP和数字端口。解析会拖慢抓包速度还可能因为DNS查询引入额外流量。-s 0抓取完整报文不截断。老版本tcpdump默认只抓68字节握手报文虽然短但为了看选项字段还是抓全比较稳妥。-w handshake.pcap写入文件而不是打印到屏幕方便后续用Wireshark分析。最后的过滤表达式只抓80端口且和指定IP相关的TCP流量避免抓到一堆无关的包。抓完之后把pcap文件拉到本地用Wireshark打开过滤tcp.flags.syn 1就能快速定位到握手报文。3.2 逐帧拆解三个报文打开Wireshark找到第一个SYN报文展开TCP协议树你会看到一堆字段。我把关键字段列出来对照着看字段第一个SYN客户端→服务端第二个SYNACK服务端→客户端第三个ACK客户端→服务端Source Port随机高位端口如54321服务端口如8054321Destination Port805432180Sequence Number客户端ISN如1000服务端ISN如50001001Acknowledgment Number0无效10015001FlagsSYN1SYN1, ACK1ACK1Window Size客户端接收窗口服务端接收窗口客户端接收窗口OptionsMSS, SACK, Window Scale等同左通常无这里有几个细节值得展开说。序列号的变化第一个SYN里客户端说我的起始序列号是1000。注意SYN报文本身会消耗一个序列号所以客户端下一次发送数据时序列号从1001开始。第二个报文里服务端确认号是1001意思是我收到了你的SYN序列号1000我期待你下一个字节是1001。同时服务端说我的起始序列号是5000。第三个报文里客户端确认号是5001同理。选项字段第一个SYN里通常会带几个选项最重要的三个是MSS最大报文段长度、SACK Permitted选择性确认和Window Scale窗口缩放。MSS告诉对方我这边能接收的最大TCP载荷是多少通常是MTU减去IP头和TCP头以太网环境下一般是1460。Window Scale是因为TCP头里的窗口字段只有16位最大65535字节在高带宽高延迟网络里不够用所以用这个选项做位移扩展。为什么第三个ACK通常没有选项因为选项的协商在SYN阶段就完成了第三个ACK只是确认不需要再带选项。不过如果客户端有数据要发会捎带数据一起发出去。3.3 用命令行快速验证握手状态不是每次都有条件开Wireshark有时候在服务器上排查问题用几个命令就能看个大概。查看当前所有TCP连接状态ss -tanss比老的netstat快很多尤其在连接数多的时候。输出里的State列会显示ESTAB已建立、SYN-SENT客户端发了SYN等回复、SYN-RECV服务端收到SYN回了SYNACK等最后确认等状态。如果你看到大量SYN-RECV说明有很多连接卡在握手最后一步可能是客户端不回ACK也可能是网络丢包。统计各状态的连接数ss -tan | awk NR1 {print $1} | sort | uniq -c | sort -rn这条命令能快速告诉你当前哪种状态的连接最多。如果SYN-RECV数量异常高就要警惕是不是有人在扫端口或者发起SYN Flood了。4. 半连接队列与全连接队列握手背后的工程细节4.1 两个队列是怎么工作的这是三次握手里最容易被忽视、但在生产环境里最容易出问题的地方。服务端收到第一个SYN之后并不会立刻为这个连接分配完整的资源而是先把它放进半连接队列也叫SYN队列同时回复SYNACK。等收到客户端的第三个ACK才把这个连接从半连接队列移到全连接队列也叫Accept队列等待应用层调用accept取走。为什么要分两个队列核心是为了防御SYN Flood攻击。如果服务端收到SYN就分配完整资源攻击者只要疯狂发SYN但不回最后的ACK就能瞬间耗尽服务端资源。有了半连接队列服务端可以用较小的成本先挂起这些未完成的连接等真正完成握手再分配更多资源。Linux里这两个队列的大小控制参数net.ipv4.tcp_max_syn_backlog半连接队列的最大长度。net.core.somaxconn全连接队列的最大长度同时受应用层listen时传入的backlog参数影响取两者较小值。查看当前值sysctl net.ipv4.tcp_max_syn_backlog sysctl net.core.somaxconn4.2 队列满了会发生什么半连接队列满了之后服务端默认会丢弃新的SYN客户端会触发超时重传。如果开启了net.ipv4.tcp_syncookies服务端在队列满时会启用SYN Cookie机制不把连接放进队列而是把状态编码进序列号里等客户端回ACK时再还原。SYN Cookie能有效抵御SYN Flood但它也有代价因为不保存连接状态一些TCP选项比如Window Scale就无法协商可能影响性能。全连接队列满了之后服务端的行为取决于net.ipv4.tcp_abort_on_overflow参数。默认是0表示服务端会忽略客户端的ACK让客户端以为连接建立了但实际上服务端并没有把连接交给应用层后续数据发过来会被丢弃或重置。如果设成1服务端会直接回RST客户端立刻知道连接失败。生产环境一般保持默认0因为直接RST对客户端体验更差而忽略ACK至少给了应用层一点缓冲时间。实操心得如果你发现服务端有大量连接建立后立刻被重置或者客户端报connection reset by peer先别急着怀疑代码去看看全连接队列是不是满了。用ss -lnt看Listen队列的Send-Q如果Recv-Q接近Send-Q基本就是队列满了。4.3 队列大小的调优经验队列大小不是越大越好。半连接队列太大会占用更多内存而且在遭受攻击时反而给了攻击者更多挂起连接的空间。全连接队列太大会导致连接在队列里等待时间过长应用层来不及处理用户体验变差。我的经验是半连接队列保持默认或适度调大即可重点是把SYN Cookie打开全连接队列要根据应用层的处理能力来定一般设置为应用层每秒能处理的连接数的1到2倍。比如你的服务每秒能accept 1000个连接那somaxconn设成2000左右比较合适。另外要注意应用层listen的backlog参数如果小于somaxconn实际生效的是backlog。很多框架默认backlog是128或511在高并发场景下明显不够需要显式调大。5. 握手异常与常见问题排查5.1 握手超时和重传客户端发出SYN后如果没收到SYNACK会触发重传。Linux下由net.ipv4.tcp_syn_retries控制重传次数默认是6次。重传间隔是指数退避的第一次1秒第二次2秒第三次4秒以此类推。6次重传加起来大约127秒也就是说客户端要等两分多钟才会彻底放弃。服务端回复SYNACK后如果没收到ACK也会重传由net.ipv4.tcp_synack_retries控制默认是5次。这个值比客户端的小是因为服务端要尽快释放半连接队列的空间。排查握手超时第一步是确认是单向不通还是双向不通。如果客户端能发出SYN但收不到回复可能是服务端没监听、防火墙拦截、或者路由问题。如果服务端收到了SYN也回了SYNACK但客户端收不到那问题多半在回程路径上。5.2 常见问题速查表现象可能原因排查方法连接建立慢但最终能成功首次SYN丢包触发重传抓包看是否有重传的SYN检查网络质量大量SYN-RECV状态半连接队列满或遭受SYN Floodss -tan统计状态检查syncookies是否开启连接建立后立刻断开全连接队列满服务端忽略ACK检查somaxconn和listen backlog客户端报connection refused服务端端口未监听或回了RST确认服务端进程状态和防火墙规则偶发握手失败重试就好网络抖动或中间设备丢包抓包分析丢包位置检查MTU是否匹配5.3 几个容易踩的坑坑一只调了somaxconn没调应用层backlog。前面说过实际生效的是两者较小值。很多人改了系统参数却发现没效果就是因为应用层没改。坑二以为开了SYN Cookie就万事大吉。SYN Cookie在队列满时才生效而且会牺牲部分TCP选项的协商。如果攻击流量不大但持续队列一直不满SYN Cookie根本不触发这时候还是得靠调大队列或上专业防护。坑三忽略MTU和MSS的影响。如果路径MTU不匹配握手报文虽然小能通过但后续大数据包会被丢弃表现为连接能建立但传不了数据。这种情况要在握手阶段就通过MSS协商来规避必要时调整网卡的MTU。坑四在容器环境里调参数调了个寂寞。容器的网络命名空间是独立的你在宿主机上改的sysctl不一定对容器生效。要么在容器启动时通过--sysctl指定要么直接改容器的网络命名空间配置。6. 从握手延伸出去几个值得深挖的方向三次握手只是TCP连接的起点顺着它往下挖还有不少有意思的东西。TFOTCP Fast Open传统握手需要1个RTT才能开始传数据TFO允许在SYN报文里就携带数据把首次数据传输的延迟省掉。它的原理是客户端首次连接时获取一个Cookie后续连接带上Cookie就能直接传数据。不过TFO的部署一直不算广泛中间设备兼容性是主要障碍。握手与TLS的结合HTTPS场景下TCP握手之后还要做TLS握手总共需要多个RTT。TLS 1.3把TLS握手压缩到1个RTT配合TFO理论上能做到接近0延迟。这也是为什么现代CDN和边缘计算节点特别关注握手优化的原因。握手过程的观测除了tcpdumpeBPF也是观测握手的好工具。通过kprobe挂到内核的握手处理函数上可以精确统计每个阶段的耗时比抓包更细粒度。比如统计从收到SYN到发出SYNACK的时间能直接反映服务端的处理压力。我自己在实际排查连接问题时最常用的组合是先用ss看状态分布定位到异常状态再用tcpdump抓包确认具体是哪个报文出了问题最后结合内核参数和应用日志定位根因。这套流程走下来绝大多数握手相关的问题都能找到答案。真正难的不是工具而是对握手每个阶段应该发生什么有清晰的预期这样一旦实际现象偏离预期就能快速锁定问题范围。
阅读完成 · 觉得有帮助?