如今相當多的程序員都是“互聯網程序員”,按說,應該對互聯網的基礎協議相當清楚。可惜至少就我的面試經驗來看,許多人這方面缺課太多,簡單說說TCP/IP協議分層就已經難倒了不少人。至于TCP/IP的“三次握手”,能說上來的人就相當少了,如果再問問“為什麼是三次握手”,基本就沒人能答上來了。一般的回答都是“這個太難”,或者“畢業太久,這個忘記了”。
如果臨時抱佛腳,把TCP的三次握手背下來應付面試,确實能做到。但是要回答TCP為什麼是三次握手,而不是兩次或者四次握手,光靠背就不行了——不信你去網絡上搜搜看,各種回答都有,衆說紛纭,不少提問者一頭霧水。
TCP相關的知識重要嗎?我覺得挺重要的,這些年來無論互聯網怎麼變化,TCP協議本身都可以承載,仔細探究會發現它的設計的确夠巧妙,有許多值得借鑒的設計思想。
那麼TCP真的很難嗎?為什麼許多人背TCP的握手流程痛苦不堪,複述起來困難重重?我覺得,原因在于大家隻把它當成“既存事實”, 就像上中學時候背曆史政治那樣對待。但TCP可不是毫無邏輯的胡說,一旦 你搞清了設計思想和邏輯,就會發現理解起來一點也不困難。所以,今天我來做個簡單講解。
首先說說“三次握手”這個譯名,我确實覺得翻譯有誤(翻譯出版過一百多萬字技術資料,我自信還是有把握的)。我以前總記不住“三次握手”的過程,因為總覺得“握了三次手”,“握手”是雙方共同往中間湊的過程,這明顯和建連流程不符合。後來才發現,“三次握手”的說法大概有問題。
“三次握手”的原文是three-way handshake,three-way更合适的翻譯恐怕是“三步”,所以整個名詞的意思是“需要三個步驟才能建立握手的機制”。這麼解釋的好處是,“步”給人感覺更形象,就是“單方面邁一步”而已。實際上,RFC 793裡說明了,握手過程也可以叫three-message handshake,通過三條消息來建立的握手。
那麼,為什麼要三步才能建立握手呢?我們可以暫時不理這個問題,想想如果我們自己來設計握手機制,應當怎麼辦。
我們都知道,TCP是可靠的通訊協議,其“可靠性”就在于,任何一方要向另一方發數據(SYN),都必須收到确認回應(ACK)。同時TCP也是雙向的通訊協議,所以通訊的兩方都可以主動發送消息。
這裡要澄清的一點,對許多“互聯網程序員”來說,TCP是掩蓋在HTTP之下的,大家熟悉的HTTP,它的經典通訊模式是“一問一答”的,沒有請求就沒有應答。不過這隻是HTTP的特性,不是TCP的特性。在TCP協議裡,客戶端和服務器都可以随時主動向對方發送數據——也正是因為如此,改用HTTP/2之後服務器可以主動推送信息給客戶端,而不必改動TCP協議。
回到TCP,既然它是雙向、可靠的通訊,可以想見,建立連接就必須确認雙方到對方的通訊都是可靠的,所以大概需要四步,發送四次消息。
如果軟件設計都這麼簡單,那就太好了。可惜,世界上沒有那麼簡單的事情。仔細觀察這幅圖,我們會發現幾個問題:
第一,網絡通訊的成本是很高的,延遲往往無法預測,哪怕能少發送一次消息,也可以大大降低成本,提高效率。所以,建立連接的步驟上限應當是四步,下限是兩步,越少越好。
第二,兩輪SYN/ACK之間必須有關聯,因為它們的功能相對獨立,都是确認到對方的通訊可靠,卻同屬于一個“建立連接”的邏輯操作。如果兩輪完全獨立,那麼如果兩輪中間間隔了特别特别長的時間,根本不是一個正常的建立連接的操作,程序卻無法識别,這顯然是不行的。所以,第二輪SYN/ACK必須要能夠和第一輪SYN/ACK關聯起來。
再仔細看看,第二步和第三步都是從服務端給客戶端發消息,所以是不是可以合并起來?這樣起碼可以節省了一次網絡通訊。
像上面這樣直接在第二步把ACK和SYN合并起來,問題就解決了?
按照之前的分析,節省消息發送次數隻是考慮之一,還需要考慮的是,第二輪SYN/ACK必須和第一輪SYN/ACK挂鈎。
上面是TCP的數據報,包含了許多的控制位,用來标識連接的狀态。其中最常見的是SYN、ACK、FIN:SYN表示synchronize,在建立連接時使用;ACK表示acknowledge,表示“确認”收到了消息;FIN表示finish,在斷開連接時使用。
還要注意的兩個東西是SEQ NO和ACK NO。SEQ NO即Sequence Number,服務端和客戶端都會維護自己的SEQ NO,表示“已經發送了多少數據”,單位是字節;ACK NO即Acknowledge Number,用來回複确認,對應SEQ NO的數據已經收到。單獨說起來,這些概念都容易理解,隻是注意不要混淆控制位的ACK和ACK NO——ACK是布爾值用來标識數據報的類型,ACK NO是數值用來确認已經收到的數據。
基于上面的知識我們可以知道,在建立連接之初,數據報中的控制位SYN應當設定為1,表示“新建連接”;同時應當包含SEQ NO。此時的SEQ NO有個專門的名字叫ISN,也就是Initial Sequence Number(要注意,ISN隻是用來稱呼這個特殊SEQ NO,并不存在專門的ISN字段)。
在服務端收到第一個SYN消息的時候,它當然需要發送ACK響應,但它如何确認其中的SEQ NO“就是”新建連接的ISN,而不是來自姗姗來遲的某個古老連接呢?所以必須向客戶端确認。恰恰因為第二步是ACK,SYN“合二為一”的獨特響應,所以收到這個消息時,客戶端就知道,既需要響應其中的SYN,也需要核實其中的ACK(如果你仔細讀過RFC793就會知道,其中專門有一段提到了: A three way handshake is necessary because…… )
到了第三步,客戶端返回的消息裡既包含對應SYN的ACK,表示收到了服務端的消息,同時設定SEQ NO=ISN 1,确認核實了ISN。服務端收到這條消息,确認無誤是要建立新連接。至此,連接建立完畢。
大流程看起來就是這樣,也不難理解。不過仔細想想,還是有不少問題得考慮的。比如狀态問題,既然TCP是網絡通訊,會發生延遲,那麼在“信息已經發送,但還沒有收到确認”的時候,應當是有個明确狀态的,否則會發生狀态的錯亂。實際上TCP也确實做到了這點,它背後有一台完整的狀态機,确保每時每刻,每個動作發生之後,狀态都完全可控,一切盡在掌握,不會出現任何“孤點”和“斷頭路”。
上圖是TCP的狀态轉移圖的局部,覆蓋了建立鍊接的狀态,感興趣的讀者可以按照自己實地走走看(說個題外話,“自己模拟在圖上走走”看起來土,其實高科技領域也挺常用。設計波音737的時候,開始大家都不知道發動機怎麼擺比較好,設計師喬·薩特就在紙上畫出機身和發動機的模型,把發動機模型剪下來在飛機各處擺放,最終發現吊在翼下最合适)。
我在之前關于軟件設計的文章裡幾次提到狀态圖、狀态轉移函數,無論是用戶生命周期、訂單流轉過程,都可以用這個工具來解決。遺憾的是,我發現還有許多設計人員不懂得或者不習慣用使用它,實在很可惜。
回到TCP建立連接的過程,我們還要注意ISN。在建立連接時必須先确定ISN,通過它把客戶端和服務器的計數對齊。通常的教材上說,ISN是随機生成的,這樣就保證了唯一性。 随機的目的是保持唯一,但千萬不要以為“随機就不會重複”,簡單的“取随機數”是很容易碰撞的。所以傳統的“随機”方案是維護一個時鐘和一個32位的計數器,時鐘每過4毫秒,計數器自增1。因為2^32毫秒就是差不多4個半小時(MSL,Max Segment Lifetime),這基本超出了任何數據包在網絡中的可能傳輸時間,所以可以認為這種ISN是獨一無二的。
但這種方案也有風險,既然這樣的ISN是連續的,那麼中途的惡意程序可能能夠預測ISN的生成規律,從而僞造ISN…… 總之ISN的生成是個有趣的設計問題,這裡不展開了,有興趣可以自己搜索資料閱讀。
我在開發中遇到不少程序員,一旦需要避免重複,就想到“生成随機數”,根本不管随機數也可能碰撞。更有甚者,一旦遇到類似ISN的場合,就想當然把初始值設定為0,真是讓人欲哭無淚(有沒有想過ISN為什麼不能設定為0呢,歡迎留言讨論)。
說完了建立連接的握手,我們再來看終止連接的揮手。通常大家都知道,TCP是“三次握手,四次揮手”(雖然我很不贊成“次”,但既然它已經約定俗成,這裡還是延用通用的說法吧)。那麼,為什麼要四次才能揮手呢?
知道這個答案的人比能講清楚“三次握手”的要多。通常的答案都是:TCP是雙向通訊協議,要結束連接,雙方都必須發送終止信号,告訴對方後續再沒有數據發過來了,并等待對方确認,所以一共需要2 2=4次。
如果你之前看過建立連接的過程,大概會有這樣的疑問:既然建立連接的時候可以節省一步,把服務端返回SYN和ACK合并到一起,那麼結束連接的時候,是否也可以把服務端返回的SYN和FIN合并起來,節省一步呢?
想到了這個問題就值得恭喜,因為你不是隻滿足于“知其然”,而希望“知其所以然”。不過我們也需要想到,既然TCP連接的建立和終止都是同一批人定義的,既然他們能想到在建立連接時節省一步,那麼他們沒有理由在終止連接時不做節省。之所以沒有“節省”,一定是有理由存在的。
沒錯,确實是有理由的,而且這個理由很好理解,因為建立和終止連接的場景是不一樣的。在建立連接之前,客戶端和服務器端都不會向對方發送任何數據,所以在服務端返回ACK的時候帶上SYN,客戶端當然知道這是從服務端收到的第一個數據包。
而在結束連接時,客戶端向服務端發送FIN,表示“我這邊不會繼續發送數據過來了”,服務端響應ACK,這都沒有問題。但此時,服務端之前向客戶端發送數據的操作可能還沒有完成,服務端仍然在向客戶端傳輸數據。如果服務端把FIN和ACK合并起來,就會出現這樣的情況:客戶端的數據還沒有接受完,忽然收到服務端的消息“後續沒有數據了,終止連接”。顯然,這種情況不應當出現,所以不能把ACK和FIN合并在一起,所以終止連接必須要四步。
最近和實習生聊天,說起開發中遇到的各種問題,以及對應的模型,大家聽得入迷。事後有人問我:為什麼我們工作中遇不到這麼有意思的問題呢?我知道,這是個比較典型的問題。其實答案也很典型:因為你沒有去深究問題背後的原型。懂得了背後的原型,就具備了“從已知推導無知”的本領,也具備了“從無知中發現已知”的眼光。
我和朋友聊開發有個共同的判斷:TCP的握手和揮手看起來簡單,但真讓如今的開發人員去設計握手和揮手流程,估計有超過一半的人設計不出穩定、可靠、高效的握手和揮手流程。這樣說來,許多業務系統裡業務層面的通訊極不可靠,協議設計錯漏百出,也是無奈的結果了。
補充一句。我曾在面試中遇到過這樣的人,非名校畢業,已經有五年工作經驗,除了對流行的框架和熱點問題對答如流,對數據庫理論、網絡基礎知識、數據結構和算法依然如數家珍。事實充分證明,不是所有人工作之後就把大學的知識丢個精光的,事實也證明,這樣的候選人确實能擔大任。
文章作者:餘晟
,更多精彩资讯请关注tft每日頭條,我们将持续为您更新最新资讯!