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