ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

TCP三次握手深度解析:为什么必须是三次?两次不行四次浪费

TCP三次握手深度解析:为什么必须是三次?两次不行四次浪费 TCP 三次握手这个面试题我见过太多次了也听过太多版本的回答。背过“SYN、SYN-ACK、ACK”这三个词的人很多但你要追问一句“为什么必须是三次两次行不行四次又有什么问题”大半人就卡壳了。这篇我打算把三次握手彻底拆开讲。从它到底在解决什么核心问题到两次握手会在真实网络里造成什么事故再到协议栈里的状态机、半连接队列、SYN洪泛这些衍生话题最后给出一套完整的面试答题框架。目标是让看过这篇文章的人不仅能背标准答案还能真正理解“为什么是这个数字”遇到追问也不慌。内容偏网络基础和开发实践适合面试党、做网络/后端开发的朋友也适合用抓包工具查过连接问题但一直没搞懂原理的人。1. 先把问题问清楚三次握手到底“握”的是什么很多人一提三次握手第一反应是“确认双方都能收发数据”。这个说法大方向没错但太模糊了。如果只是确认收发能力那发一个包就可以了对方回了就是通的为什么非要画一个三段的时序图1.1 握手不是打招呼是双向的“能力确认参数同步”TCP 是面向连接的可靠传输协议。所谓“建立连接”本质上不是建立一条物理链路而是让通信双方就三个东西达成一致双方的序列号Sequence Number起点是多少双方的接收窗口、MSS 等传输参数是多少双方各自准备就绪可以开始收发应用数据。这三件事里最核心的是序列号同步。TCP 是字节流协议每个字节都有一个序号接收方靠序号做去重、排序和确认。如果双方不知道对方的起始序号数据一上来就是乱的。三次握手的目的可以概括成一句话在一个不可靠的网络上用最少的报文往返让双方完成序列号和窗口参数的协商并互相确认“我准备好了你也准备好了”。注意“互相确认”这四个字。TCP 是全双工协议A 要向 B 发数据B 也向 A 发数据。所以连接建立的过程天然是双向的A 需要确认 B 能收、B 需要确认 A 能收同时 A 和 B 都要知道对方的序号起点。这也是为什么“一次握手”和“两次握手”都不可能成立的根源。一次握手A 发了个 SYN 说“我要建立连接”B 收到了但 B 不知道 A 是否收到了自己的回应——因为 A 根本没给 B 回话的机会。两次握手B 收到 SYN 后回了 SYN-ACKB 就认为连接建立了但 A 不一定收到了 SYN-ACK这时候双方的状态就不一致了。后面我会用具体场景演示这种不一致有多致命。1.2 序列号和确认号握手的真正主角要理解三次握手必须先理解 TCP 报头里两个最重要的字段序号Sequence Number简称 SEQ和确认号Acknowledgement Number简称 ACK。客户端发送 SYN 时会携带自己的初始序列号记作 ISN(c)。这个包不携带应用数据但消耗一个序号所以后续实际数据的第一个字节序号是 ISN(c)1。服务端收到后回复 SYN-ACK携带自己的初始序列号 ISN(s)同时把确认号设为 ISN(c)1表示“我收到了你的初始序号”。客户端收到 SYN-ACK再回一个 ACK确认号为 ISN(s)1表示“我也收到了你的初始序号”。三次握手结束后双方手里都握着两个关键数字对方序号起点、自己的序号起点。这就像两个不同的歌手在录音棚对节拍器先各自报出自己的基准拍号听完对方的再校准一次然后才敢合奏。错了任何一步后面的音符全都是乱的。顺便说一句很多人背八股文时会漏掉一个细节第三次握手报文里也可以携带应用数据。因为此时客户端已经确认“服务端能收也能发”所以严格的握手阶段到第三次 ACK 发出时已经宣告结束后续的 ACK 和数据可以一起走。这也是“三次够用、四次浪费”的一个效率论据。2. 为什么不能是两次握手一个“历史报文”就能把连接搅黄网上最常见的解释是“两次握手无法确认客户端收到服务端的 SYN-ACK”这句话表面看有点绕但确实是核心。不过我觉得说服力最强的方式是直接还原一场事故。2.1 用一个具体场景还原两次握手的崩溃现场假设现在没有第三次握手只有 SYN 和 SYN-ACK 两步。时间线和故事是这样的客户端 A 发送一个连接请求 SYN(seq100)要连接服务端 B。这个 SYN 报文很不幸在网络里被某个中间链路卡了很久比如因为负载均衡排队、骨干网拥塞延迟超过了客户端的超时重传时间。客户端 A 等不到回复按 TCP 重传机制重新发了一个新的 SYN(seq900)。这次网络通畅服务端 B 收到后回了 SYN-ACK(seq500, ack901)客户端 A 也正常处理两边愉快地建立了连接传完数据后关闭。看起来一切都正常但问题出在时间点上。如果那个被卡了很久的旧 SYN(seq100) 是连接关闭之后才被网络“吐”出来的呢A 和 B 的连接 1 已经关闭了。此时旧的 SYN(seq100) 终于到达服务端 B。B 一看来了个 SYN按流程返回 SYN-ACK(seq600, ack101)然后 B 认为“连接已经建立”开始为这个连接分配资源、等待 A 发数据。可客户端 A 那边呢A 早就放弃了 seq100 这个连接它的状态机里根本没有这个连接记录。收到一个莫名其妙的 SYN-ACK 后A 会把它当成无效报文直接丢弃甚至回一个 RST 去拆掉它。于是服务端 B 就维护了一条“幽灵连接”占着文件描述符、占着接收缓冲区却永远等不到任何数据直到超时被系统回收。如果这种场景频繁发生服务端的资源会被大量空转的连接拖垮。这就是两次握手最致命的问题服务端无法分辨一个 SYN 请求到底是客户端当前发起的、还是历史残留的。一端认为连接已建立另一端压根不认账状态不一致。2.2 从不可靠信道推导两次握手下服务器永远“心里没底”上面的故事本质上推导出了“不可靠信道上分布式共识”的困境。客户端和服务端之间没有一条绝对可靠、有序、不重复的通道报文可能丢、可能重、可能延迟乱序。在这个前提下两次握手等于只验证了“服务端收到了请求并发了回复”但服务端无法确认自己的回复是否到达了客户端。你可以这么感受一下两次握手的模型里服务端 B 的状态切换是“发了 SYN-ACK 就进 ESTABLISHED”它等于是在赌“客户端一定收到了我的 SYN-ACK”。但这个赌注没有依据——如果 SYN-ACK 在途中丢了客户端会重新发 SYN如果超时了客户端可能已经放弃了。无论哪种情况B 的状态都是盲目的。三次握手把这步赌注去掉了服务端必须收到客户端的第三个 ACK才从 SYN-RCVD 切换到 ESTABLISHED。第三个 ACK 本身就构成一个“回执”表示“我确实收到了你的 SYN-ACK我准备好了”。到这里服务端才敢确认双方状态一致。所以可以给一个更硬核的总结三次握手不是“确认了一次收发”而是“确认了两轮收发链条”。第一轮客户端发 SYN证明自己的发送链路是通的第二轮服务端收到 SYN 并发 SYN-ACK同时证明自己的发送链路和接收链路都是通的第三轮客户端发 ACK让服务端确认自己的接收链路没骗人。每一轮都补上了上一轮缺失的那半段信息。3. 为什么四次是浪费三次已经是“双方达成共识”的最小轮次面试里经常有人反问既然两次不够那是不是越多越稳干脆五次、六次行不行答案是不需要。三次刚好够四次是冗余。3.1 第三握手里藏着的“互为确认”闭环我们把三次握手的信息流画成一张关系表轮次发送方报文发送方因此知道了什么接收方因此知道了什么第 1 次客户端SYN(ISNc)无发出去不知道有没有到客户端想连我客户端的序号起点是 ISNc第 2 次服务端SYN-ACK(ISNs, ackISNc1)服务端活着服务端序号起点是 ISNs客户端能收到我的回复暂定第 3 次客户端ACK(ackISNs1)无新信息但确认了连接可用客户端确实收到了我的 SYN-ACK服务端侧闭环完成注意第 3 行客户端在发出第三个 ACK 后进入 ESTABLISHED它其实在第 2 轮就已经拿到了所有需要的信息服务端则需要第 3 轮的 ACK 做最终确认。换句话说第三握手的核心受益人不是客户端而是服务端。它完成的是服务端侧的“你确认了我的确认”相当于一次 ACK 的 ACK。这有点像团队协作里最稳妥的沟通方式A 说“我明天上午到”B 回“收到明天上午等你”A 再回一句“好就这么定了”。如果 A 只说完第一句就没了B 心里没底如果 B 回了第二句后 A 不吱声B 还是没底A 补一句确认双方都踏实了。再多来回一轮就是纯客套没有信息增量。信息论里可以解释得更准确在仅有“可靠/不可靠”两种状态、且存在丢包和延迟的信道上建立一次双方共同认可的状态需要 3 个时间点上的报文传递。2 个报文只能形成单向确认4 个报文里必然存在一个不增加状态机信息的冗余轮次。所以三次就是这个问题的下确界。3.2 四次握手的真实成本与协议留白有人会问四次握手也不是不行吧最多慢了一个 RTT往返时延。为什么协议设计者不干脆多用一轮让双方知道的信息更充分答案很简单成本收益不划算。三次握手已经让双方都进入了 ESTABLISHED 状态应用层可以开始发数据。多一轮握手意味着所有连接建立都要多付一次 RTT 的延迟。对局域网可能无所谓但对跨洋链路、卫星链路那种几百毫秒 RTT 的场景一次握手多一个 RTT连接建立时间就多三成以上。互联网上每秒钟建立的海量连接乘以这个多出来的 RTT累计浪费的时间相当可观。而且协议本身留了弥补空间TCP 不是靠“握手的轮数”去保证所有报文不丢而是靠后续的累计确认、超时重传、滑动窗口来做可靠性保障。握手阶段只需要消除“历史报文导致的错误连接”和“状态不一致”这两个关键风险剩下的事交给传输阶段去解决。如果真因为报文丢失导致连接建不起来重传机制会兜底而不是靠更多轮握手去硬撑。另外补充一个细节三次握手并不是唯一合法形态。RFC 793 里明确写了一种“同时打开”simultaneous open场景双方同时给对方发 SYN这时要建立连接需要 4 次报文的交互。但在实际互联网环境里这种情况极其罕见绝大部分应用只会经历标准的 3 次握手。面试时能主动提一句“同时打开需要 4 次交互但它不在主流讨论范围”会显得你是真读过协议文档的。4. 协议栈里的三次握手状态机、队列与安全代价前面讲的是“应该是什么”这一节讲“内核里真的发生了什么”。三次握手不是一个抽象协议流程它是存在于 TCP 协议栈里的具体状态机而且带着一系列现实意义上的代价和坑。4.1 状态变迁从 CLOSED 到 ESTABLISHED 的每一步用 Linux 内核的视角看三次握手涉及到的状态有这么几个CLOSED、SYN_SENT、SYN_RCVD、ESTABLISHED以及一个经常被忽略的中间角色——LISTEN。CLOSED初始状态连接不存在客户端调用 connect()发送 SYN状态变成 SYN_SENT服务端在 LISTEN 状态下收到 SYN分配连接块状态进入 SYN_RCVD同时回复 SYN-ACK服务端收到客户端的 ACK 后状态切到 ESTABLISHED客户端收到 SYN-ACK 后状态直接切到 ESTABLISHED。有一个顺序很容易记错客户端是先进入 ESTABLISHED 的一方服务端是后进入的一方。因为客户端收到 SYN-ACK 时它已经掌握了自己需要的一切信息服务端则必须等到第三个 ACK。抓包时如果你盯着服务端的 Netstat 看会发现它在收到 ACK 之前一直停在 SYN_RCVD这个状态对排查问题很有意义——大量连接堆积在 SYN_RCVD要么是被 SYN 攻击打了要么是客户端死活不回 ACK。如果某个环节出了问题状态机会怎么兜底SYN 丢了客户端收不到 SYN-ACK会按指数退避重传 SYN默认重传次数是 5-6 次Linux 的 tcp_syn_retries 控制。SYN-ACK 丢了服务端在 SYN_RCVD 里等待 ACK 超时会重传 SYN-ACK默认 tcp_synack_retries 次。ACK 丢了服务端收不到 ACK继续重传 SYN-ACK客户端此时其实已经 ESTABLISHED收到重复的 SYN-ACK 后会再回一次 ACK或直接丢弃看具体实现。我实测过一次 ACK 丢失的路径往里加 netem 丢包规则把客户端发出的第三个 FINACK 丢掉服务端确实会在 SYN_RCVD 卡住并重传 SYN-ACK客户端收到后会照常回 ACK连接建成的总时间被拉长了一倍左右。这说明三次握手对丢包是有自愈能力的只是会付出重传延迟的代价。4.2 半连接队列、backlog 与 SYN 洪泛攻击服务端在 SYN_RCVD 状态链接的不是完整连接而是“半连接”half-open connection。内核为这些半连接准备了一个专门的队列叫半连接队列对应的概念是 SYN Queue。完整连接则放在 Accept Queue 里由应用层调用 accept() 取出。这就是三次握手的一个衍生问题攻击者可以故意不完成第三次握手让服务端的半连接队列被塞满。著名的 SYN FloodSYN 洪泛攻击思路很简单伪造大量源 IP给服务端狂发 SYN但从不回应后面的 ACK。服务端为每个 SYN 创建一个半连接填满队列后正常用户的 SYN 就进不了队列表现为“连 TCP 都连不上”。内核里和这相关的关键参数和机制面试经常被问到tcp_max_syn_backlog限制 SYN 队列长度tcp_syncookies半连接队列满时启用 SYN Cookie不分配半连接资源而是在 SYN-ACK 里带一个经过编码的 Cookie 值客户端回 ACK 时通过校验来确认连接有效性tcp_abort_on_overflow队列溢出时直接丢弃新 SYN或者返回 RST。SYN Cookie 是我觉得 TCP 协议栈里最精巧的安全机制之一。它没有靠增加存储资源硬扛攻击而是把“验证客户端是否真实存在”的负担转嫁到客户端的第三次 ACK 上属于用逻辑换资源的思路。面试时能把这个原理讲清楚比只报参数名好得多。4.3 初始序列号为什么必须是随机的三次握手花了整整一轮来同步初始序列号那这个初始序列号是怎么定的直接随便用个固定数比如 1行不行答案是不行而且有真实的攻击案例。历史上确实有系统把 ISN 做成可预测的递增值后来就被研究者利用过攻击者不需要真的参与握手就能根据上一个连接的 ISN 推算出下一个连接的初始序号然后伪造一个伪装成目标主机的报文插入到连接里实施会话劫持。这就是著名的 TCP 会话劫持攻击路径。为了堵住这个洞RFC 6528 建议 ISN 的生成要尽量随机化。Linux 内核里用了一个随时间变化的哈希值来生成 ISN和四元组源 IP、源端口、目的 IP、目的端口绑定每个连接不一样每次重连也不一样。给面试的补充点随机 ISN 还有一个“防串扰”的作用。如果两个连接的序号范围重叠且由于之前 TIME_WAIT 期间的延迟报文没有完全消散新连接就有可能收到上一个连接残留的旧报文造成数据污染。随机 ISN 虽然不能彻底消除这种风险但能把碰撞概率降到非常低。从这里也能串起另一道常考题为什么要 TIME_WAIT、端口为什么会出现“Address already in use”后面第 6 节会一起说。5. 面试答题框架从“背结论”到“讲推导”前面铺垫了这么多其实已经覆盖了大部分内容。但这道题毕竟是面试题我单独用一节说说答题策略。面试官问三次握手很多时候不是真的想听你背流程而是想听你脑子里的“网络思维”。5.1 一套可以直接用的回答思路如果被问到“为什么 TCP 要三次握手解决了什么问题”推荐按下面这个结构来每一个层次都对应一种面试官心里的打分点第一层说现象保底分。三次握手是指客户端先发 SYN服务端回 SYN-ACK客户端再回 ACK然后双方进入 ESTABLISHED。第二层说核心目的基础分。它要解决的核心问题有三个一是同步初始序列号让双方知道对方的序号起点二是确认双方收发能力都正常三是防止“历史连接请求”造成的错误连接。第三层做对比拉分项。为什么不是两次因为两次握手只能让服务端单方面认为连接建立服务端无法确认客户端是否收到了自己的 SYN-ACK在出现延迟重传的旧 SYN 时服务端会建立一条客户端根本不认的“幽灵连接”。为什么不是四次因为第三次 ACK 已经让服务端完成了确认闭环第四次没有新增信息量只会白白多一轮 RTT。第四层扯底层加分项。可以补充状态变迁客户端在收到 SYN-ACK 后进 ESTABLISHED服务端在收到 ACK 后才进 ESTABLISHED可以补充半连接队列和 SYN Flood 的关系也可以提一句“同时打开”这类特殊场景。刚复习过的内容顺口提一段比机械背诵好得多。这里有面试官特别容易追问的“为什么不是两次”的严格表述“因为网络是不可靠的会把一个历史连接请求延迟很久后重复送达到服务端。三次握手里服务端能通过 SYN-RCVD 等待 ACK 来分辨这一次请求是否是当前有效请求两次握手没有这个分辨机会。”5.2 追问环节最容易翻车的几个点很多人在“三次”这个主问题上答得很顺挂在追问上。我汇总了我在面试和被面时遇到的几个高频追问追问一第三次 ACK 丢了会怎样答案是客户端已经 ESTABLISHED服务端还在 SYN_RCVD会重传 SYN-ACK第二次 SYN-ACK 到达客户端后客户端会重发一个 ACK。最终连接仍能建立但多了一次重传延迟。追问二如果客户端发出 SYN 后一直收不到 SYN-ACK什么原因排查思路是先看服务端有没有收到 SYN再看 SYN-ACK 是不是被防火墙拦了再看服务端半连接队列有没有溢出。原因可能出在网络路径、防火墙丢包、或服务端资源不足。追问三三次握手建立连接时能不能携带数据答案是第三次可以第一、二次一般不行因为双方还没确认收发能力此时发数据对方未必能做正确处理实际上 Linux 允许客户端在 SYN 里携带少量数据也可以但涉及 TCP Fast Open 的扩展机制不在标准讨论内。追问四为什么服务端不是收到 SYN 就立刻进入 ESTABLISHED因为它还不能确认客户端收到了自己的 SYN-ACK贸然进 ESTABLISHED 就是两次握手的错误逻辑。追问五SYN 攻击怎么防最常答的三种手段调大 tcp_max_syn_backlog、开启 tcp_syncookies、配合防火墙限制单位时间 SYN 速率。这些问题如果都能脱口而出说明是真的理解三层握手而不是背了一个动图就上了考场。6. 实战验证与排查三次握手在真实网络里的样子理论讲再多不如亲手看一次三次握手的过程。这一节很实用适合那些已经看完原理、想自己在机器上验证一把的读者。6.1 用 Wireshark 亲手看一次三次握手准备一台本机和任意一个公网或局域网 TCP 服务目标比如直接抓包访问某一个网站首页或者在本机起一个简单的 Nginx 服务。用 Wireshark 打开抓包过滤表达式写tcp.flags.syn 1 || tcp.flags.ack 1或者抓包时直接用tcp.port 80 or tcp.port 443然后发起一个 HTTP 请求比如浏览器打开一个网页或者用 curl 命令。你会在列表里看到一连串报文其中典型的 60-70 行以内就是你那次 TCP 连接的三次握手。逐个点开看三个关键帧。第一个帧是 SYN 包TCP 头里 SYN 标志位为 1序列号是一个随机生成的值比如 3180349973。第二个帧是 SYN-ACKTCP 头里 SYN 和 ACK 都是 1序列号是服务端的随机值确认号字段等于第一帧的序列号加 1。第三个帧是 ACK只有 ACK 标志位为 1确认号又等于服务端序列号加 1。把这三个帧连起来看就能直观验证前面讲的“ACK 是对端 SEQ1”的规律。我第一次自己抓包亲眼看的时候突然觉得“三次握手”四个字不再是一个干巴巴的流程图而是一段真正发生在自己机器上的短对话。还有一个技巧用命令行工具也能快速验证不需要图形界面。在 Linux/macOS 上跑 netstat 或 ss 就能看到 SYN_SENT、SYN_RCVD、ESTABLISHED 这些状态实时切换。比如用 ss -tn 在连接建立的过程中反复刷能看到客户端先出现 SYN_SENT几毫秒后变成 ESTABLISHED服务端则能看到 SYN_RCVD 一闪而过。6.2 握手相关故障速查超时、重传、端口复用作为一个写过后端服务、也骂过后端服务的人我把最常见的握手故障整理成了一张实战排查表每一条都踩过每一条都有对应的定位手段。故障现象可能的根因排查手段常见解法connect() 长时间卡在 SYN_SENT客户端发出的 SYN 被中间链路丢弃或对端没有监听该端口ss -tn 看状态tcpdump -i any port 目标端口 抓包确认服务端口是否监听排查防火墙策略调 tcp_syn_retries服务端大量连接卡在 SYN_RCVDSYN-ACK 没发出去或发出去被丢弃或半连接队列满ss -tn state syn-recvnetstat -s 看 SYN 统计检查服务端防火墙是否拦了回包加大 backlog开启 syncookies响应很慢握手多次重传SYN、SYN-ACK 或 ACK 经常性丢失Wireshark 看是否存在 TCP Retransmission排查中间设备丢包、链路质量问题客户端重连时报 Address already in use端口还处于 TIME_WAIT 状态ss -tan 查看 TIME_WAIT 数量开启 SO_REUSEADDR或调短 tcp_tw_reuse注意安全性大量短连接导致端口不够用连接建立和关闭频率太高ss -tan 统计 TIME_WAIT 连接数用连接池复用连接减少新建和关闭频率这里面我要重点说两句TIME_WAIT 和 Address already in use。TCP 在主动关闭的一方最后会进入 TIME_WAIT 状态要等 2 倍报文最大生存时间2MSL才彻底消失。原因是最后一个 ACK 可能丢失需要 TIME_WAIT 里的重传能力兜底同时要防止旧连接的延迟报文串扰到新连接。这个状态本身是协议正确性的保障不是什么 bug。但实际开发里特别是 Java 客户端写短连接、频繁重连的时候经常因为端口还在 TIME_WAIT 里被系统回收新连接时报“地址已在使用”。解法里最常用的是在 socket 上设置 SO_REUSEADDR它允许新连接和 TIME_WAIT 状态的连接使用同一组端口地址。不过要注意SO_REUSEADDR 只在主动连接的客户端侧场景下安全服务端设置它有不同的语义处理不好会有端口被多个进程同时绑定的风险。我见过有人图省事全局加了一行代码结果本地调试时一个端口被两个进程按先后顺序绑上排了半天查。另外如果你在搞 Nginx 反向代理这类高并发 TCP 透传场景backlog 和 worker 连接数也会直接影响握手成功率。Nginx 里有一个直接控制 TCP 代理连接数的参数叫 worker_connections它决定单个 worker 进程最多同时打开的连接数普通 TCP 反向代理要特别留意这个数值因为不像 HTTP 有连接复用的兜底透传模式下每个客户端都要独占一条上游 TCP 连接。配置不够时握手阶段表现非常像“服务端满拒”其实根因在代理层没资源了。说到排查工具我再推荐一个组合tcpdump ss Wireshark。tcpdump 负责在服务器上抓原始报文ss 负责看状态机Wireshark 负责事后分析。三步配合基本能覆盖 99% 的握手问题定位场景。具体命令不展开网上随手就能搜到但思路要明确先确认 SYN 有没有到再确认 SYN-ACK 有没有回然后确认 ACK 有没有到逐段切断问题自然会逼出来。我个人做网络排查的经验是不要一开始就怀疑协议坏了协议本身在设计上已经考虑了丢包和重传绝大多数握手问题都出在“某个中间环节直接把包丢了”或者“资源被占满了导致进程来不及处理”这两个方向上。抓包看一次实际流量定位基本就是几分钟的事。这个题讲到这里该覆盖的点都覆盖了。要是你在实践里也撞上过奇怪的握手表现比如 SYN_RCVD 堆积、TIME_WAIT 过多导致的端口复用问题或者 Nginx 代理层连接数卡脖子照着上面的排查顺序走一遍多半能找到答案。
返回列表