? ? ? ? 之前我們已經使用udp/tcp的相關接口寫了一些簡單的客戶端與服務端代碼。也了解了協議是什么,包括自定義協議和知名協議比如http/https和ssh等。現在我們再回到傳輸層,對udp和tcp這兩傳輸層巨頭協議做更深一步的分析。
一.UDP
? ? ? ? UDP相關內容很簡單,因為它不保證可靠性,面向數據報的特性。常用于直播和游戲等場景,所以我們分析也會簡單些。
1.1UDP協議端格式?
? ? ? ? UDP報文的格式非常簡單。16位源/目的端口號,這個不多介紹。16位UDP長度表示整個數據報(UDP 首部+UDP 數據)的最大長度,16位檢驗和則是確認UDP報文是否正確,如果錯誤直接丟棄。所以我們說UDP是面向數據報的,本身也不會產生粘包的問題。
1.2UDP的特點
????????UDP 傳輸的過程類似于寄信,也就是說,我們自己(應用層)吧信(數據報)寄出去后。郵局派人(UDP)將我們的信封放到我們事先說好的位置(目的端口號)后,如果說信封在郵箱里面別人取錯了。原來要收我們信件的人此時就收不到信了。但郵局不管,想要對方收到信我們只能自己重發一次再郵寄出去。
所以我們可以總結UDP如下的幾個特點:
- 無連接: 知道對端的 IP 和端口號就直接進行傳輸, 不需要建立連接;
- 不可靠: 沒有確認機制, 沒有重傳機制; 如果因為網絡故障該段無法發到對方,UDP 協議層也不會給應用層返回任何錯誤信息;
- 面向數據報: 不能夠靈活的控制讀寫數據的次數和數量;
1.3面向數據報?
? ? ? ? 我們可以這樣理解,應用層交給 UDP 多長的報文, UDP 原樣發送, 既不會拆分, 也不會合并;用 UDP 傳輸 100 個字節的數據。也就是說如果發送端調用一次 sendto, 發送 100 個字節, 那么接收端也必須調用對應的一次 recvfrom, 接收 100 個字節; 而不能循環調用 10 次 recvfrom, 每次接收 10 個字節。
1.4UDP的緩沖區
- UDP 沒有真正意義上的 發送緩沖區. 調用 sendto 會直接交給內核, 由內核將數據傳給網絡層協議進行后續的傳輸動作;
- UDP 具有接收緩沖區. 但是這個接收緩沖區不能保證收到的 UDP 報的順序和發送 UDP 報的順序一致; 如果緩沖區滿了, 再到達的 UDP 數據就會被丟棄;
所以我們此時就明白了為什么UDP是全雙工的,因為UDP的socket 既能讀,也能寫。?
1.5UDP使用時的注意事項?
????????我們注意到, UDP 協議首部中有一個 16 位的最大長度. 也就是說一個 UDP 能傳輸的數據最大長度是 64K(包含 UDP 首部).然而 64K 在當今的互聯網環境下, 是一個非常小的數字.如果我們需要傳輸的數據超過 64K, 就需要在應用層手動的分包, 多次發送, 并在接收端手動拼裝;
? ? ? ? 基于UDP知名的應用層協議有:NFS: 網絡文件系統,TFTP: 簡單文件傳輸協議,DHCP: 動態主機配置協議,BOOTP: 啟動協議(用于無盤設備啟動),DNS: 域名解析協議等等。
二.TCP?
? ? ? ? 在分析TCP之前,博主想多提一嘴。我們經常說UDP比TCP快,但博主自己認為這是不太對的。快也是要就場景的不同而言的,有的場景下TCP還比UDP快呢。所以我們不談UDP和TCP誰快誰慢的問題。歸根結底, TCP 和 UDP 都是程序員的工具, 什么時機用, 具體怎么用, 還是要根據具體的需求場景去判定。
2.1TCP協議與其協議端格式
????????TCP 全稱為 "傳輸控制協議(Transmission Control Protocol"). 人如其名, 要對數據的傳輸進行一個詳細的控制。
- 源/目的端口號: 表示數據是從哪個進程來, 到哪個進程去。
- 序號與確認信號:個人認為是TCP中最為重要的部分,我們后面結合場景細致分析。
- 4位TCP首部長度:表示TCP報頭長度大小。首先4位bits可以表示0~15的大小。因為我們的每個選項都是4字節的,所以0~15的每一個單位表示4字節大小。也就是說TCP報頭實際長度為:4位首部長度*4(字節)。又因為選項最多有10個,最少為0個。所以TCP的4位首部長度取值范圍為5~15(20字節大小~60字節大小)。
- 6位保留位:是預留給未來擴展使用的字段,當前協議(RFC 793及后續標準)中并未定義其具體用途。它們的默認值為全0,發送方必須將這些位設置為0,接收方則會忽略其值。?
- 6位標志位: URG(確認緊急指針是否有效),ACK(表示當前報文中的確認序號是否有效),PSH(催促對端應用程序盡快把數據從緩沖區讀走),RST(表示對方請求重新連接,也就是重新進行三次握手過程),SYN(對端請求建立連接; 我們把攜帶 SYN 標識的稱為同步報文段),FIN(對端請求斷開連接,我們稱攜帶 FIN 標識的為結束報文段)。
- 16位窗口大小:表示發送當前報文的一方TCP緩沖區的剩余大小,后面我們還會再提到它。
- 16位校驗和:與UDP一樣,確認報文的合法性,錯誤了直接丟棄。由發送端填充, CRC 校驗. 接收端校驗不通過, 則認為數據有問題. 此處的檢驗和不光包含 TCP 首部, 也包含 TCP 數據部分。
- 16位緊急指針:標識那部分是緊急數據,位置為報頭首部地址+緊急指針大小。其實也就是記錄了緊急數據位置相對于報頭首部地址的偏移量。
- 40字節頭部選項:暫時不提。
????????那么TCP是如何保證其傳輸的可靠性的呢,有什么優化傳輸速度的機制嗎?我們從下面幾個TCP的核心機制來分析。?
2.2確認應答機制?
? ? ? ? TCP的確認應答機制是TCP可靠性保證的基石。因為我們最終的目的是要讓對方收到我發出的數據,怎么能確認100%收到對方發送的數據呢。也就是確認應答機制,但這里說的確認是對歷史數據的100%確認,我們下面細說:?
我們以C->S一個傳輸方向為例:
?
? ? ? ? (多提一嘴,為什么我們畫箭頭是斜著畫的而不是平直的呢,這是因為報文到達對端需要一定的時間,斜著畫能體現出這種時間間隔)。?
????????當客戶端發送數據給對端時,對方會發送一個攜帶有確認應答號的報文(此時報文中的ACK置為1)。那我們此時就能夠確定我們歷史上發給服務端的1000序號以前的數據都被服務端收到了。那服務端如何保證自己的應應答報文被客戶端收到了呢?
? ? ? ? 客戶端也發一個ACK回去嗎,顯然這又成了一個雞生蛋,蛋生雞的問題。所以我們的策略是:不對應答做應答,使用超時重傳機制來讓服務端確定對端是否收到應答報文。
2.3超時重傳機制?
我們先說數據丟失的情況:
????????主機 A 發送數據給 B 之后, 可能因為網絡擁堵等原因, 數據無法到達主機 B,如果主機 A 在一個特定時間間隔內沒有收到 B 發來的確認應答, 就會進行重發。
再說我們之前提到的ACK丟失的情況:
????????因此主機 B 會收到很多重復數據。那么 TCP 協議需要能夠識別出那些包是重復的包,并且把重復的丟棄掉,這時候我們可以利用前面提到的序列號,就可以很容易做到去重的效果。
? ? ? ? 那么這個超時的時間大小如何確定呢?最理想的情況下, 找到一個最小的時間, 保證 "確認應答一定能在這個時間內返回".但是這個時間的長短, 隨著網絡環境的不同, 是有差異的.如果超時時間設的太長, 會影響整體的重傳效率;如果超時時間設的太短, 有可能會頻繁發送重復的包;
????????TCP 為了保證無論在任何環境下都能比較高性能的通信, 因此會動態計算這個最大超時時間。具體的過程如下:
- Linux 中(BSD Unix 和 Windows 也是如此), 超時以 500ms 為一個單位進行控制, 每次判定超時重發的超時時間都是 500ms 的整數倍.
- 如果重發一次之后, 仍然得不到應答, 等待 2*500ms 后再進行重傳.
- 如果仍然得不到應答, 等待 4*500ms 進行重傳. 依次類推, 以指數形式遞增.
- 累計到一定的重傳次數, TCP 認為網絡或者對端主機出現異常, 強制關閉連接.
2.4連接管理機制
在正常情況下, TCP 要經過三次握手建立連接, 四次揮手斷開連接:
由于TCP中通信雙方的地位是對等的,所以我們只分析客戶端主動向服務端發起連接/斷開請求的情況。服務端主動的情況是一樣的。?
2.4.1三次握手過程
客戶端流程:connect()
?→ 發送SYN
?→?SYN_SENT
?→ 收到SYN+ACK
?→ 發送ACK
?→?ESTABLISHED。
服務端流程:listen()
?→ 收到SYN
?→ 發送SYN+ACK
?→?SYN_RCVD
?→ 收到ACK
?→?ESTABLISHED
?→?accept()
返回confd
?。
? ? ? ? 為什么要進行三次握手,因為通信必須要雙方同意才能進行。就比如結婚,一方如果不同意怎么結嗎。其次,此過程中雙方也進行了數據序號起始位置的交換,同時也以最短的方式驗證了雙方的全雙工能力是否正常。(因為此時雙方都能進行正常的接收發送)?
? ? ? ? 其次我們發現ACK其實在傳輸過程中在雙方的報文99%的情況下是置為1的,什么時候不置為1呢,一個是真失效了,還有一個就是主動發起連接的一方首次發送SYN給對端請求建立連接時。
? ? ? ? 還有幾個細節問題,第一個既然要雙方同意,不應該是4次握手嗎,為什么是三次握手。因為服務端一般對客戶端的請求是無條件同意的(這個我們下面說四次揮手時就明白了)。
????????第二個,TCP為了數據傳輸的高效性,通常會在應答報文中捎帶上我當前端要發的數據。那么這個捎帶數據不可能在前兩次握手中出現。也就是說前兩次握手時,雙方發給對端的報文只有報頭沒有數據。因為此時連接并沒有建立,不能發送數據給對方。
2.4.2四次揮手過程?
? ? ? ? 因為雙方斷開連接,需要雙方均同意才會斷開連接。當主動斷開方發送的FIN請求到達對端時,對端此時進入半關閉狀態(如果想要看到這個狀態可以把服務端代碼中的close(fd)刪除即可)CLOSE_WAIT。并發送ACK到主動斷開方,主動斷開方收到ACK后進入FIN_WAIT_2狀態。
? ? ? ? 過了一段時間,服務端想要與客戶端斷開連接(調用close(fd)),發送FIN請求到客戶端,客戶端收到服務端斷開連接請求后進入TIME_WAIT狀態,并發送ACK報文給對方。客戶端等待一段時間后進入CLOSED狀態。服務端收到ACK后當前通信文件立馬進入CLOSED狀態。
TIME_WAIT狀態
? ? ? ? 為什么主動斷開連接的一方要等待一段時間呢?因為有的時候,對端在發送完FIN請求后。TIME_WAIT這段時間內有可能收到之前因為網絡問題此時才到的對端之前發送的報文(因為我們的報文為了效率是并發的而不是上面那樣一個一個發的,下面我們會說這個問題,此處先了解即可)。所以他的第一個作用為處理過期報文。
? ? ? ? 第二個作用,確保發送的ACK到達對端。因為超時重傳機制,對端沒有收到我發回的ACK時會重發FIN請求。收到了就不發了。也就是說在TIME_WAIT時間內,只要客戶端沒有收到消息,我客戶端就認為對方收到了我的ACK應答。
? ? ? ? 唉,這時候就有人要鉆牛角尖了。那如果更為極端的一種情況,過了TIME_WAIT時間后,再使用原來的端口號綁定,這個過期報文此時到達了,此時怎么辦。
-
需要同時滿足以下極不可能的條件:
-
報文在網絡中存活超過MSL(違反IP協議規范)
-
新連接的五元組完全匹配舊連接
-
新連接的初始序列號恰好與舊報文匹配
-
-
現代TCP實現會使用隨機化初始序列號(ISN),使得這種碰撞概率約為1/4 billion
????????所以基本上不太可能,有了我們也不考慮。因為工程上不會為了?“理論上可能,但實際幾乎不會發生”?的情況增加過多復雜度。
那么TIME_WAIT等待的時間是多長呢,為什么??
????????TIME_WAIT 的時間是 2MSL,MSL 在 RFC1122 中規定為兩分鐘,但是各操作系統的實現不同, 在 linux?上默認配置的值是 60s。可以通過 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看 msl 的值。
????????MSL 是 TCP 報文的最大生存時間, 因此 TIME_WAIT 持續存在 2MSL 的話就能保證在兩個傳輸方向上的尚未被接收或遲到的報文段都已經消失(否則服務器立刻重啟, 可能會收到來自上一個進程的遲到的數據, 但是這種數據很可能是錯誤的);同時也是在理論上保證最后一個報文可靠到達(假設最后一個 ACK 丟失, 那么服務器會再重發一個 FIN. 這時雖然客戶端的進程不在了, 但是 TCP 連接還在, 仍然可以重發 LAST_ACK)。
bind_error的原因?
? ? ? ? 因為主動斷開連接的一方會進入TIME_WAIT狀態。所以原來綁定的端口號在這段時間內仍然是被占用的。但是1分鐘內不允許重新綁定,有些不太合理。比如說即將到來的618,如果在618期間我們就說tb,tb的服務器在此期間發生了服務器崩潰,重啟要等上一分鐘,但對于人家的服務器來說,分分鐘幾百萬上下啊。所以這種情況下我們就可以在程序內設置TIME_WAIT的時間。
????????使用 setsockopt()設置 socket 描述符的 選項 SO_REUSEADDR 為 1, 表示允許創建端口號相同但 IP 地址不同的多個socket描述符。此處?opt=1
?表示啟用?SO_REUSEADDR。

2.5滑動窗口機制
????????剛才我們討論了確認應答策略, 對每一個發送的數據段, 都要給一個 ACK 確認應答. 收到 ACK 后再發送下一個數據段. 這樣做有一個比較大的缺點, 就是性能較差. 尤其是數據往返的時間較長的時候。既然這樣一發一收的方式性能較低, 那么我們一次發送多條數據, 就可以大大的提高性能(其實是將多個段的等待時間重疊在一起了)。
? ? ? ? 那么每次并行發送的數據量為多少,丟包了怎么辦。應答報文丟了怎么辦。TCP中的滑窗機制正是解決這些問題的存在。
? ? ? ? 先來說窗口大小的問題。還記得上面說的TCP報文里面的16位窗口大小嗎?唉,那我們滑窗的大小根據對方剩余接收緩沖區大小來動態變化,大的時候我窗口大些發快些,小的時候反之。也就是說,滑窗的大小是由對方的接收緩沖區剩余空間大小決定的。(這里我們為了方便理解暫時這樣說,后面說到擁塞機制了再補充)。這其實也正是TCP的流量控制機制。
再來說丟包丟應答的問題:
我們把要發送的數據抽象化為一個一維的數組:
? ? ? ? 我們一般以滑窗的位置對數據進行劃分。start左邊的我們稱為已經發出并且確認收到的數據。start-end之間的我們稱為已經發出但未確認對方收到的數據。end右邊的便是未發出的數據。
? ? ? ? 所以我們先說丟包的情況,可以分為三種:最左端數據包丟失,中間丟失,右端丟失。實際情況一般是上面三種情況的組合。我們對最左端數據包丟失進行討論,如果最左端數據包丟失,那么我們發出數據包的一段會收到多個相同的確認序號應答。比如以上圖為例,會收到多個2001。此時我們客戶端就可以確定100%我們最左端的數據包丟失了,此時我們對該位置的數據包重發即可。(我們也稱這種機制為快重傳)。
? ? ? ? 那中間,最右端以及混合情況呢。比如在中間,那我們收到報文的確認序號就可以判斷中間那個位置100%數據丟失,對其進行重發同時start指針移動到該位置即可。我們發現,最終無論是哪種情況,都會轉化為最左端數據包丟失的情況。
? ? ? ? 同樣的我們說應答丟失的情況,如果說最左端,中間應答丟失了,我們不管,為什么?因為最右端的數據發出后的確認應答就可以幫助我們確認前面沒有數據丟失,如果后面一連串的應答都丟失了?過了超時重傳時間我們客戶端就認為數據包丟失了,于是又回到了上面丟包時候的情況了。
傳數據時應答序號一直增加,會溢出嗎?
? ? ? ? 當然不會,通過環形數組的思想便可以輕松解決這個問題。每次取收數據時start與end對整個緩沖區大小取模不就可以了。
2.6擁塞控制機制
????????雖然 TCP 有了滑動窗口這個大殺器, 能夠高效可靠的發送大量的數據. 但是如果在剛開始階段就發送大量的數據, 仍然可能引發問題.因為網絡上有很多的計算機, 可能當前的網絡狀態就已經比較擁堵. 在不清楚當前網絡狀態下, 貿然發送大量的數據, 是很有可能引起雪上加霜的.
????????TCP 引入慢啟動機制, 先發少量的數據, 探探路, 摸清當前的網絡擁堵狀態, 再決定按照多大的速度傳輸數據;
????????此處引入一個概念稱為擁塞窗口。發送開始的時候, 定義擁塞窗口大小為 1;每次收到一個 ACK 應答, 擁塞窗口加 1;每次發送數據包的時候, 將擁塞窗口和接收端主機反饋的窗口大小做比較, 取較小的值作為實際發送的窗口;也就是說,實際上我們的窗口大小是取擁塞窗口和接收端主機的反饋窗口的較小值。
????????為了不增長的那么快, 因此不能使擁塞窗口單純的加倍.此處引入一個叫做慢啟動的閾值,當擁塞窗口超過這個閾值的時候, 不再按照指數方式增長, 而是按照線性方式增長:
當 TCP 開始啟動的時候, 慢啟動閾值等于窗口最大值;
????????在每次超時重發的時候, 慢啟動閾值會變成原來的一半, 同時擁塞窗口置回 1;少量的丟包, 我們僅僅是觸發超時重傳; 大量的丟包, 我們就認為網絡擁塞;當 TCP 通信開始后, 網絡吞吐量會逐漸上升; 隨著網絡發生擁堵, 吞吐量會立刻下降;擁塞控制, 歸根結底是 TCP 協議想盡可能快的把數據傳輸給對方, 但是又要避免給網絡造成太大壓力的折中方案。但這個中折的幾乎完美,所以我們說指數增長的方法極為巧妙。
? ? ? ? 那么線性增長會有上限嗎,因為網絡穩定時他會一直增長,答案是有的。 線性增長到最后最終由接收方窗口、網絡容量(BDP)或算法自身的收斂機制限制。畢竟網絡穩定的時候再增長本身這件事情也就失去了意義。
2.7延遲應答機制
如果接收數據的主機立刻返回 ACK 應答, 這時候返回的窗口可能比較小.
????????假設接收端緩沖區為 1M. 一次收到了 500K 的數據; 如果立刻應答, 返回的窗口就是 500K;但實際上可能處理端處理的速度很快, 10ms 之內就把 500K 數據從緩沖區消費掉了;在這種情況下, 接收端處理還遠沒有達到自己的極限, 即使窗口再放大一些, 也能處理過來;如果接收端稍微等一會再應答, 比如等待 200ms 再應答, 那么這個時候返回的窗口大小就是 1M;一定要記得, 窗口越大, 網絡吞吐量就越大, 傳輸效率就越高. 我們的目標是在保證網絡不擁塞的情況下盡量提高傳輸效率;
????????那么所有的包都可以延遲應答么? 肯定也不是;數量限制: 每隔 N 個包就應答一次;時間限制: 超過最大延遲時間就應答一次;具體的數量和超時時間, 依操作系統不同也有差異; 一般 N 取 2, 超時時間取 200ms;
?
2.8捎帶應答機制?
????????在延遲應答的基礎上,我們發現, 很多情況下, 客戶端服務器在應用層也是 "一發一收"的. 意味著客戶端給服務器說了 "How are you", 服務器也會給客戶端回一個 "Fine, thank you";那么這個時候 ACK 就可以搭順風車, 和服務器回應的 "Fine, thank you" 一起回給客戶端。
?
????????其他的面向字節流,粘包等問題,我們之前編寫簡答服務器時已經有結合場景介紹過,這里不再過多介紹。?
?2.9TCP異常情況
- 進程終止: 進程終止會釋放文件描述符, 仍然可以發送 FIN. 和正常關閉沒有什么區別.
- 機器重啟: 和進程終止的情況相同.
- 機器掉電/網線斷開: 接收端認為連接還在, 一旦接收端有寫入操作, 接收端發現連接已經不在了, 就會進行 reset. 即使沒有寫入操作, TCP 自己也內置了一個保活定時器, 會定期詢問對方是否還在. 如果對方不在, 也會把連接釋放.
- 另外, 應用層的某些協議, 也有一些這樣的檢測機制. 例如 HTTP 長連接中, 也會定期檢測對方的狀態. 例如 QQ, 在 QQ 斷線之后, 也會定期嘗試重新連接.
2.10小結
為什么 TCP 這么復雜? 因為要保證可靠性, 同時又盡可能的提高性能。
可靠性:
- 校驗和
- 序列號(按序到達)
- 確認應答
- 超時重發
- 連接管理
- 流量控制
- 擁塞控制
提高性能:
- 滑動窗口
- 快速重傳
- 延遲應答
- 捎帶應答
其他:
- 定時器(超時重傳定時器, 保活定時器, TIME_WAIT 定時器等)
????????基于 TCP 應用層的知名協議有HTTP,HTTPS,SSH,Telnet,FTP,SMTP等,有興趣的讀者可以自行下去了解這些協議。
?
?
?
?????????