网络编程(五) ———— 万字详解TCP协议
文章目錄
- TCP首部格式
- 1.確認應答和序列號
- 2.超時重傳
- 3.連接管理
- 為啥要三次握手,為啥要建立連接?
- 如果三次握手,握四次行不行,握兩次行不行?
- 如果三次握手,握兩次行不行?
- 四次揮手中的ACK和FIN為啥不合并?
- 4.滑動窗口
- 如果滑動窗口的場景中出現丟包了,咋辦?
- 快速重傳
- 5.流量控制
- 6.擁塞控制
- 7.延時應答
- 8.捎帶應答
- 9.面向字節流
- 如何解決粘包問題?
- 10.TCP中的一些異常情況(心跳機制)
- 11.如何基于UDP協議實現可靠傳輸?
- 啥樣的場景中適合用TCP,啥樣的場景中適合用UDP
TCP首部格式
- 源端口號
表示發送端端口號,字段長16位 - 目標端口號
表示接收端端口號,字段長 16位 - 序列號
字段長32位。序列號(有時也就序號)是指發送數據的位置。每次發送一次數據,就累加一次該數據字節數的大小
序列號不會從0或1開始,而是建立連接時有計算機生成的隨機數作為其初始值,通過SYN包傳給接收端主機。然后再將每轉發過去的字節數累加到初始值上表示數據的位置。此外,在建立連接和斷開連接時發送的SYN包和FIN包雖然并不攜帶數據,但是也會作為一個字節增加對應的序列號。
- 確認應答號
確認應答號字段長度32位,是指下一次應收到的數據的序列號,實際上,它是指已收到確認應答號減一為止的數據,發送端收到這個確認應答以后可以認為在這個序號以前的數據都已經被正常接收
- 數據偏移
這個字段表示所傳輸的數據部分應該從TCP包的哪個位開始計算,也可以把它看作TCP首部的長度
1.確認應答和序列號
確認應答也是保證可靠性傳輸的核心
發送方發數據給接收方了,接收方就回應一個應答報文,如果發送方收到了這個應答報文,那么認為是對方已經收到了。
由于網絡上的傳輸,順序是不確定的,可能出現后發先至的情況,因此不能就單純的通過收到數據的順序來確定邏輯,就需要對應答進行的編號。
實際上,TCP傳輸數據,不論條,而是論字節(面向字節流)
實際上,TCP的序號和確認序號,是以字節為單位進行編號的
比如下面這張圖,第一個請求A給B發送了1000個字節的數據,序號就是1-1000(假設從1開始編號了),這個操作相當于是發了一個TCP數據報,這個數據報,這個數據報的序號是1,長度是1000,確認應答數據報,里面的確認序號是1001(意思就是1001之前的數據,B已經收到了,另外,也可以理解成,B在向A索要1001開始的數據)
針對每個字節分別編號即為序列號,依次進行累加(TCP的序號的起始不一定是從1開始的),每個ACK都帶有對應的確認序列號,意思是告訴發送者,我已經收到了哪些數據,下一次你從哪里開始發
發送方,就可以根據確認的應答報文來確定接收方是否是收到了,只要發送方收到了應答,就認為接收方已經收到,可靠傳輸就完成了。
反之,在一定時間內沒有等到確認應答,發送端就可以認為數據已經丟失,并進行重發。因此,即使產生了丟包,仍然能夠保證數據能夠到達對端,實現可靠傳輸
2.超時重傳
確認應答機制中,這個是比較順利,但是傳輸過程中還是可能會出現丟包的,一旦數據發生丟包,就要進入超時重傳機制中了。
主機A發送數據給B之后,可能因為網絡擁堵等原因,數據無法到達主機B
如果主機A在一個特定時間間隔內沒有收到B發來的確認應答,就會進行重發
舉個列子:
發送方無法區分,當前是發的數據丟了,還是應答數據丟了,發送方能做的事情,就是在一段時間之后,重發一條數據
超時的時間怎么確定呢?
- 最理想的情況下,找到一個最小的時間,保證 “確認應答一定能在這個時間內返回”
- 但是這個時間的長短,隨著網絡環境的不同,是有差異的 。
- 如果超時時間設的太長,會影響整體的重傳效率
- 如果超時時間設的太短,有可能會頻繁發送重復的包
TCP為了保證無論在任何環境下都能比較高性能的通信,因此會動態計算這個最大超時時間
這個等待,不同的系統實現的方式不一樣,數據在網絡上傳輸過程,是需要一定的時間的,不能說數據剛發出去,就期望得到回應,可能要經歷一段時間之后,才能得到回應
Linux中(BSD Unix和Windows也是如此),超時以500ms為一個單位進行控制,每次判定超時重發的超時時間都是500ms的整數倍 ,發送方,把數據發出去之后,等待500ms,如果沒有收到應答就認為是丟包了。
如果重發一次之后,仍然得不到應答,等待 2500ms 后再進行重傳。
如果仍然得不到應答,等待 4500ms 進行重傳。依次類推,以指數形式遞增
累計到一定的重傳次數,TCP認為網絡或者對端主機出現異常,強制關閉連接
超時時間會動態變化,不是一成不變的。
等待時間會逐漸延長,延長也就意味著讓重試的頻率盡量降低,只要重傳也失敗,其實就認為,大概率這個傳輸是通不了的。達到一定重發次數以后,如果仍沒有任何回應,就會 判斷為網絡或對端主機發生了異常,強制關閉連接,并且通知應用通信異常強行終止。
在超時重傳的時候,無法區分是發送過去的數據丟了,還是返回的ACK丟了,就一視同仁了,都要進行重傳 ,以500ms為單位,依次增加、
如果數據重復了怎么辦?
接收方收到的數據會先放在內核的”接收緩沖區中“,
接收緩沖區是一段內存,每個socket都有,按照序號來進行去重,此時在應用程序中讀取的數據,讀到的結果就是不帶重復的
3.連接管理
UDP是一種面向無連接的通信協議,因此不檢查對端是否可以通信,直接將UDP包發出去,TCP與此相反
TCP是有連接的,連接管理就是,如何建立連接(三次握手),如何斷開連接(四次揮手)
-
三次握手本質上就是:A向B請求連接,B給與回應,B也向A請求連接,A也給與回應
-
本來應該是“四次握手”,但是中間兩次操作,是可以合在一起的,這兩個操作在時間上是同時發生的
-
當A的SYN到達B的時候,B的內核就會第一時間進行應答ACK,同時也會第一時間發起SYN,這兩件事同時觸發,于是就沒有必要分成兩次傳輸,直接一步到位
為啥要三次握手,為啥要建立連接?
主要有兩個目的
如果網絡出現了問題,此時,三次握手都會難以成功,此時也就沒有必要進行后續的傳輸了
如果三次握手,握四次行不行,握兩次行不行?
握四次,完全可以。沒有必要,效果和3次是一樣的。
中間的ACK和SYN是可以合并在一起的,如果分成兩個,傳輸的開銷就比一個大。
如果三次握手,握兩次行不行?
握兩次是肯定不行的,A 給 B發送一個 SYN,B再給A同時發送一個ACK和SYN就完成了兩次握手。
此時A知道自己發送和接收能力沒有問題,也知道B的發送和接收能力沒問題
但是B只知道自己的接收能力沒問題,但不知道自己的發送能力有沒有問題
B只知道A的發送能力沒有問題,并不知道A的接收能力有沒有問題。所以并不能進行傳輸。
服務器和客戶端
四次揮手中的ACK和FIN為啥不合并?
對于B來說 ACK 和 FIN 的觸發時機是不一樣的。
- CLOSE_WAIT:服務器收到FIN之后,進入的狀態,等待用戶代碼調用close,來發送FIN
- TIME_WAIT:表示的客戶端收到了FIN之后進入了TIME_WAIT,這個狀態存在的意義主要就是為了處理最后一個ACK包
在三次握手和四次揮手的過程中,同樣可能會丟包,一旦丟包就會觸發超時重傳
假設,如果A收到FIN,并返回ACK之后,連接就銷毀,而不是進入TIME_WAIT狀態,會咋樣?
此時一旦最后一個ACK丟了,此時就無法重傳ACK了(連接已經銷毀了)
TINE_WAIT即使進程已經退出了,TIME_WAIT狀態仍然會存在(TCP連接不會立即銷毀),TIME_WAIT 會等待一定時間,如果一定時間之內也沒有重傳的FIN過來,才會真正銷毀
這個等待時間為2*MSI
MSI理論上是,主機A和B之間最長的一次通信時間
通常在Linux中這個MSL默認是1min,這個MSL默認是1min,當然這個MSL都是可以配置的
如果服務器上出現大量的CLOSE_WAIT,是啥情況?
這是代碼出現bug,close,沒有及時被調用到,
如果服務器上出現大量的TIME_WAIT,是啥情況?
主動發起FIN的一方會進入TIME_WAIT,就需要排查服務器是否應該主動斷開連接
這個也可能是代碼bug,但是不能石錘
哪方先斷開連接,哪方就會進入TIME_WAIT,進程退出之后,TIME_WAIT狀態仍然存在,TCP連接仍然存在
如果讓服務器先退出,服務器這邊就會進入到 TIME_WAIT狀態(原來的連接占據著端口),接下來如果服務器立即啟動,新的進程又會嘗試重新綁定這個端口可能
會存在端口綁定失敗的情況,如果使用原生的socket來試效果會非常明顯,第二次啟動就會啟動失敗。
在Java socket,一般來說第二次啟動也是能成功的
在socket api 里有一個 REUSE ADDR 選項,如果把這個選項加上,就能夠讓我們綁定端口的時候復用TIME_WAIT狀態中的端口,(Java socket里面應該是默認就設置了這個選項)
- LISTEN:手機開機 ,信號良好,隨時可以打入電話,服務器的狀態,當我們創建好ServerSocket實例的時候,就進入了LISTEN狀態
- ESTABLISHED:接通了電話,雙方可說話了,代碼中accept放回了,得到了一個clientSocke
- stable:穩定的
四次揮手一定是四次嘛?是否可能是三次呢?
有可能的,后面的:延時應答和捎帶應答雖然ACK和FIN是不同時機,但是在延時應答和捎帶應答的情況下是可能合并在一起的
四次揮手一定會執行嘛?也不一定
四次揮手是一個TCP正常斷開的流程,但是實際上,有的時候TCP連接也會異常斷開(比如網斷了)
4.滑動窗口
TCP不僅僅是為了保證可靠性,還要盡可能的提高傳輸效率。
其實可靠性和效率,是矛盾的。TCP努力的在可靠性的前提下,又做出了很多性能優化的手段
下圖的這個發送過程,發送方需要花很多時間來等,這個等待其實就浪費了大量時間
現在通過批量發送,一次發送一波,一次等一波的ACK,把多組數據的ACK的等待時間給重疊起來了。
一次批量發的數據的長度,就稱為“窗口大小”
如果沒有批量發送數據的長度限制(窗口無限大,完全不等,ACK就一頓發),其實就沒有可靠性而言
如果窗口越大,其實整體的效率就越高
如果窗口越小,整體的效率就越低
當前窗口范圍是1001 - 5000,也就意味著,發送方現在同時發送了(1001 - 2000;2001-3000;3001-4000;4001-5000),同時再等待著四組數據的ACK
假設,2001這個ACK先到,發送方就知道了1001-2000這個數據已經被對方收到了
發送方也就不用繼續等這個數據了,接下來就立即再發一個5001 - 6000,仍然保證窗口大小時4份數據,仍然保證當前同時等待4份數據的ack
并不是把4份ack都等到,才發新的數據,而是隨著收到ack,就隨著往后發送。
假設有后發先至的情況
ack 2001,3001,4001,5001 都在網絡上傳輸呢,不一定非得是2001先到,是否可能3001先到呢?
這是非常有可能的。
確認序號表示,從該序號之前,前面的數據都收到了
如果收到了3001這個ack,意思就是 1001-2000 和 2001-3000都被對方收到了,此時2001這個ack收或者不收,已經不關鍵。
如果滑動窗口的場景中出現丟包了,咋辦?
情況1:數據包已經抵達,ACK丟失了
這種情況沒有關系,只要不是全部ACK丟失就好了,哪怕丟個50%也沒事
發送方:1001-2000,2001-3000
接收方:2001,3001
如果2001這個acck丟了,3001這個ack到了
此時發送方也就知道了,3001前面的數據都被正確收到了
此時1001-2000這個數據報也得到了一個確認應答
實際上,TCP為了偷懶(提高效率),滑動窗口下,并不是每一條數據都有ACK,會隔幾條數據才有一個ACK
快速重傳
快速重傳,效率很高,尤其是不需要重復傳輸數據
如上圖,因為1001丟包了,主機B就會一直索要1001,確認序號就仍然是1001
1001-2000這個數據丟了,2001-3000,3001-4000…還在繼續發
發送方這邊,如果連續看到幾次1001這個ACK,就知道了是1001這個數據丟失了,接下來就會重傳1001
前面的2001-7000這些數據已經到達了接收端了,值不過是在接收緩沖區里待著,當1001-2000這個數據到達的時候,B就知道了,7001之前的數據就都到齊了,此時就繼續索要7001這個數據即可
5.流量控制
流量控制,本質上就是在控制滑動窗口的大小,也是保證可靠性的。
窗口大小決定了傳輸的效率,窗口越大,效率就越高,窗口越小,效率就越低。
既然如此,窗口大小取多少合適呢?
窗口越大,為了保證可靠性,資源開銷就得越多
窗口太小,速度也得不到保證
流量控制是基于接收方的處理能力來限制窗口大小的
TCP這個傳輸數據的過程,其實也就類似于一個生產者消費者模型
主機A發送的數據就到達了主機B的接收緩沖區,此時主機A就是生產者,主機B的應用程序,通過socket api 來讀取數據。被soket api 讀到的數據就從緩沖區中刪掉了,應用程序就是消費者,接收緩沖區就是交易場所(類似于一個隊列)
所說的窗口大小,是指發送發(主機A)批量發多少數據,比如,主機A發的數據很快,窗口很大,此時接收緩沖區的數據也會增長很快,如果主機B的應用程序讀取數據讀的不快,隨著時間的推移,接收緩沖區逐漸就滿了,如果不加任何限制,主機A還是按照一樣的速度發,此時新來的數據沒有地方保存,就被內核丟了。
類似于一個水池,一邊注水,一邊出水,如果注水速度比出水大,池子很快就滿了
流量控制這個機制,就是為了解決這個問題的,根據接收方的處理能力(接收緩沖區的剩余空間大小),來動態決定發送方的發送速率(控制窗大小)
接收緩沖區大小是4000
1-1000數據到達的時候,緩沖區這里面用了1000,還剩3000,返回的ack中就會把3000這個信息告訴發送方
發送方再次發送數據的時候,就按照3000作為窗口大小來進行發送
窗口大小(接收緩沖區的剩余空間)是如何放回給發送方的?
如果窗口大小為0 了(接收端這邊滿了),然后發送方就停了嗎?
此時發送方式不再繼續發數據了,但是為了能夠查詢當前接收方的窗口大小。每隔一段時間,還會再來觸發一個窗口探測包,通過這個包(不傳輸具體的業務數據),觸發ACK,在這個ACK中就能知道當前窗口的大小了
那么問題來了,16位數字最大表示65535,那么TCP窗口最大就是65535字節么?
64K窗口大小夠嗎?
TCP首部40字節選項中還包含了一個窗口擴大因子,實際窗口大小是 窗口字段的值 左移M位(左移以為相當于*2)
6.擁塞控制
擁塞控制是站在另外一個角度來限制發送方的窗口大小,站在一個宏觀角度來看待這個問題,把整個中間鏈路都看成了一個整體,只看結果。
先使用一個比較小的窗口來傳輸數據,看看是否丟包,
如果不丟包,說明網絡比較通暢,如果丟包,說明網絡發送擁堵
當網絡通暢的時候,就逐漸加大發送速率。當網絡出現丟包的時候,就立即降低發送速率。
通過這樣的方式,就可以逐漸實驗出一個比較合適的窗口大小
真實的發送窗口大小 = min(流量控制的窗口,擁塞控制的窗口)
慢啟動:剛開始啟動的時候,給一個較小的窗口(比較慢的發送速率)
這個圖描述了擁塞控制中,窗口大小的變化規則
指數增長速度是非常快的,由于剛開始啟動的時候,窗口比較小,此時通過指數增長,就能在很少的輪次中就能把窗口大小給頂上去
如果達到閾值就從指數增長變成線性增長
7.延時應答
延時應答也是用來提高效率的(琢磨窗口大小)
讓窗口大小,在保證可靠的基礎之上,能盡量再大一點
流量控制來說,窗口大小就是接收緩沖區的剩余空間大小
主機A給主機B發送數據,如果接收方立刻放回ACK,此時放回的窗口大小,就是當下這個緩沖區剩余的空間。
但是如果接收方稍等一會,再返回ACK,稍等這個時間里,應用程序可能就會消費一部分數據,此時緩沖區的剩余空間就更大了
8.捎帶應答
在延時應答的基礎之上
很多的客戶端/服務器的通信服務器的通信模式,都是這種“一問一答”
但是由于有了延時應答,返回的ACK不是立即返回,而是等一會。
正好等一會之后,服務器要返回業務上的response了,此時就可以把這個ACK和response合二為一,把兩個包變成一個包
網絡通信涉及到大量的封裝和分用,針對每個包都要一頓封裝,收到之后再一頓解析。
針對四次揮手來說,確實是可能四次變成三次的
捎帶應答,是可能吧中間的ACK和FIN給合并成一個,于是四次揮手就變成了三次揮手。
四次揮手啥時候能變成三次?這是不能確定的。
捎帶應答本身就是一個“概率性的機制”,當前ACK延時的時間正好要比接下來發業務數據的時間要更長一些。
列如,服務器收到請求到返回響應,這個過程消耗時間50ms
但是延時應答假設最多等20ms,這個情況就無法觸發捎帶應答了
但是延時應答假設是最多等60ms
第50ms的時候,此時觸發了響應,ACK就可以和這個響應一起過去了,也就是觸發了延時應答
9.面向字節流
在這種面向字節流的情況下,需要注意一個重要的問題:粘包問題(指的是應用層的數據報)
應用程序從接收緩沖區讀數據的時候,就不知道從哪里到哪里是一個完整的應用層數據報
應用程序此時只能看到接收緩沖區中的一個一個字節,無法區分當前接收緩沖區里有多少個應用層數據報,以及從哪到哪是一個完整的應用層數據報
如何解決粘包問題?
通過設計一個合理的應用層協議來解決
方式一:設定結束符,約定每個應用層數據報一定以 ; 結尾
方式二:設定包的長度,約定每個應用層數據報的前4個字節,存儲數據報的長度
在UDP中是不存在粘包問題的
10.TCP中的一些異常情況(心跳機制)
常見的異常情況
進程終止
不管進程是咋終止的,本質上都會釋放對應的PCB,也會釋放對應的文件描述符,一樣會觸發 四次揮手
"進程終止"不代表連接就終止,進程終止其實就相當于調用了 soket.close() 方法而已
機器重啟
機器重啟的時候,其實也是先殺進程,仍然是進行四次揮手
機器掉電、網線斷開
突發情況,機器來不及進行任何動作的
如果掉電的是接收方,
此時另外一邊還在發送數據,此時顯然發送方不會再有ACK,于是就會超時重傳.
重傳幾次之后,就會嘗試重置連接,這個時候,RST(復位報文段)就會設置為1
再然后發送方就會放棄這個連接,把連接對應的資源就回收了。
如果掉電的是發送方(心跳機制)
此時另外一方在嘗試接收數據,此時接收不到任何數據
接收方如何知道,發送方式掛了?還是說發送方暫時還沒發呢?
此時接收方采取的策略,就是"心跳包"機制(也叫做“保活”)
每隔一段時間,向對方發送一個 PING包,期待對方返回一個PONG包。
如果PING包發故去,過了很久還沒有PONG,并且重試幾次也不行,此時就認為對方已經掛了
心跳包是一個應用非常廣泛的機制,不僅僅是在TCP
在微服務中,如果某個主機宕機了,此時入口服務器就得即使發現這個事情,就需要把請求切走
就可以使用心跳包機制,直接使用一個TCP連接時不行的,雖然使用TCP連接能夠感知是哪個主機掛了,但是TCP感知的不夠及時,如果希望能夠更加及時更快速的發現問題
就需要在應用層實現心跳機制
11.如何基于UDP協議實現可靠傳輸?
這個問題其實是在考TCP
啥樣的場景中適合用TCP,啥樣的場景中適合用UDP
如果需要可靠傳輸,肯定首選TCP
如果傳輸單個數據報比較長(超過64K),還是首選TCP
如果特別注重效率,優先考慮UDP
典型的場景:機房內部的主機通信
網絡環境簡單,帶寬充裕,丟包的概率不大
機房內部主機之間的通信,往往傳輸數據量更大,更需要速度
尤其是在當下的"微服務"這樣的環境中,其實特別需要
如果需要廣播,優先考慮UDP
一份數據同時發給多個主機
UDP自身就支持廣播的
但是TPC自身不支持廣播,就只能在應用程序中,通過多個連接,輪詢的方式給每個主機發送數據(偽廣播)
除了TPC和UDP之外還有很多其它協議,有的協議就可以盡可能的兼顧到可靠性和效率(兼顧可靠性和效率,付出的代價可能激素會更多的機器資源)
總結
以上是生活随笔為你收集整理的网络编程(五) ———— 万字详解TCP协议的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 通过调用rundll32.exe来打开一
- 下一篇: 10106