Hello,大家好!我是木榮。今天給大家分享一份網絡中傳輸層UDP和TCP相關的知識,在實際的工作中,網絡編程是十分重要的一個知識點,尤其是TCP和UDP通信,也是我們編程中打交道最多的。本文非常詳細地講解了UDP和TCP通信中各個知識點,希望對小伙伴們有幫助!
一. 應用層和傳輸層的聯(lián)系
程序員在應用層要做的事情
在網絡通信 的過程中, 應用層描述了應用程序如何理解和使用網絡中的通信數據, 和程序員打交道最多的就是應用層了, 針對不同的業(yè)務場景, 很多時候程序員需要去自定義應用層協(xié)議, 自定義協(xié)議主要需要完成下面的兩件事情:
-
結合業(yè)務場景和需求, 分析清楚, 客戶端和服務器之間(請求/響應)要傳遞哪些信息.
-
明確傳遞的信息要以什么樣的格式來組織, 可以是普通文本的方式, 也可以一些廣泛使用的數據格式, 比如: XML, json, protobuffer.
其中xml和json都是按照文本的方式來組織的, 優(yōu)點是可讀性好, 用戶不需要借助其他工具, 肉眼就能看懂數據的含義, 缺點是要額外傳很多的標簽或鍵名, 占用較多的網絡帶寬, 影響效率; 而 protobuffer會將文本數據壓縮 為二進制數據傳輸, 特點是肉眼無法解析, 但占用空間更小小, 傳輸占用的帶寬也就降低了.
應用層也有知名并廣泛使用的成品協(xié)議, 就比如 : HTTP協(xié)議.
傳輸層和應用層的聯(lián)系
除了最上層的應用層, 下面的傳輸層, 網絡層, 數據鏈路層, 物理這四層都是已經在系統(tǒng)內核/驅動程序/硬件中已經實現好了, 不許要我們去實現, 傳輸層是緊接著應用層的一層, 雖然傳輸層是操作系統(tǒng)內核實現好了, 但是我們在寫應用層代碼的時候, 是要調用系統(tǒng)的socket API去完成網絡編程, 所以需要我們了解這里傳輸層的一些關鍵協(xié)議UDP和TCP.
端口號的使用注意
端口號是傳輸層協(xié)議的概念, TCP和UDP協(xié)議的報頭中都會包含源端口和目的端口, 并且都是使用 2 個字節(jié), 16bit來表示端口號, 范圍也就是 0 -> 65535; 但是我們日常寫的程序使用的端口號一般都是從 1024 開始的, 因為0 -> 1023這個范圍的端口號也稱為 “知名端口號/具名端口號”, 這些端口號系統(tǒng)已經分配給了一些知名并廣泛使用的應用程序.
這里我們并不是完全不能使用0 -> 1023這個范圍的端口號, 只是建議使用, 雖然這些端口被分配給了特定程序, 但是這些程序是否在主機運行著, 主機上是否安裝了這些程序都是不一定的, 要使用0 -> 1023這些端口, 需要注意 2 點 :
- 要確定這個端口沒有和程序綁在一起.
- 要擁有管理員權限.
二. UDP協(xié)議
UDP是User Datagram Protocol的縮寫, UDP的特點是無連接, 不可靠傳輸, 面向數據報, 全雙工, UDP使用起來簡單高效, 但它的數據載荷較小, 一般適用于以下場景:
- 包總量較少的通信(DNS、SNMP等)
- 視頻、音頻等多媒體通信(即時通信)
- 限定于LAN等特定網絡中的應用通信
- 廣播通信(廣播、多播)
UDP協(xié)議的格式 :
上面是教科書中的畫法, 這樣畫是為了排版方便, 實際上畫成下面這樣更合理一些, 不過這并不影響理解.
UDP會把從應用層拿到數據(就是網絡編程中應用層調用socket API, send()發(fā)送的數據)的基礎上再前面拼裝上 8 個字節(jié)的報頭.
下面就來分析一下報頭中所包含的這些屬性, 首先UDP報頭一共是 8 個字節(jié), 有 4 個部分分別占 2 個字節(jié), 源端口號和目的端口號不必多說, 要完成一次網絡通信, 涉及到源IP, 源端口, 目的IP, 目的端口, 協(xié)議類型這五元組, 其中端口信息是是由傳輸層負責的.
UDP的包長度, 也叫報文長度(報頭+載荷), 這個屬性表示了一個UDP數據報的大小, 單位為字節(jié), 2字節(jié)表示 0->65535這個范圍, 也就是說一個UDP數據報最大不超過64KB.
那如果應用層的數據報大于64KB怎么辦呢?
這里有兩種方案:
- 一是可以在應用層的代碼層面將應用層的數據報手動進行分包, 這樣拆分成多個小的包通過多個UDP數據報進行傳輸.
- 第二種方案是就不用UDP協(xié)議了, 改用TCP協(xié)議, TCP沒有這樣的限制的, 后文會介紹到.
而且使用TCP也是更方便的, 如果使用第一種方案, 應用層的代碼實現起來也比較復雜, 代碼也得進行很多的測試, 畢竟代碼復雜了, 隨之bug也會更多更難處理一些, 這就好比叫貨拉拉搬家, 如果 東西很多, 一般的或者裝不下, 如果采用方案一, 就需要多約幾輛車, 這樣約車, 裝車, 清點都比較麻煩; 而采用方案二, 之間叫一個大一點的貨車就好了.
最后一個屬性就是校驗和, 作用是驗證傳輸的數據是否是正確的, 網絡傳輸, 本質上是在傳輸光信號/電信號, 在傳輸過程中, 可能會受到一些物理環(huán)境的干擾等, 在這些干擾下就可能出現 “ 比特翻 轉 ” 的情況, 0 會變成 1 , 1會變成 0.
一旦數據變了, 對于數據含義的解析可能就致命的, 舉一個典型的例子, 程序中經常使用 1 表示某個功能開啟, 0表示關閉, 本來網絡數據報是想開啟功能, 結果因為翻轉, 就導致變成了關閉了.
像這樣的現象是客觀存在不可避免了, 我們能做的只是及時的識別出當前的數據是否出現了問題, 因此就引入了校驗和來進行鑒別, 校驗和是針對數據的內容進行一系類的運算(每一個比特位都會參與運算)得到一個比較短的結果, 我們可以認為, 數據內容一定, 得到的校驗和就是相同的, 如果我們的數據變了, 那么的校驗和就變了, 如此即可驗證得到數據是否準確.
比如發(fā)送方要發(fā)送的數據是 “反射導彈”, 計算出來的校驗和也會放到報頭中發(fā)送, 當接收方拿到數據就會有兩種情況了, 第一種情況是接收方拿到數據后重新計算一個校驗和, 得到的結果如果和拿到的校驗和相同, 就認為數據是正確的, 不同, 則認為數據不正確.
還有就是說, 數據傳輸過程出錯了, 但是可能計算得到的校驗和和之前的校驗和恰好─樣, 這種情況理論上是存在的, 但在工程實踐中出現這種情況的概率極小, 就忽略不計了.
針對網絡傳輸中, 生成校驗和的算法有很多種, 這里簡單介紹一下下面幾個比較知名的:
- CRC(循環(huán)冗余校驗): 這種方法簡單粗暴, 就是報數據的每個字節(jié)循環(huán)往上累加, 如果累加溢出, 高位就不要了, 這種方法比較好算, 但是校驗的效果不夠理想, 萬一你的數據同時變動了兩個bit位(前一個字節(jié)少1, 后一個字節(jié)多1), 就會出現內容變了, CRC沒變這樣的情況.
- MD5: MD5是使用一系列公式來進行更復雜的數學運算, MD5算法得到的結果具有下面幾個特點.
- 定長:無論原始數據多, 得到的MD5值都是固定長度(4/8字節(jié)).
- 沖突概率很小: 原始數據即使變動了一點點, 算出來的MD5值都會差別差別很大(讓MD5結果更分散了).
- 不可逆: 通過原始數據計算出MD5比較容易, 但是通過MD5還原成原始數據很難, 理論上是不可實現的(計算量極大).
基于MD5的這些特點, 讓MD5適用于更多場景, 計算校驗和, 作為哈希函數計算Hash值, 用于加密等; 網上會看到一些解密MD5的方法, 都不是真正的解密, 因為真正的的想將數據還原回去憑借現 在的計算機是做不到的, 這里的解密實質上是將一些常見的字符串的MD5值進行匯總成一張表, 解密的過程相當于查表, 總不可能把所有的字符串都列出來吧...
- SHA1: SHA1和MD5是類似的, 只是運算過程不一樣吧, 這就屬于數學問題了...
三. TCP協(xié)議
1. TCP報頭介紹
TCP, 即Transmission Control Protoco, TCP協(xié)議相比于UDP協(xié)議要更復雜, TCP的特點是有連接, 可靠傳輸, 面向字節(jié)流, 全雙工.
TCP協(xié)議格式:
可以看到TCP報頭中報還的屬性還是很多的, 下面就來介紹這里的關鍵數屬性.
首先對于端口號和校驗和這幾個屬性這里就不做介紹了, 和UDP是一樣的.
數據偏移和選項(option), 選項可有可無也可以有多個, 用于對TCP一些功能的擴展和TCP中的一些屬性進行解釋說明, 可能包括 “窗口擴大因子”, “時間戳” 等選項, 數據偏移表示TCP數據起始處與TCP報文起始處之間的距離, 也就是 4 個比特位( 0 到 15 )表示TCP首部報頭的長度, 單位是 4 字節(jié);
正是由于TPC當中有了數據偏移和選項這兩個屬性, 致使使TCP的報頭長度是可變的, 不像UDP一樣固定是 8 字節(jié), 選項之前的部分是固定的長度( 20 字節(jié)), 選項長度 = 首部長度 - 20 字節(jié), 通過首部長度就可以去調節(jié)選項長度; 如果首部長度值是 5 , 表示整個TCP報頭是 20 字節(jié)(相當于沒有選項), 如果首部長度值是 15 , 表示整個TCP報頭是 60 字節(jié)(選項部分就是 40 字節(jié)), 填充是為了保證選項為 32 比特的整數倍.
保留項, 數據偏移后面還有 6 位保留項, 這里保留項的存在是為了未來TCP協(xié)議的拓展升級準備的, 網絡協(xié)議的拓展升級是一件成本極高的事情, 比如現在UDP協(xié)議報文長度是最大是 2 字節(jié)(64KB), 如果想要升級一下讓UDP的報文支持更大的長度, 在技術上可以實現, 但實際上要想讓世界上所有能上網的設備所安裝的各式的操作系統(tǒng)都能夠同步完成升級, 支持新的UDP, 這是不現實的; 一種系統(tǒng)升級了, 其他系統(tǒng)不升級, 就辦法進行通信了...
而像TCP這樣引入了 “保留位”, 如果在未來想要引入一些新的功能, 就可以使用這些保留位, 這樣對于TPC本來的報頭結構影響是比較小的, 針對不升級的設備也更容易兼容就版本的TPC.
剩下的屬性和TCP內部的工作機制有關, 在后文的內容中會引入介紹, 這里先簡單概述一下; 32位序號和 32 位確認序號和TCP確認應答機制有關; 16位窗口和滑動窗口, 流量控制機制有關; 16 位緊急指針要配合URG控制位一起使用的, 用于指明緊急數據之后正常數據的起始位置; 6位控制位從左至右分別為 , URG, ACK, PSH, RST, SYN, FIN, 含義如下:
- URG(Urgent Flag)
該位為 1 時, 表示包中有需要緊急處理的數據.
- ACK(Acknowledgement Flag)
該位為 1 時, 確認應答的字段變?yōu)橛行? TCP規(guī)定除了最初建立連接時的SYN包之外該位必須設置為1.
PSH(Push Flag)
該位為 1 時, 表示需要將受到的數據立刻傳給上層應用協(xié)議, PSH為 0 時, 不需要立即傳而是先進行緩存.
- RST(Reset Flag)
該位為 1 時表示TCP連接中出現異常必須強制斷開連接.
- SYN(Synchronize Flag)
用于建立連接, SYN為 1 表示希望建立連接, 并在其序列號的字段進行序列號初始值的設定(Synchronize本身有同步的意思, 也就意味著建立連接的雙方, 序列號和確認應答號要保持同步).
- FIN(Fin Flag)
該位為 1 時, 表示今后不會再有數據發(fā)送, 希望斷開連接.
2. TCP實現可靠傳輸的核心機制
2.1 確認應答
確認應答是實現TCP可靠傳輸最核心的機制, 這里的可靠并不是指發(fā)送方能夠百分百能將數據發(fā)送到接收方, 可靠傳輸是要盡可能的把數據發(fā)過去, 發(fā)送方能夠準確的知道接收方是否收到了數據.
比如, 女神跟你關系是非常要好的, 你發(fā)的消息她一定立刻會回, 當你給你的女神發(fā)消息說要請她吃麻辣燙, 當你看到她的回復的時候, 你就知道消息她是收到了的.
這里女神回復的好啊好啊就稱為 “應答報文”, 也叫ACK(acknowledge), TCP進行可靠傳輸最主要就是靠這個確認應答機制來保證的, A給B發(fā)一個消息, B收到之后就會返回一個ACK, A收到這個應答之后, 就知道了發(fā)的數據順利到達了(沒有丟包), 如果沒有收到ACK就說明A發(fā)的數據消息大概率不見了(丟包).
考慮更復雜的情況, 如果多條消息同時發(fā)送, 可能會發(fā)生 “后發(fā)先至” 的情況, 比如你問你的女神是否吃麻辣燙后緊接著又問一句: “女神, 女神, 你做我女朋友好嗎?”, 就可能會是下面的情況:
在網絡通信中, 兩個主機之間, 傳輸路線存在多條, 數據報 1 和數據報 2 可能走的都是不同路線, 而且設備的轉發(fā)速率也是有快有慢, 受這樣的網絡環(huán)境的影響, 這種后發(fā)先至的情況是很常見也是無法避免的, 收到消息的順序就會存在變數, 這樣應答錯亂后, 解析數據的含義就出現歧義了, 就如上圖, 如果按照正常的邏輯解析的話就是 “吃麻辣燙->滾, 做女朋友->好啊好啊”, 而實際上女神沒想做你女朋友...
為了解決上述先發(fā)后至的問題, TCP中就引入了序號和確認序號.
上面發(fā)送方的序號就是序號, 接收方的序號就是確認序號, 這樣即使出現先發(fā)后至的情況導致消息的順序錯位了, 也能區(qū)分清楚應答報文是針對哪一條消息做出的應答.
這里的序號和確認序號就是對應與TCP報頭中的序號與確認序號, 它們都是 32 位大小, ACK同樣也在報頭中占據 1 個比特位.
要注意任何一條數據(包括應答報文)都是有序號的, 但確認序號只有應答報文有, 是否為應答報文取決于ACK這個標志位是否為1, 如果為 1 就表示是應答報文, 如果是 0 就表示不是應答報文.
實際上的TCP序號并不是上面舉例那樣簡單的以1, 2這種方式編號, TCP是面向字節(jié)流的, 在實際的TCP傳輸中, 是針對每一個字節(jié)都進行了編號, 假設我們的一條數據長度是 1000 個字節(jié), 傳輸的數據第一個字節(jié)的序號是1, 由于這 1000 個字節(jié)都是屬于同一TCP報文的, 因此就將一條數據中的第一個字節(jié)作為了TCP報頭里記錄的序號也就是1, 代表要發(fā)送的這一條數據.
確認序號的取值是收到的數據的最后一個字節(jié)的序號+1, 接收方收到數據后會返回一個 1001 做為確認序號, 這里的確認序號表示著兩個含義:
- 小于 1001 的數據都已經確認收到了
-
發(fā)送方接下來應該從 1001 這個序號開始發(fā)送數據(接收放向A索要 1001 開始的數據)
這里總的來說就是, TCP可靠傳輸的能力最主要是通過確認應答機制保證的, 通過應答報文不僅可以讓發(fā)送方清楚的知道是否傳輸成功, 而且通過序號和確認序號對多組數據的應答對應關系進行了詳細的區(qū)分.
2.2 超時重傳
上面說的確認應答只是討論了數據順利傳輸的情況, 那如果出現意外了呢? 比如網絡原因可能會導致發(fā)送數據或者返回的ACK丟包了又該如何呢?
也好解決, 出現了丟包的情況, 發(fā)送方重新再發(fā)一個就行了, 這就是TCP引入的超時重傳機制, 當一個數據發(fā)送后, 如果在一個時間閾值內沒有收到ACK, 就認為是丟包了, 就會重發(fā)一份同樣的數據.
觸發(fā)重傳有兩種情況, 一是數據包丟了, 此時發(fā)送方等待一段時間沒有收到ACK就會重新發(fā)送.
第二種情況是返回的ACK丟包了, 對于發(fā)送方是不知道是自己這邊發(fā)送的消息丟包了還是對方返回的ACK出問題了, 此時就會按照最壞的情況, 重發(fā)數據, 這就可能導致接受方重復收到多次同樣的消息, 但TCP針對這種重復數據的傳輸是有特殊處理的, TCP實現了一個去重機制, TCP中存在一個發(fā)送緩沖區(qū)和接收緩沖區(qū)(操作系統(tǒng)內核里面的一段內存), 接收方拿到數據后是將數據放到了接收緩沖區(qū)(有阻塞隊列功能, 但不限于阻塞隊列)中, 緩沖區(qū)會根據數據的序號進行排序, 并且根據數據的序號, TCP很容易識別當前接收緩沖區(qū)里的這兩條數據是否是重復的, 如果重復就把后來的這份數據就直接丟棄了, 然后重新返回一個與之前相同的ACK.
然后就是說, 重傳也是有可能出現丟包的情況的, 可能會出現多次重傳都丟包的情況, 而在網絡正常的情況下丟包的概率是很小的, 兩次以及多次丟包的概率那就更加小了, 所以重發(fā)一定次數后就會降頻重發(fā)(), 還是不行就會認為是網絡出現故障了, TCP就會嘗試重置連接(斷開重連), 如果重置還是失敗, 就會徹底斷開連接了.
關于超時重傳這里, 總的來說就是, 由于去重和重新排序機制的存在, 發(fā)送方只要發(fā)現ACK沒有按時到達, 就會重傳數據, 即使數據的順序亂了, 重復了, 依賴于TCP報頭的序號, 接收方都能很好進行處理.
TCP的可靠傳輸就是通過確認應答和超時重傳來體現出來的, 其中確認應答描述的是傳輸順利的情況, 超時重傳描述的是傳輸出現問題的情況, 這兩者相互配合, 共同支撐整體的TCP可靠性.
3. 連接管理(三次握手, 四次揮手)
TCP要完成通信是需要先建立連接的, 所以就有了連接管理的機制, 連接管理一定程度上也可以體現TCP的可靠性, 但保證可靠傳輸最核心的機制還是上面介紹的確認應答和超時重傳.
TCP這里的連接指的是由一個四元組(源IP, 源端口, 目的IP, 目的端口)來標識, 一個連接建立完成就表示通信雙方知曉對方的IP和端口信息, 就是通信雙方各自都維護著連接這樣的一個數據結構,雙方把對方的地址信息都保存下來就是完成了連接, 而斷開連接就是把各自存儲的連接刪除掉.
對于TCP的連接管理就是建立連接(三次握手), 斷開連接(四次揮手)了.
3.1 建立連接(三次握手)
客戶端與服務器之間進行三次交互建立連接的過程被形象地稱為 “三次握手”, 在這三次交互中, 通信雙方要完成對彼此信息的記錄.還是你和你的女神聊天, 你想要讓女神做你的女朋友, 于是就有了下面的對話.
首先是你向女神表白, 女神接受了就表明, 你和女神就有了一個認同, 女神是你的唯一, 但此時你和女神還沒有成為男女朋友的關系, 因為女神不知道你是不是她的唯一(萬一你腳踏好幾只呢...),然后女神也對你表白, 你有接收了, 此時就說明你倆互為唯一了, 這就真正建立了男女朋友關系, 相當于客戶端和服務器建立成功了.
把每次通信形象的稱為一次揮手, 上面的過程就是四次揮手了, 但在實際TCP的建立連接過程上是三次揮手, 其實就相等于于把上面女神的兩條消息合并成了一條消息.
所以, 三次握手本質上是四次信息交互, 通信雙方各自需要發(fā)送一個 “建立連接” 的請求, 然后再各自向返回一個ACK, 中間兩次是合并完成的, 而且必須是三次握手, 兩次握手是不行的(沒有最后一次, 女神就不知道你是不是她的唯一了...)
我們知道客戶端是主動發(fā)出請求的一端, 服務器是被動響應的, 所以, TCP建立連接需要讓客戶端向服務器發(fā)送一個連接請求, 即SYN, 然后服務器收到請求后會給客戶端響應一個ACK和SYN, 客戶端收到服務器的SYN后會立即發(fā)送一個ACK給服務器, 服務器收到客戶端的確認應答后,客戶端與服務器就連接成功了.
要注意理解的是, 服務器給客戶端發(fā)送的ACK和SYN這兩次通信是必須要進行合并的, 因為封裝分用兩次一定是比一次成本要高的, 而建立連接的這四次信息交互是在純內核中完成的, 服務器的系統(tǒng)內核是可以做到收到SYN之后就立即同時發(fā)送ACK和SYN的, 也就必須是這樣做.
三次握手的作用不限于建立連接, 除此之外三次握手還能檢測發(fā)送能力與接收能力是否正常, 還是你和女神之間的日常, 你和女神帶著耳機連麥打游戲, 有如下場景:
第一次通信, 當女神聽到你的聲音時, 女神就知道了你的的麥克風和女神的耳機是都是可以使用的, 而你現在還什么都判斷不了; 第二次通信, 當你聽到女神的回復, 你就知道了你和女神的麥克風和耳機都是可以使用的, 但此時女神還不知道你的耳機和她的麥克風能不能使用; 第三次通信, 當女神再次聽到你的回復時間, 你和女神就都知道了你們雙方的耳機和麥克風都是可以正常使用的,這里的麥克風就對應發(fā)送能力, 耳機就對應接收能力.
再從實際上TCP的三次握手來說, 當發(fā)送方發(fā)出SYN后, 接收方都到發(fā)送方的SYN后, 此時接收方就能夠確定發(fā)送方的發(fā)送能力和接收方的接收能力是正常的, 然后接收方回應ACK和SYN, 當接收方收到ACK和SYN后, 就知道了發(fā)送方和接收方的發(fā)送能力, 接收能力都是正常的, 最后發(fā)送方回應ACK給接收方, 此時接收方也確定了接收方和發(fā)送方的接收能力, 發(fā)送能力都是正常的.
所以, 總結一下三次握手的意義就是:
- 讓通信雙方各自建立對對方的 “認同”.
- 驗證通信雙方各自的發(fā)送能力和接收能力是否正常
- 在握手的過程中, 雙方還會 “協(xié)商” 配置一些重要的參數(完成一些數據的同步).
發(fā)起建立建立連接請求的報文就稱為SYN, 也叫同步報文段, SYN是TCP首部控制控制位當中的一位, 這個標志位為 1 就表示是請求建立連接的報文, 其他控制位同樣如此, 用 0 和 1 來進行控制.
在建立連接的過程中, 服務器與客戶端是存在著不同的狀態(tài)的, 不同的狀態(tài)體現了TCP當前的工作, 具體如下:
- CLOSED 表示客戶端或服務器處于關閉狀態(tài).
- LISTEN 表示服務器已經準備就緒, 等待客戶端連接的狀態(tài).
- SYN_SENT 表示客戶端連接請求已發(fā)送, 此時客戶端進入阻塞等待服務器確認應答狀態(tài), 一般此狀態(tài)的存在時間很短.
- SYN_RCVD 表示服務器已經收到客戶端的連接請求, 發(fā)送ACK和SYN并進入阻塞等待客戶端連接狀態(tài), 一般此狀態(tài)存在時間很短.
- ESTABLISHED 表示客戶端或服務器已經建立成功連接, 隨時可以進行通信, 要注意理解, 兩次握手后, 從客戶端來看, 客戶端已經把該發(fā)送的和該接收的都完成了, 此時客戶端就認為進行成功建立連接; 而對于服務器, 當第三次握手后才能認為成功建立連接.
三次握手這里也能一定程度上保證TCP的可靠傳輸, 但只是輔助做作用, 真確保可靠傳輸個關鍵機制還是上面介紹的確認應答和超時重傳.
3.2 斷開連接(四次揮手)
與三次握手類似, 客戶端與服務器通過四次交互斷開連接的過程稱為 “四次揮手”, 通信雙方向對方發(fā)起一個斷開連接的請求FIN, 再各自給對方一個回應ACK, 這個時候你和女神的緣分就到了盡頭了, 出現了下面的場景:
斷開連接的請求也被稱為FIN, 在TCP報文中也是一個控制位, 斷開連接的請求可以是客戶端先發(fā)起, 也可以是服務器先發(fā)起, 這里以客戶端主動斷開連接為例繼續(xù)介紹.
這里與三次握手需要區(qū)別的是, 斷開連接的過程中, 中間兩次通信通常是不能合并的, 這是因為FIN的發(fā)起并不是由系統(tǒng)內核控制的, 而是由應用程序調用socket的close方法(或者進程退出)才會觸發(fā)FIN, ACK則是由系統(tǒng)內核控制的, 所以, 上圖描述的客戶端發(fā)出斷開連接FIN請求, 然后服務器收到請求后立即響應ACK(內核返回ACK), 但此時并不能確定服務器應用層程序到底什么時候才會調用socket.close()從而觸發(fā)返回給客戶端的FIN.
也就是說, 四次揮手不像三次握手那樣是在重內核中完成的, 三次握手可以做到中間兩次交互的時機是相同的, 而四次揮手的中間兩次交互時機首否是相同的完全取決于服務器應用層的代碼是怎么寫的, 如果特殊情況下中間的兩次交互時機是相同的也有可能是合并完成的.
同樣斷開連接過程中, 客戶端和服務器的的狀態(tài)如下:
- FIN_WAIT_1 出現在主動發(fā)起斷開連接的一方, 主動方發(fā)送FIN后, 進入等待被動方確認斷開響應狀態(tài).
- FIN_WAIT_2 1 出現在主動發(fā)起斷開發(fā)起連接的一方, 收到被動方的ACK, 進入等待被動方FIN狀態(tài)
- CLOSE_WAIT 出現在被動發(fā)起斷開連接的一方, 當被動方收到主動方發(fā)送的FIN請求后, 被動方響應ACK, 然后等待關閉連接( 等待socket調用close方法).
- LAST_ACK 出現在被動發(fā)起斷開連接的一方, 被動方FIN發(fā)送后, 進入等待最后一個ACK狀態(tài).
- TIME_WAIT 出現在主動發(fā)起斷開發(fā)起連接的一方, 收到被動方FIN, 發(fā)送最后一次ACK, 然后繼續(xù)保持當前的TCP狀態(tài), 再等一會兒后釋放連接.
這里重點要理解的是TIME_WAIT這個狀態(tài), 從上圖來看在客戶端看來它的將最后一次ACK發(fā)出去后, 四次揮手就已經是完成了? 那為什么TIME_WAIT這里還要等待一會而不是立即釋放連接, 這是因為最后一次客戶端發(fā)送ACK后是可能存在丟包的情況的, 在三次握手和四次揮手的過程中, 同樣是存在超時重傳的, 如果丟包了, 服務器就會以最壞情況認為自己的FIN丟了, 會重發(fā)FIN, 此時客戶端就需要等待以預防服務器重發(fā)FIN的這種情況, 因此使用TIME WAIT狀態(tài)保留一定的時間, 就是為了能夠處理最后一個ACK丟包的情況, 能夠在收到重傳的FIN之后, 進行ACK響應.
TIME_WAIT這里等待的時間為2MSL , MSL表示報文最大生存時間(通常是60s), 也就是在兩個節(jié)點進行網絡傳輸過程中消耗的最大時間, 如果TIME_WAIT維持了2MSL都沒用收到重傳的FIN, 就認為最后一個ACK順利到達了, 服務器與客戶端就完全斷開連接了.
4. 滑動窗口
上面介紹的TCP機制都是再給TCP的可靠性提供支持, 但保證了可靠性其實就犧牲了一定的效率, 滑動窗口做的事情就是在保證傳輸的可靠性的基礎上, 盡量地去提高傳輸效率.
在進行IO操作的時候, 時間成本主要是兩個部分, 一是等, 二是數據傳輸, 大多數情況下, IO花的時間成本大頭都是在等上面, 滑動窗口本質上就是降低了等待確認應答ACK消耗的時間.
對于基本的確認應答機制來說, 每發(fā)送一次數據, 都需要等待ACK返回后才能進行下一次發(fā)送, 這樣大部分的時間都用在等ACK上了.
滑動窗口的本質就是不進行等待發(fā)送多條數據, 然后使用一份時間來等待多個ACK返回.
把不需要等待, 就能直接發(fā)送的最大數據量, 稱為 “窗口大小”, 上圖中窗口的大小就是4000, 客戶端發(fā)送了 4 條數據之后并不是等到 4 個ACK都都返回后才能繼續(xù)發(fā)送, 而是每收到一次ACK就繼續(xù)發(fā)下一條數據, 這樣就讓客戶端這里等待ACK的數據始終始終都是 4 條, 就如上圖, 客戶端發(fā)出1-1000,1001-2000, 2001-3000, 3001-4000這四條數據后, 客戶端收到1001, 緊接著就發(fā)送4001-5000 這條數據, 收到2001, 就繼續(xù)發(fā)送5001-6000...
這里圖中本來等待ACK是1001-5000, 接下來, 收到了 2001 這個ACK, 就說明 2001 之前的數據(1001-2000)已經被確認了, 此時就可以立即發(fā)送5001-6000的數據, 此時意味著等待ACK的范圍就是2001-6000, 這就相當于一個大小始終不變的窗口, 但窗口框住的數據變了, 相當于窗口向右滑動了一格, 所以這里就形象的稱為 “滑動窗口”.
上述是正常傳輸的情況, 那如果丟包了應該如何處理呢? 下面就來分析一下,
情況1, ACK丟了
這種情況下是不用做任何的處理的, 數據還是能夠正常傳輸, 比如 1001 丟了, 但實際上1-1000的數據是服務器是收到了的, 當客戶端收到 2001 時就表明 2001 之前的數據都已經確認到達服務器, 就會接著再發(fā)送兩條數據, 所以只要大部分的ACK沒有丟, 客戶端可以通過下一次或者后面的確認應答序號來進行確認, 不處理也沒事.
情況2, 數據丟了
數據都丟了, 這就必須得處理了, 比如1-3000的數據中, 其中1001-2000的數據丟了, 那服務器每收到一個數據, 都會返回1001, 表示讓客戶端重傳1001-2000這個數據, 當客戶端收到若干個個相同的確認應答序號時, 就明白了, 數據丟了, 就會對丟失的數據進行重傳, 直到服務器收到1001-2000的數據, 就會返回最新的確認應答序號, 當然, 如果中間還有數據都丟包,返回的就是新丟的包的序號了, 然后還是上述操作.
這種丟包重傳的方式被稱作 “ 快速重傳 ”, 讓重傳操作只重傳了丟失的數據, 可以視為是超時重傳機制在滑動窗口下的變形; 如果當前傳輸數據密集, 按照滑動窗口的方式來傳輸, 此時按照快速重傳來處理丟包; 如果當前傳輸數據稀疏, 就不再按照滑動窗口方式了傳輸了, 此時還是按照之前的超時重傳處理丟包.
5. 流量控制
滑動窗口機制是在提高TCP的傳輸效率, 窗口越大, 傳輸效率就越高, 但是窗口也是不能無限大下去的.
首先如果窗口無限大了, 那么一個窗口就把數據都發(fā)完了, 也就是完全沒有在等ACK了, 數據傳輸的可靠性就得不到保障了, 這就和TCP的初心背道而馳了, 而且窗口太大, 也會消耗大量的系統(tǒng)資源.
然后就是說, 窗口大了, 發(fā)送方的發(fā)送效率確實提高了, 但是接收方能接受得過來嗎? 但是如果發(fā)送速度過快, 接收方的接緩沖區(qū)滿了之后, 接收方就處理不過來了, 白發(fā)了...
所以并不是窗口大小越大, 傳輸效率就越高, 只有保證發(fā)送方發(fā)送與接收方接收的速率最大并保持一致時, 傳輸效率才是最高的, 而流量控制要做的工作就是根據接收方的處理能力, 動態(tài)協(xié)調發(fā)送方的發(fā)送速率.
接收方的處理能力是通過接收方緩沖區(qū)的剩余容量來衡量的, 接收方緩沖區(qū)的容量剩余多少, 下次發(fā)送方的窗口大小就是多少, 可以接收方的緩沖區(qū)想象成一個蓄水池, 那么發(fā)送方的工作就是注水, 接收方的工作就是使用水池中的水, 當水位比較低(剩余空間大)那就注水的時候就快一點, 水位比較高(剩余空間小)那就注水的時候就慢一點, 池子滿了就暫時先停止注水.
當發(fā)送方的數據到達接收方的時候, 接收方都會返回一個ACK,這個ACK除了確認能夠確認應答, 還能告知接收方緩沖區(qū)的剩余容量, 然后發(fā)送方就會根據接收方緩沖區(qū)的剩余容量來控制發(fā)送速度(窗口大小), 當接收方得知接收方緩沖區(qū)空間滿了的時候, 就暫時不會發(fā)送數據了, 而是會定期去給接收方發(fā)送一個探測窗口報文, 這個報文不攜帶具體的業(yè)務數據, 只是為了觸發(fā)ACK查詢接收方緩沖區(qū)的剩余容量.
上面獲取接收方緩沖區(qū)的剩余容量, 是通過TCP報頭中的窗口大小來進行獲取的, 占 16 個比特位(即64kb), 但這并不意味著窗口大小最大就是64kb, 因為TCP報頭的選項部分里面有一個窗口擴大 因子M, 實際窗口大小是將窗口大小字段左移M位, 也就是擴大 2 ^ M 倍, 比如窗口大小已經是64kb, 如果擴展因子M標識為2, 最大的窗口大小就拓展為了256KB(64KB << 2).
6.擁塞控制
流量控制是考慮到了接收方的處理能力來調節(jié)發(fā)送方的窗口大小, 而實際上數據傳輸不單單是簡單的從發(fā)送方直接到了接收方, 往往還有復雜的中間轉發(fā)過程(眾多交換機和路由器等節(jié)點), 擁塞控制描述的是傳輸過程中中間節(jié)點的處理能力, 同樣的如果中間轉發(fā)過程中鏈路的擁堵了, 那接收方的處理能力再快也是白搭的(木桶效應).
擁塞控制本質上就是通過實驗的方式來逐漸找到一個合適的窗口大小(合適的發(fā)送速率).
接收方處理能力是好量化衡量的, 但是由于設備眾多, 數據每次傳輸路線也大概率是不相同的... 眾多影響因素導致中間節(jié)點的處理能力是不好量化衡量的, 因此擁塞控制采取了 “測試實驗” 的方式逐漸調整不同情況下合適的發(fā)送速度.
初始的時候接收方會以較小的窗口進行發(fā)送(0輪, 窗口大小是1, 但不是一個字節(jié)), 由于初始窗口比較小(發(fā)送速率慢), 每一輪不丟包都會使窗口大小擴大一倍(指數增長), 當增長速率達到閾值之后, 指數增長就成為了線性增長, 再當窗口達到一定的大小, 就會出現丟包的情況, 這就意味著鏈路就出現了 “擁堵”, 說明此時發(fā)送的速率已經接近網絡的極限的; 此時就會減小窗口的大小(速度很 快, 立馬縮成很小的值), 因為如果出現丟包減小窗口大小的速度不夠大, 可能會出現持續(xù)性的丟包, 對網絡通信的質量會造成很大的影響, 然后就是重復剛才指數增長和線性增長的過程了.
擁塞窗口不是固定數值, 而是一直動態(tài)變化的, 隨著時間的推移, 逐漸達到一個動態(tài)平衡的過程, 使窗口大小隨著網絡的動態(tài)變化而動態(tài)變化.
實際上的窗口大小是擁塞控制和流量控制共同決定的, 取的是擁塞窗口和流量控制窗口的較小值.
7. 延時應答
延時應答也是提升TCP效率的機制, 流量控制是為了在接收方能夠處理得了的前提下, 盡可能的把窗口大小放大一點, 延時應答相當于流量控制的延伸, 想要在此基礎上, 讓窗口的大小盡量再大一點.
就是接受方收到數據之后, 不是立即返回ACK了而是稍微等會再返回, 等待的時間里, 接收方的應用程序就能夠把接收緩沖區(qū)的數據給再處理一波, 此時接收緩沖區(qū)的剩余容量就更大了.
實際上延時應答采取的方式, 就是在滑動窗口下, ACK不再每一條數據都返回了, 比如下圖就是隔一條返回一個ACK.
實際上接收緩沖區(qū)剩余空間大小的變化是一個復雜的過程, 既取決于發(fā)送方的發(fā)送也取決于接收方的處理.
8. 捎帶應答
同樣捎帶應答也是提升TCP效率的機制, 是延遲應答的延伸, 由于延時應答的存在, 接收方并不是立即就返回響應ACK的, 而很多情況下, 客戶端服務器在應用層也是 “一發(fā)一收” 的, 當服務器的應用層有業(yè)務數據要發(fā)送給客戶端時, 就可以捎帶的將ACK一起發(fā)送, 此時應用層代碼需要響應的時機與ACK響應時機重合的, 就可以將這兩個數據合二為一進行發(fā)送, 結合下圖理解, 還是你和女神的日常,
圖中你給女神發(fā)送的ACK是由系統(tǒng)內核返回的, 業(yè)務數據是由應用程序發(fā)送的, 這兩條數據的發(fā)送本來是在不同的時機發(fā)送的, 由于延時應答機制的存在, 就導致等待ACK的過程中, 接收方就要發(fā)送業(yè)務數據給發(fā)送方了, 此時就可以讓業(yè)務數據捎上這個ACK一起發(fā)過去就行了.
也就是說, 上面的ACK和業(yè)務數據本來是在不同的時機的, 但在延時應答的情況下是可能成為相同時機的, 然后就合并發(fā)送了, 延時應答是提高了這里合并的概率, 捎帶應答就是針對這種能合并的情況進行的特殊處理.
9. 面向字節(jié)流(粘包問題)
TCP是面向字節(jié)流的, 在接收緩沖區(qū)其實是把多個數據都放到一起的, 這就導致應用層去使用read()讀取緩沖區(qū)的數據時, 會出現分不清讀到哪里才算是一個完整的應用層數據報, 由于TCP是面向字節(jié)流的, 那么一次讀 1 個字節(jié)或者讀N個字節(jié), 都是可以的, 這就導致一次讀到的數據可能是半個應用層數據報, 可能是一個應用層數據報, 也有可能是多個應用層數據報...
也就是說在TCP層次的socket API中是沒有告訴我們應該讀幾個字節(jié)的, 具體怎么讀, 完全是由程序員自己負責, 但我們所希望的是每次讀的是一個完整的應用層數據報, 這就是需要程序員自己去解決了.
其實解決方案也很簡單, 程序員是可以控制應用層協(xié)議 的, 只需要在應用層代碼中約定好應用層數據報和應用層數據報之間的邊界就好了, 比如可以應用層數據報結尾約定一個分隔符, 這樣在讀取的時候, 就能區(qū)分出一個完整的應用層數據報了; 也可以約定好每個包的長度, 讀取時先讀取長度, 讓然后再讀取讀到長度的字節(jié)數就能得到完整的數據報了.
10. TCP下的異常情況
在進行TCP協(xié)議傳輸過程中會出現由于不可抗力導致的異常情況, 針對如下幾種進行簡單介紹:
????進程崩潰了(進程終止)
TCP連接是通過socket來進行連接的, socket本質上是進程打開維護的一個PCB, 進程終止了, 對應的PCB就沒了, 再對應在文件描述表中的位置就釋放了, 就相當于文件自動關閉了, 這個過程和手動調用socket.close方法沒有區(qū)別, 系統(tǒng)內核依然會完成四次揮手的過程, 此時其實還時一個正常斷開連接的流程.
主機關機(按照正常流程關機)
主機關機首先終止的是進程, 就和上面一樣的, 還是會觸發(fā)四次揮手, 然后正式關機.
主機掉電
當電源或網絡直接斷開時, 是沒有任何時間留給操作系統(tǒng)去反應的, 所以根本來不及去完成四次揮手.
假設是接收方掉電了, 此時發(fā)送方仍然是繼續(xù)在發(fā)數據的, 發(fā)完數據要等待ACK返回, 接收方都掛了肯定時傳不了了, 那么發(fā)送方就會進行超時重傳, 但不管怎么重傳, 都是收不到ACK的, 重傳了 幾次, 還是沒有收到ACK, 發(fā)送方就會嘗試重置TCP連接, 顯然這個重置也會失敗, 然后發(fā)送方就會單方面放棄連接了.
TCP重置連接的報文的是通過復位報文段來判斷的, 即RST, 也是TCP報頭中控制位中的一位.
再考慮發(fā)送方掉電的情況, 此時接收方會發(fā)現, 發(fā)送方很差時間沒有數據發(fā)送過來了, 但從接受方的角度來看, 接受方不知道是發(fā)送方掛了還是發(fā)送方在組織數據, 所以針對這種情況, 接受方會周
期性的給發(fā)送方發(fā)送一個探測報文, 觸發(fā)服務器的ACK, 如果沒有反應, 就說明是發(fā)送掛了.
這樣的探測報文也被形象的叫做 “ 心跳包 ”, 用來確認通信雙方是否處在正常的工作狀態(tài)中, 因為心跳是周期性的, 如果心跳沒了, 說明就掛了, 心跳包是非常常見并且經常用到的?;顧C制.
最后在這里對比一下UDP和TCP, TCP優(yōu)勢在于可靠傳輸, 在絕大部分場景中都需要進行的是可靠傳輸; 而UDP優(yōu)勢在于高效率, 如果有些場景對于性能要求更苛刻使用UDP就很合適, 比如同一個機房內部的服務器之間通行就可以使用UDP, 因為這種場景下的網絡結構相對簡單, 網絡帶寬也是比較充裕的, 轉發(fā)設備也是比較好的設備, 整體丟包的可能性就比較小了, 這里就可以要求以更高的效率進行傳輸; UDP還有一個天然的優(yōu)勢就是支持廣播, IP地址中有一種特殊的地址叫 “廣播IP”, 通過UDP往廣播IP上發(fā)送數據報, 此時該局域網內所有的設備都能收到數據.
TCP是個非常復雜的協(xié)議, 上面所介紹的十大特性只是TCP中比較核心的特性, 其他的就不在這里介紹了, 傳輸層的協(xié)議也不只有UDP與TCP這兩個, 比如還有KCP, QUIC等, 在游戲場景中經常使用。