TCP 为什么是3次握手,而不是2次或4次?

📅 2026/8/6 14:26:47
TCP 为什么是3次握手,而不是2次或4次?
一、三次握手面试场上的照妖镜技术面试里有一道题堪称程序员界的你有没有对象——不管你简历写得多花哨面试官总要不经意地问一句“TCP 为什么是三次握手”十个人里有九个会条件反射地背出“为了确认双方的收发能力”面试官笑而不语。因为这个答案只答对了一半甚至可以说只摸到了皮毛。真正让面试官眼前一亮的答案藏在一个更本质的问题里TCP 运行在一个完全不讲道理的网络之上。IP 协议是什么脾气搞网络的都懂——尽力而为Best Effort说白了就是我尽量帮你送送不送得到、送到的顺序对不对、送不送重复了概不负责。数据包会丢、会重传、会抄近道插队导致乱序、会在某个路由器里堵车堵到地老天荒才姗姗来迟。TCP 的使命就是在这么一条三天两头翻车的破路上硬生生给你修出一条可靠的双向高速公路。而三次握手就是这条高速公路开工前双方对暗号、验身份、防诈骗的第一道工序。搞懂了这个大前提你才能明白两次握手为什么是引狼入室四次握手又为什么是脱裤子放屁。今天咱们就把这事儿从骨头缝里给它扒开了讲。二、为什么两次握手绝对不行2.1 教科书答案的漏洞大多数人的理解停留在“两次握手不行是因为客户端没法确认服务器的接收能力。”这话不算错但格局小了。真正让 RFC 793 的设计者们眉头一皮的核心原因是一个听起来更玄乎、实则朴素得很的问题如何防止一个迟到的历史连接请求把服务器的资源白白骗走这个问题在协议设计里有个专门的名字叫防止历史重复连接的建立Prevent Old Duplicate Connections。2.2 迟到三年的求婚信咱们打个比方。小王给小张寄了一封信信里写着我要向你求婚请回信告诉我你的意愿。这封信因为邮局的分拣机故障被压在一个麻袋底下整整耽误了三年。三年里小王早就心灰意冷认为这段感情黄了转头娶了隔壁村的小李日子过得红红火火孩子都会打酱油了。结果三年后的某一天压箱底的那封求婚信终于寄到了小张手上。如果按两次握手的逻辑来处理这件事——小张一看信二话不说直接开始装修婚房、置办嫁妆也就是说只要收到求婚的请求服务器小张立刻就分配资源、进入已建立连接状态那会是什么后果小张兴冲冲地把新房装修完才发现小王早就是别人的丈夫了。这个婚房也就是服务器为这个连接开辟的内存缓冲区就这么白白浪费了还没人来收拾这个烂摊子。这就是两次握手的死穴它无法区分这是一个当下真实发起的连接请求还是一个在网络里堵车堵了半天、姗姗来迟的历史包。2.3 僵尸连接是怎么产生的我们把这个比喻换成协议报文过程是这样的客户端在T0时刻发出一个SYN(Seq100)请求建立连接。但这个包在网络中因为路由拥堵迟迟没有送达这在真实网络里太常见了尤其是弱信号的无线链路。客户端等待超时判定这次连接失败主动放弃可能过了几秒后又发起了一次新的连接一切正常地完成了三次握手数据传完连接也正常关闭了。就在此时那个迟到的旧SYN(Seq100)包姗姗来迟终于晃悠悠地抵达了服务器。如果是两次握手的世界服务器一看到SYN想都不想立刻回复ACK并且直接进入ESTABLISHED已建立状态分配好接收缓冲区、发送缓冲区一整套内存资源就位。问题来了发这个旧包的客户端压根不知道自己曾经发过这个包更不会给服务器回任何数据。服务器就这么傻乎乎地守着一个没人认领的连接白白占着内存和端口资源直到内核的保活机制Keepalive判定超时才肯把它清理掉。这就是典型的僵尸连接一旦有网络攻击者故意伪造大量这种迟到的旧连接请求服务器的连接资源池分分钟被打穿这就是一种资源耗尽型攻击的雏形。而三次握手的世界服务器收到这个旧SYN(Seq100)依然会回复一个SYNACK(Ack101)但这只是一个半开连接SYN_RCVD状态并没有真正分配完整资源也没有对上层应用可见。这个SYNACK会发回给客户端。而客户端呢它压根没指望还能收到这个三年前的回信一看这个Ack序号对应的是自己早已经作废的旧连接立刻明白这不是我现在的连接这是个过期的幽灵。于是它会毫不留情地甩回一个RSTReset报文直接把服务器这边的半连接就地正法。三次握手的精髓就在这临门一脚的验证上只有客户端亲口确认了这次握手是当下发起的服务器才敢真正地把资源交出去。两次握手少的正是这最后一次回执确认导致服务器只能听一面之词就当真这才是设计上真正致命的漏洞。三、为什么四次握手又是多此一举搞懂了两次握手为什么太少咱们再看看反方向的问题既然要确认双方的收发能力这件事听起来这么复杂那干脆各自独立地问一遍、各自独立地答一遍凑够四次是不是更严谨我们先把这个逻辑上严谨的四次握手方案摆出来看看客户端 → 服务器SYN(Seqx)意思是我要给你发数据了我这边的起始序号是 x。服务器 → 客户端ACK(x1)意思是收到你的 x 我确认了。服务器 → 客户端SYN(Seqy)意思是我也要给你发数据了我这边的起始序号是 y。客户端 → 服务器ACK(y1)意思是收到你的 y 我也确认了。这四步逻辑上确实一步都不少每一句话都说得明明白白。但工程设计有一条朴素的原则叫能省一次交互就绝不多跑一趟——毕竟每一次网络往返都是实打实的时间成本。这时候就该奥卡姆剃刀登场了如无必要勿增实体。3.1 把两句话攒成一句话说仔细观察上面四步中的第 2 步和第 3 步你会发现一个巧合这两步都是服务器发给客户端的而且几乎是前脚跟后脚同时产生的——服务器一边要确认收到了你的连接请求一边自己也要发起我也要跟你建立反向连接。这两件事服务器完全可以在同一个 TCP 报文里一次性说完压根不需要分两趟跑腿。就像你去银行办业务柜员一边给你盖章确认您的申请我收到了一边顺手把我这边需要您补充的材料清单也一并递给你而不是先盖完章让你出去再把你重新叫回来给材料清单——这不是脱裤子放屁吗这种把两个动作合并到一个报文里发出去的技巧在协议设计里有个专门的术语叫捎带应答Piggybacking。具体到 TCP 头部就是把ACK标志位和SYN标志位同时置为 1塞进同一个 TCP 报文头里发出去业内都管这个报文直接叫SYNACK。于是原本逻辑上的四步交互因为第 2 步和第 3 步在时间点上完全重合、没有任何需要中间等待的业务逻辑被天然地压缩成了三次物理交互客户端 → 服务器SYN(Seqx)服务器 → 客户端SYN(Seqy), ACK(x1)这一步身兼二职客户端 → 服务器ACK(y1)这就是三次握手的由来——它不是三这个数字本身有什么魔力而是确认双方收发能力最少需要交换的信息量再加上能合并的报文一定要合并这两条原则叠加之后自然收敛出来的最优解。三次是理论上能把事情说清楚的最少次数一次都不能再省了。四、TCP 三次握手的精细化交互过程光说不练假把式咱们把整个过程用状态机的方式画出来把每一个报文的标志位、序号计算方式都标注清楚。客户端 (Client) 服务器 (Server) 状态: CLOSED 状态: LISTEN | | | SYN, Seqx | | -------------------------------------- | 状态: SYN_SENT 状态: SYN_RCVD | | | SYN, ACK, Seqy, Ackx1 | | -------------------------------------- | 状态: ESTABLISHED | | | | ACK, Seqx1, Acky1 | | -------------------------------------- | | 状态: ESTABLISHED | | | (双向数据传输开始) |4.1 SYN 报文不带数据为什么也要消耗一个序列号很多人第一次看到Ackx1这个计算方式会犯嘀咕SYN 报文压根没有携带任何应用层数据凭什么它也要在序号空间里占一个坑导致后续的Ack要 1答案在于TCP 的可靠性传输机制是靠确认号来保证每一个字节都被对方收到的。而SYN标志位本身虽然不携带业务数据但它承载着建立连接这个动作本身的语义——这个动作也需要被可靠地确认万一SYN包丢了呢如果它不占用序号接收方就没办法通过Ack号明确告诉发送方你的这个建立连接的请求我确认收到了。所以协议设计者干脆规定SYN和FIN断开连接的标志位虽然不带数据但各自都要消耗一个序列号这样它们才能被纳入 TCP 统一的可靠性确认体系里和普通数据字节享受同等待遇。4.2 第三次握手的 ACK 包能不能捎带业务数据这也是个经典的送命题——很多人下意识地觉得握手阶段就该老老实实地握手业务数据要等握手完全结束之后才能发。但仔细想想状态机当客户端发出第三次握手的ACK包时它自己的状态已经从SYN_SENT跳转到了ESTABLISHED——也就是说对客户端而言连接在它发出这个 ACK 的那一刻就已经算是建立完成了。既然连接都建立了那这个ACK包顺便夹带一些业务数据比如 HTTP 请求的第一行或者 MQTT 的 CONNECT 报文当然是允许的而且在实际工程里也确实是这么干的能省一次往返就是一次往返。不过这里有个小小的不对称服务器这边要等到收到这个第三次握手的包之后才会从SYN_RCVD跳转到ESTABLISHED。所以严格来说只有客户端能在第三次握手时顺路捎带数据服务器则必须老老实实等这一步完成之后才能确认连接、才能开始收数据——这也是为什么半连接队列和全连接队列是两个独立的资源池下面案例二会细讲。五、TCP 握手在工程实践中的血泪史原理讲完了接下来讲点真金白银踩出来的坑。这两个案例一个是客户端视角一个是服务端视角都是在真实的现场设备和高安全级别专网设备上摸爬滚打出来的经验。1那台闹鬼的车载网关这事儿说出来现在都觉得离谱。当年我们有一批网关设备装在货运车队的车头里靠 4G 专网跟后台报位置、报状态。项目验收前的路测阶段客户那边天天给我打电话语气一次比一次冷“你们这设备是不是有问题跑到山区一进隧道、一进信号差的地界主板直接重启跟见了鬼似的。”我第一反应跟大部分人一样肯定是硬件的锅。电压不稳还是某个电容选型有问题我们把那批闹鬼的板子全部换了一遍甚至专门挑了几块送去做高低温测试、做振动测试一切正常。可只要一拉到信号差的野外闹鬼现象照样发作跟设备本身好像没啥关系倒像是跟信号差这三个字过不去。我带着示波器和一台笔记本跑去山区蹲了整整两天现场。终于抓到一次现行设备一进隧道信号格数从满格掉到一格紧接着connect()这个函数就跟被点了穴一样死死地卡在那儿不动了。我盯着日志一秒一秒地数1 秒、2 秒、4 秒、8 秒……卡了差不多快一分钟主板啪一声自己把自己重启了。回办公室翻内核文档我才后知后觉地一拍大腿这压根不是硬件问题是我们自己给自己挖的坑。Linux 内核对SYN包的重传走的是指数退避策略——发不出去等 1 秒重试再不行等 2 秒再等 4 秒、8 秒……一路翻倍下去默认策略下connect()这一个函数调用硬是能把你晾在原地卡上一分钟。而我们的应用程序里connect()用的还是最傻白甜的阻塞式调用——一旦调用整个主线程直接被焊死在那儿非得等到连接成功或者彻底超时才肯往下走。这一分钟里主线程压根抽不出空去执行喂狗这个动作也就是没法按时告诉硬件看门狗我还活着别把我毙了。看门狗左等右等等不到信号理所当然地认定主程序已经死透了二话不说直接把整块主板拉闸重启。说白了这台设备不是被信号差弄死的是被自己内核里那套沉默的重试机制活活给闷死的。后来我们改了两处一是把connect()全部换成非阻塞模式配合select做超时轮询喂狗的动作可以在轮询间隙里见缝插针地执行不再被网络层的重试策略绑架二是主动把内核的tcp_syn_retries调小把最长等待时间收敛到几秒钟以内而不是任由它按默认策略死等一分钟。改完之后那批设备再也没闹过鬼。2一场连接海啸引发的深夜告警还有一次是某个物联网项目上线不久凌晨接到的告警电话睡意瞬间清醒。现场情况是那片区域的基站因为突发停电断了三个多小时挂在这个基站下面的一万多台终端设备全部离线。电力抢修完成、基站一恢复供电问题也就跟着来了——这一万多台设备几乎是在同一秒钟里铆足了劲一起往网关服务器发起重连。网关那边的反应用假死两个字形容都算客气。日志里刷屏一样地报Connection reset by peer运维那边反馈说不但新设备接不进来连之前还在线的一些老连接都开始莫名其妙地掉线整个后台一片鸡飞狗跳。我们连夜抓包排查抓包的结果让人心里咯噔一下服务器压根没有回复大部分设备的SYN请求连一个拒绝的信号都没给就这么悄无声息地把包给吞了。这就是典型的半连接队列被瞬间打爆——服务器处理连接的时候内部维护着两个队列一个是半连接队列专门存放已经收到SYN、已经回了SYNACK还在等第三次握手确认的连接另一个是全连接队列存放三次握手已经彻底走完、就等应用程序来取的连接。一万多台设备在同一时刻疯狂涌入半连接队列瞬间见底队列一满内核对后续新到的SYN包默认策略就是不声不响地直接丢弃既不回应也不报错。客户端那头完全被蒙在鼓里只当是网络丢包了于是老老实实地按重传机制继续傻等、继续重发——这下可好等于是往已经着火的房子里又浇了一桶油重传风暴越滚越大。那天夜里我们干了三件事第一把半连接队列和全连接队列的上限狠狠调大也就是内核参数tcp_max_syn_backlog和somaxconn同时把应用程序listen()调用里的backlog参数也一并调上去给这种突发洪峰留足缓冲空间第二把tcp_syncookies开关打开让内核在半连接队列快满的时候不再依赖队列本身去记住每一个半连接的状态而是把连接的关键信息编码进返回的SYNACK序列号里等第三次握手的ACK一回来反向解码验证一下就能确认连接的真伪全程不占用队列里的一分内存第三也是后来我们复盘时觉得最治本的一招——在设备固件里加了一段随机延迟重连的逻辑断电恢复后每台设备各自随机等个几秒到几十秒不等再发起连接硬生生把这波万军齐发的海啸从时间线上摊薄成了细水长流。那次之后团队里流传出一句话代码逻辑是不是严谨平时看不出来真正一考验人的永远是那种所有设备在同一秒钟一起做同一件事的极端场景。六、越底层的设计越优雅的哲学聊到这儿咱们回过头来看 TCP 三次握手这件事会发现它压根不是一个孤立的、拍脑袋定下来的数字。它是在IP 网络完全不可靠这个残酷的大前提下协议设计者们用最经济的三次报文交换同时解决了三件事验证双方的收发能力是否正常、防止历史上迟到的旧连接被误认成新连接从而骗走资源、并且在协商过程里顺带确定好双方的初始序列号让后续的可靠传输有一个干干净净的起点。两次握手省掉了最后一次回执确认看似省了事实则给了历史重复连接可乘之机是一种图快不图稳的偷懒。四次握手看似逻辑上无懈可击实则忽略了两个报文完全可以合并发送的客观条件是一种为了严谨而严谨的形式主义。唯有三次恰到好处地卡在了够用和不浪费之间的那个平衡点上。这也是我一直想跟大家分享的一个工程师心态面对任何一个看似背下来就完事的技术八股文都值得多问自己一句——这背后的第一性原理是什么它究竟解决了什么样的现实约束少一次会怎样多一次又图什么只有真正把这些问题在脑子里过一遍、甚至在现场故障里踩过坑这个知识点才算真正长在了你自己身上而不是浮在嘴皮子上的一句空话。下次再有人问你为什么是三次握手你大可以慢悠悠地跟他讲讲小王那封迟到三年的求婚信——保管让对方对你刮目相看。