
提到TCP三次握手我第一反應不是“SYN、SYNACK、ACK”而是“面試官到底想聽什么”。很多人把三次握手的流程背得滾瓜爛熟但一旦被追問“為什么不能是兩次”“第三次ACK丟了會怎么樣”立刻卡殼。這個問題之所以經(jīng)典是因為它考的不是記憶而是對TCP連接本質(zhì)的理解。寫這篇文章就是想把三次握手背后的三個核心問題講透它確認了什么、同步了什么、防御了什么以及這套機制在真實網(wǎng)絡編程里的延伸影響。無論你是準備面試的開發(fā)者還是在排查連接的坑都應該吃透這一塊。1. 三次握手到底在解決什么問題三次握手不是簡單的“打招呼”它本質(zhì)上是兩個端點之間進行的一次狀態(tài)協(xié)商。TCP是一個面向連接的可靠協(xié)議“連接”這兩個字不是虛的它意味著收發(fā)雙方需要維護一些共同狀態(tài)才能正確地把數(shù)據(jù)字節(jié)流傳送給對方。在連接建立之前雙方對彼此一無所知通過三次交換至少要達成下面三個子目標。1.1 三個子目標能力確認、序列號同步、防歷史復發(fā)第一個子目標是能力確認。邏輯上雙方需要確認“我能發(fā)你也能發(fā)我能收你也能收”。這聽起來簡單但在分布式網(wǎng)絡中一個消息從A發(fā)出B能否正確收到在收到ACK之前是無從得知的。所以需要一方發(fā)探測消息另一方回確認再讓發(fā)起方對確認做二次確認形成一條完整的確認鏈。第二個子目標是序列號同步。TCP每個方向都有自己的字節(jié)流序列號接收方需要知道發(fā)送方的初始序列號ISN才能做排序、去重、累計確認。ISN是隨機選擇的如果不做同步雙方都不知道對方下一個字節(jié)從哪個序號開始后續(xù)所有數(shù)據(jù)包都無法對應。第三個子目標是防止歷史重復連接。網(wǎng)絡環(huán)境不是理想狀態(tài)一個很久以前發(fā)出的SYN報文可能因為延遲突然出現(xiàn)在服務器門口。如果服務器收到一次SYN就直接認定連接建立那么這個“僵尸請求”就會白白占用資源甚至干擾一條已經(jīng)存在的連接。三次握手通過客戶端最后一次ACK來表達“當前這個連接是我真實想要的”從而把歷史SYN過濾掉。1.2 兩次握手為什么一定不行很多人說“兩次不行”但講不清為什么。我們推演一下只有兩次握手的情況客戶端發(fā)SYN服務器收到后回SYNACK然后雙方就算建立了連接。問題出在服務器永遠無法確認客戶端是否收到了自己的SYNACK。假設SYNACK在路上丟了客戶端這邊一直等不到回復會認為連接沒有建立可能再次發(fā)起SYN重試。服務器卻已經(jīng)進入了ESTABLISHED狀態(tài)開始分配資源、等待業(yè)務數(shù)據(jù)。這樣一個連接客戶端根本不知道它的存在服務器卻在為它做著無謂的支撐。如果是客戶端因為某種原因已經(jīng)關閉了端口當服務器收到這種無法投遞的后續(xù)數(shù)據(jù)時只能依賴RST去兜底而資源浪費已經(jīng)發(fā)生了。再從序列號同步的角度看兩次握手也有致命傷。第一次SYN攜帶了客戶端的初始序列號第二次SYNACK攜帶了服務端的初始序列號此時客戶端確實知道了雙方的序列號但服務器并不知道客戶端是否已經(jīng)知道了自己的序列號。缺少一個針對服務端SYNACK的確認服務器后續(xù)發(fā)送數(shù)據(jù)時就無法確定客戶端的接收窗口和ACK基準是否已經(jīng)就緒。所以二次握手是信息不對稱的握手總有一方在“盲猜”。三次握手剛好把這種不對稱補上。2. 三次握手中每一步的角色與細節(jié)面試時把“SYN、SYNACK、ACK”說出來只是第一步每一步到底改了哪些狀態(tài)、攜帶了什么信息才是加分項。2.1 三次交換與狀態(tài)遷移下面這張表把三次握手過程中雙方的狀態(tài)變化列出來步驟發(fā)送方報文類型接收方狀態(tài)變化同時攜帶的信息第1次客戶端SYNseq x服務端LISTEN - SYN_RCVD客戶端的初始序列號x第2次服務端SYNACKseq y, ack x1客戶端SYN_SENT - ESTABLISHED服務端初始序列號y同時確認收到x第3次客戶端ACKack y1服務端SYN_RCVD - ESTABLISHED確認收到服務端的序列號y注意這里的序列號都使用了相對編號實際抓包中seq是絕對隨機數(shù)比如4000000000。第1次SYN中的seqx代表客戶端發(fā)送數(shù)據(jù)的第一個字節(jié)的序號第2次SYNACK中的ackx1表示“我已經(jīng)收到你seqx的報文期望你下一個報文從x1開始”。第3次ACK中的acky1同理是對服務端SYN的確認。2.2 為什么客戶端最后一個ACK是“和事佬”第三個ACK不攜帶任何應用數(shù)據(jù)往往被忽略但它的角色非常關鍵。服務端在發(fā)出SYNACK后立刻面臨一個不確定性我這條消息到底到?jīng)]到如果沒到我盲目進入ESTABLISHED狀態(tài)后續(xù)發(fā)數(shù)據(jù)客戶端可能會因為沒收過SYNACK而無法正常解析。所以服務端必須等一個針對SYNACK的確認。這個ACK一旦發(fā)出并到達服務端服務端心里那塊石頭才落地至此雙方的發(fā)送和接收通道都經(jīng)過了完整確認連接才真正進入穩(wěn)定可用的狀態(tài)。有個好記的說法第一次握手是客戶端說“我想建立連接”第二次握手是服務端說“我收到你的請求我也準備好給你發(fā)數(shù)據(jù)了你能收到嗎”第三次握手是客戶端回一句“我能收到我也確實要和你建立連接”。話雖通俗但道出了雙方信息對齊的本質(zhì)。2.3 報文在抓包里長什么樣我們起一個tcpdump就能清清楚楚看到這個過程。假設客戶端ip 192.168.1.10用端口56789訪問服務端192.168.1.20的80端口14:58:31.123456 IP 192.168.1.10.56789 192.168.1.20.80: Flags [S], seq 4000000000, win 64240, length 0 14:58:31.123489 IP 192.168.1.20.80 192.168.1.10.56789: Flags [S.], seq 1000000000, ack 4000000001, win 65535, length 0 14:58:31.123500 IP 192.168.1.10.56789 192.168.1.20.80: Flags [.], ack 1000000001, win 65496, length 0Flags[S]表示SYN置位[S.]表示SYN和ACK同時置位[.]表示僅ACK置位。這正是三次握手最容易辨認的形態(tài)。這里seq都寫得很大因為TCP初始序列號是隨機的防止外部猜測序號去偽造報文。第二個報文中ack4000000001等于第一個報文的seq1。第三個報文中ack1000000001等于第二個報文的seq1。這個1是因為SYN報文會占用一個序列號即使它不攜帶數(shù)據(jù)。3. 為什么“三次”是數(shù)學上的最優(yōu)解很多面試官會追問為什么不能是四次五次答案要從分布式確認的“最少次數(shù)”去理解。3.1 最少必要次數(shù)一次握手不用多說連基本方向都確認不了。兩次握手的問題在第1節(jié)已經(jīng)剖析無法讓服務器確認“客戶端的接收能力”。三次握手讓雙方都完成一輪“發(fā)送請求、收到確認、確認對方已收到確認”的閉環(huán)??梢赃@樣算要讓雙方都確定“對方已經(jīng)知道我的初始序列號”需要幾次握手第一次客戶端讓服務器知道了自己的ISN第二次服務器讓客戶端知道了自己的ISN第三次客戶端告訴服務器“我已經(jīng)知道你的ISN”。到這一步服務器的ISN信息在兩端達成一致而客戶端的ISN從第一次握手開始就是已知的其實第二次的ack也在向客戶端確認“服務器知道我的ISN”。所以三次足夠把兩個ISN都同步好。四次行不行也行但多出來的第四次交換并沒有帶來新的協(xié)議信息。所有需要同步的狀態(tài)在第三次時已經(jīng)全部對齊。多一次只是增加握手開銷和時延純屬浪費。TCP是講究效率的協(xié)議能用最低成本達成就絕不加戲。3.2 歷史重復SYN的防御RST的作用三次握手防歷史重復請求的機制值得單獨拿出來講。假設一臺客戶端曾經(jīng)向服務端發(fā)送過SYN但這個SYN在網(wǎng)絡中滯留了很久。期間客戶端已經(jīng)超時重傳并成功建立了新連接然后數(shù)據(jù)傳輸完畢連接關閉。此時那個舊的SYN才姍姍來遲。如果采用兩次握手服務端收到舊SYN會直接分配資源、進入ESTABLISHED然后開始期待從該客戶端發(fā)送數(shù)據(jù)。問題是客戶端已經(jīng)完全不想要這個連接了于是這個“幽靈連接”就掛在了服務端直到超時回收期間服務端的內(nèi)存、文件描述符、端口資源都被白白占用。惡意攻擊者如果利用這個機制批量發(fā)送滯留SYN甚至可以直接把服務端的資源打滿。三次握手之所以能解決是因為服務端不輕易相信一個SYN就建連。它回一個SYNACK后會等待客戶端發(fā)來ACK。如果客戶端沒有當前連接意圖收到SYNACK后會返回一個RST報文告訴服務端“我不認這個連接”。服務端收到RST后就會釋放半開狀態(tài)資源隨之回收。這個一推一拉的過程完美過濾了歷史殘留。3.3 資源分配和SYN Flood帶來的威脅三次握手在服務器端天然涉及兩個隊列半連接隊列和全連接隊列??蛻舳薙YN到達服務端在SYN_RCVD狀態(tài)時會把連接放入半連接隊列第三次ACK到達后狀態(tài)變?yōu)镋STABLISHED再從半連接隊列遷移到全連接隊列等待應用層accept。這個設計雖然解決了歷史SYN問題但也誕生了SYN Flood攻擊。攻擊者可以故意不回第三次ACK只不斷發(fā)送SYN讓服務端半連接隊列被占滿后續(xù)正常的連接請求無法進隊。實際防護手段里SYN Cookies是非常經(jīng)典的一招在SYNACK時不再保持半連接狀態(tài)而是把連接狀態(tài)編碼進cookie等客戶端ACK回來再解碼恢復。這也是理解三次握手“狀態(tài)”價值的一個延伸握手狀態(tài)可以被隱藏在cookie里未必一定要占用服務端內(nèi)存。從協(xié)議層面看三次握手讓連接建立這個動作的“代價”和服務端的“信任”達到了平衡既不會因為一次SYN就浪費資源也不會因為多次往返增加不必要的延遲。4. 從抓包和代碼看三次握手的實戰(zhàn)表現(xiàn)理論說再多不如動手看一眼。這一節(jié)我從實際編程和抓包的角度把三次握手和操作系統(tǒng)協(xié)議棧聯(lián)系起來。4.1 用tcpdump實時看一次連接建立你可以在Linux機器上運行命令抓取自己發(fā)起的連接sudo tcpdump -i eth0 tcp port 80 -nn然后在另一個終端用curl訪問一個網(wǎng)站curl -I http://example.com抓包里除了能看到TCP握手包還能看到HTTP請求隨之而來。注意HTTP請求數(shù)據(jù)一定是在第三次ACK之后的包中發(fā)出不會出現(xiàn)“連接沒建立就發(fā)數(shù)據(jù)”的情況。這個順序本身就是TCP為應用層提供的可靠連接保證數(shù)據(jù)包和連接狀態(tài)嚴格綁定。4.2 C語言socket編程中三次握手藏在哪里用C語言寫過TCP服務的同學都知道服務端調(diào)用socket()、bind()、listen()后端口進入LISTEN狀態(tài)??蛻舳苏{(diào)用connect()時內(nèi)核會立刻發(fā)出SYN然后阻塞等待SYNACK收到后發(fā)出ACKconnect()才會返回成功。這個過程對應用層來說是透明的但內(nèi)核里的狀態(tài)機在飛速運轉(zhuǎn)。服務端在accept()返回握手成功的套接字之前這個連接可能已經(jīng)在內(nèi)核態(tài)走完了三次握手進入全連接隊列。如果應用層一直不accept連接會堆積在隊列里導致客戶端connect成功但服務端應用完全無感知。這種“握手已完成業(yè)務無法處理”的現(xiàn)象在高并發(fā)下很常見本質(zhì)是TCP狀態(tài)機和業(yè)務代碼沒有協(xié)同好。編程時容易忽視一點connect()是有超時的。SYN發(fā)出后如果一直收不到響應內(nèi)核會按指數(shù)退避重傳SYN。Linux下可以通過修改/proc/sys/net/ipv4/tcp_syn_retries調(diào)整重傳次數(shù)默認通常是6次總耗時約1分多鐘。所以一個connect()卡在那里幾十秒不是程序卡死而是內(nèi)核在等握手響應。sysctl net.ipv4.tcp_syn_retries這段話對排查“連接建立慢”的問題很有用你去抓包會看到SYN、SYNACK、ACK的間隔是否正常。如果只有SYN重傳沒有響應八成是服務端端口沒監(jiān)聽或者防火墻把SYN丟掉了。4.3 握手成功后你可能踩的TIME_WAIT坑握手解決的是建立連接但連接一定會被關閉。關閉時四次揮手會產(chǎn)生TIME_WAIT狀態(tài)然后出現(xiàn)經(jīng)典報錯Java客戶端重連時報“Address already in use”。很多人不理解我客戶端主動connect為什么本地端口會被占用原因很簡單本地隨機選了一個臨時端口去connect連接關閉后進入TIME_WAIT這個端口和遠端IP、遠端端口構(gòu)成的四元組會保留一段時間默認60秒具體由tcp_fin_timeout等參數(shù)影響。如果客戶端在短時間內(nèi)反復創(chuàng)建新連接又恰好隨機到同一個源端口就會嘗試bind一個正在TIME_WAIT中的地址于是觸發(fā)Address already in use。解決辦法不是改三次握手而是設置SO_REUSEADDRserverSocket.setReuseAddress(true);這個選項允許在TIME_WAIT期間復用本地端口。對于客戶端頻繁重連的業(yè)務更好的方案是使用連接池而不是反復建立短連接。這也是為什么很多數(shù)據(jù)庫客戶端、消息隊列客戶端都會維護連接池而不是每次請求新建連接。再說一句三次握手作為“連接創(chuàng)建”的成本在延遲敏感場景中不可小覷。一次握手至少要一個RTT跨地域長距離時RTT動輒上百毫秒如果業(yè)務頻繁建連性能損耗肉眼可見。5. 面試高頻追問與排查技巧這一節(jié)整理幾個圍繞三次握手的高頻追問和實際排查場景希望幫你在面試和工作中都能對上號。5.1 第三次ACK丟失會發(fā)生什么這是最常見的追問。第三次ACK如果丟了服務端在SYN_RCVD狀態(tài)下一直等不到ACK會按遞增間隔重傳SYNACK同時客戶端已經(jīng)進入ESTABLISHED狀態(tài)開始發(fā)業(yè)務數(shù)據(jù)。服務端收到客戶端的數(shù)據(jù)包時發(fā)現(xiàn)該數(shù)據(jù)包的ACK正好確認了自己的SYNACK它會將這個連接遷移到ESTABLISHED然后繼續(xù)處理數(shù)據(jù)。所以第三次ACK丟失不一定導致連接建立失敗只是多了一次重傳同時可能讓客戶端早于服務端進入“連接已可用”狀態(tài)造成短暫的半開窗口。但如果服務端一直沒有收到任何來自客戶端的數(shù)據(jù)或重傳的ACK它會一直重試SYNACK直到超過tcp_synack_retries后放棄向客戶端發(fā)送RST??蛻舳舜藭r如果還在高高興興發(fā)數(shù)據(jù)就會收到RST包連接立刻被斷開。面試時這樣回答能體現(xiàn)出你對TCP狀態(tài)機的顆粒度掌握而不僅僅是背流程。5.2 “已經(jīng)建立的連接”和“半開連接”的區(qū)別三次握手建立的是完整連接。如果連接建立之后一端突然宕機另一端不會立刻知道這種狀態(tài)叫半開連接。網(wǎng)上有人把它和半連接混淆。半連接是三次握手尚未完成、服務端處于SYN_RCVD的狀態(tài)半開連接則是完成握手后鏈路中斷但雙方?jīng)]有感知。排查半開連接時可以用Linux的ss命令看狀態(tài)ss -t state established如果發(fā)現(xiàn)大量連接長時間沒有任何收發(fā)可以依賴TCP的KeepAlive機制去探測。TCP KeepAlive默認關閉打開后會在空閑一段時間發(fā)送探測報文但這和三次握手沒有直接關系屬于連接?;畹姆懂牎C嬖嚴飬^(qū)分開這兩個概念同樣很加分。5.3 面試時如何組織回答框架如果你在面試現(xiàn)場遇到“為什么TCP要三次握手”可以按三層框架答。第一層講能力確認說明為什么最少要三次。第二層講序列號同步說明SYN如何攜帶初始序列號以及ACK如何確認對方序列號。第三層講歷史重復連接防御和資源分配點明兩次握手會讓服務端盲目建連、產(chǎn)生資源浪費三次握手可以借助RST清理無效請求。每層都給出一個實際場景佐證比如SYN Flood或者TIME_WAIT。這個回答結(jié)構(gòu)既能讓面試官看到你的協(xié)議理解深度也能讓你的思路始終圍繞“解決了什么問題”展開。單純背流程是拿不到高分的重點在于“為什么”。我自己帶過不少新人發(fā)現(xiàn)能把三次握手講透的人排查網(wǎng)絡問題的時候思路往往特別清楚。因為他們知道TCP的一切機制背后都是為了解決分布式環(huán)境下的不確定性問題——報文可能丟、可能重、可能亂序也可能遲到。三次握手不是繁文縟節(jié)而是用最小的代價把連接建立的不確定性降到了最低。