文章目錄
- TCP協議
-
- 談談可靠性
- TCP協議格式
-
- 序號與確認序號
- 窗口大小
- 六個標志位
- 確認應答機制(ACK)
- 超時重傳機制
- 連接管理機制
-
- 三次握手
- 四次揮手
- 流量控制
- 滑動窗口
- 擁塞控制
- 延遲應答
- 捎帶應答
- 面向字節流
- 粘包問題
- TCP異常情況
- TCP小結
- 基于TCP的應用層協議
TCP協議
TCP全稱為“傳輸控制協議(Transmission Control Protocol)”,TCP協議是當今互聯網當中使用最為廣泛的傳輸層協議,沒有之一。
TCP協議被廣泛應用,其根本原因就是提供了詳盡的可靠性保證,基于TCP的上層應用非常多,比如HTTP、HTTPS、FTP、SSH等,甚至MySQL底層使用的也是TCP。
談談可靠性
為什么網絡中會存在不可靠?
現代的計算機大部分都是基于馮諾依曼體系結構的。
雖然這里的輸入設備、輸出設備、內存、CPU都在一臺機器上,但這幾個硬件設備是彼此獨立的。如果它們之間要進行數據交互,就必須要想辦法進行通信,因此這幾個設備實際是用“線”連接起來的,其中連接內存和外設之間的“線”叫做IO總線,而連接內存和CPU之間的“線”叫做系統總線。由于這幾個硬件設備都是在一臺機器上的,因此這里傳輸數據的“線”是很短的,傳輸數據時出現錯誤的概率也非常低。
但如果要進行通信的各個設備相隔千里,那么連接各個設備的“線”就會變得非常長,傳輸數據時出現錯誤的概率也會大大增高,此時要保證傳輸到對端的數據無誤,就必須引入可靠性。
總之,網絡中存在不可靠的根本原因就是,長距離數據傳輸所用的“線”太長了,數據在長距離傳輸過程中就可能會出現各種各樣的問題,而TCP就是在此背景下誕生的,TCP就是一種保證可靠性的協議。
思維擴展:
- 實際單獨的一臺計算機可以看作成一個小型的網絡,計算機上的各種硬件設備之間實際也是在進行數據通信,并且它們在通信時也必須遵守各自的通信協議,只不過它們之間的通信協議更多是描述一些數據的含義。
為什么會存在UDP協議?
TCP協議是一種可靠的傳輸協議,使用TCP協議能夠在一定程度上保證數據傳輸時的可靠性,而UDP協議是一種不可靠的傳輸協議,那UDP協議這種不可靠的協議存在有什么意義呢?
不可靠和可靠是兩個中性詞,它們描述的都是協議的特點。
- TCP協議是可靠的協議,也就意味著TCP協議需要做更多的工作來保證傳輸數據的可靠,并且引起不可靠的因素越多,保證可靠的成本(時間+空間)就越高。
- 比如數據在傳輸過程中出現了丟包、亂序、檢驗和失敗等,這些都是不可靠的情況。
- 由于TCP要想辦法解決數據傳輸不可靠的問題,因此TCP使用起來一定比UDP復雜,并且維護成本特別高。
- UDP協議是不可靠的協議,也就意味著UDP協議不需要考慮數據傳輸時可能出現的問題,因此UDP無論是使用還是維護都足夠簡單。
- 需要注意的是,雖然TCP復雜,但TCP的效率不一定比UDP低,TCP當中不僅有保證可靠性的機制,還有保證傳輸效率的各種機制。
UDP和TCP沒有誰最好,只有誰最合適,網絡通信時具體采用TCP還是UDP完全取決于上層的應用場景。如果應用場景嚴格要求數據在傳輸過程中的可靠性,那么就必須采用TCP協議,如果應用場景允許數據傳輸出現少量丟包,那么肯定優先選擇UDP協議,因為UDP協議足夠簡單。
TCP協議格式
TCP協議格式如下:
TCP報頭當中各個字段的含義如下:
- 源/目的端口號:表示數據是從哪個進程來,到發送到對端主機上的哪個進程。
- 32位序號/32位確認序號:分別代表TCP報文當中每個字節數據的編號以及對對方的確認,是TCP保證可靠性的重要字段。
- 4位TCP報頭長度:表示該TCP報頭的長度,以4字節為單位。
- 6位保留字段:TCP報頭中暫時未使用的6個比特位。
- 16位窗口大小:保證TCP可靠性機制和效率提升機制的重要字段。
- 16位檢驗和:由發送端填充,采用CRC校驗。接收端校驗不通過,則認為接收到的數據有問題。(檢驗和包含TCP首部+TCP數據部分)
- 16位緊急指針:標識緊急數據在報文中的偏移量,需要配合標志字段當中的URG字段統一使用。
- 選項字段:TCP報頭當中允許攜帶額外的選項字段,最多40字節。
TCP報頭當中的6位標志位:
- URG:緊急指針是否有效。
- ACK:確認序號是否有效。
- PSH:提示接收端應用程序立刻將TCP接收緩沖區當中的數據讀走。
- RST:表示要求對方重新建立連接。我們把攜帶RST標識的報文稱為復位報文段。
- SYN:表示請求與對方建立連接。我們把攜帶SYN標識的報文稱為同步報文段。
- FIN:通知對方,本端要關閉了。我們把攜帶FIN標識的報文稱為結束報文段。
TCP報頭在內核當中本質就是一個位段類型,給數據封裝TCP報頭時,實際上就是用該位段類型定義一個變量,然后填充TCP報頭當中的各個屬性字段,最后將這個TCP報頭拷貝到數據的首部,至此便完成了TCP報頭的封裝。
TCP如何將報頭與有效載荷進行分離?
當TCP從底層獲取到一個報文后,雖然TCP不知道報頭的具體長度,但報文的前20個字節是TCP的基本報頭,并且這20字節當中涵蓋了4位的首部長度。
因此TCP是這樣分離報頭與有效載荷的:
- 當TCP獲取到一個報文后,首先讀取報文的前20個字節,并從中提取出4位的首部長度,此時便獲得了TCP報頭的大小 s i z e size size。
- 如果 s i z e size size的值大于20字節,則需要繼續從報文當中讀取 s i z e ? 20 size-20 size?20字節的數據,這部分數據就是TCP報頭當中的選項字段。
- 讀取完TCP的基本報頭和選項字段后,剩下的就是有效載荷了。
需要注意的是,TCP報頭當中的4位首部長度描述的基本單位是4字節,這也恰好是報文的寬度。4為首部長度的取值范圍是0000 ~ 1111,因此TCP報頭最大長度為 15 × 4 = 60 15\times4=60 15×4=60字節,因為基本報頭的長度是20字節,所以報頭中選項字段的長度最多是40字節。
如果TCP報頭當中不攜帶選項字段,那么TCP報頭的長度就是20字節,此時報頭當中的4位首部長度的值就為 20 ÷ 4 = 5 20\div4=5 20÷4=5,也就是0101。
TCP如何決定將有效載荷交付給上層的哪一個協議?
應用層的每一個網絡進程都必須綁定一個端口號。
- 服務端進程必須顯示綁定一個端口號。
- 客戶端進程由系統動態綁定一個端口號。
而TCP的報頭中涵蓋了目的端口號,因此TCP可以提取出報頭中的目的端口號,找到對應的應用層進程,進而將有效載荷交給對應的應用層進程進行處理。
說明一下: 內核中用哈希的方式維護了端口號與進程ID之間的映射關系,因此傳輸層可以通過端口號快速找到其對應的進程ID,進而找到對應的應用層進程。
序號與確認序號
什么是真正的可靠?
在進行網絡通信時,一方發出的數據后,它不能保證該數據能夠成功被對端收到,因為數據在傳輸過程中可能會出現各種各樣的錯誤,只有當收到對端主機發來的響應消息后,該主機才能保證上一次發送的數據被對端可靠的收到了,這就叫做真正的可靠。
圖注:實線表示該數據能夠被對方可靠的收到,虛線則不能保證。
但TCP要保證的是雙方通信的可靠性,雖然此時主機A能夠保證自己上一次發送的數據被主機B可靠的收到了,但主機B也需要保證自己發送給主機A的響應數據被主機A可靠的收到了。因此主機A在收到了主機B的響應消息后,還需要對該響應數據進行響應,但此時又需要保證主機A發送的響應數據的可靠性…,這樣就陷入了一個死循環。
因為只有當一端收到對方的響應消息后,才能保證自己上一次發送的數據被對端可靠的收到了,但雙方通信時總會有最新的一條消息,因此無法百分之百保證可靠性。
所以嚴格意義上來說,互聯網通信當中是不存在百分之百的可靠性的,因為雙方通信時總有最新的一條消息得不到響應。但實際沒有必要保證所有消息的可靠性,我們只要保證雙方通信時發送的每一個核心數據都有對應的響應就可以了。而對于一些無關緊要的數據(比如響應數據),我們沒有必要保證它的可靠性。因為對端如果沒有收到這個響應數據,會判定上一次發送的報文丟失了,此時對端可以將上一次發送的數據進行重傳。
這種策略在TCP當中就叫做確認應答機制。需要注意的是,確認應答機制不是保證雙方通信的全部消息的可靠性,而是只要一方收到了另一方的應答消息,就說明它上一次發送的數據被另一方可靠的收到了。
32位序號
如果雙方在進行數據通信時,只有收到了上一次發送數據的響應才能發下一個數據,那么此時雙方的通信過程就是串行的,效率可想而知。
因此雙方在進行網絡通信時,允許一方向另一方連續發送多個報文數據,只要保證發送的每個報文都有對應的響應消息就行了,此時也就能保證這些報文被對方收到了。
但在連續發送多個報文時,由于各個報文在進行網絡傳輸時選擇的路徑可能是不一樣的,因此這些報文到達對端主機的先后順序也就可能和發送報文的順序是不同的。但報文有序也是可靠性的一種,因此TCP報頭中的32位序號的作用之一實際就是用來保證報文的有序性的。
TCP將發送出去的每個字節數據都進行了編號,這個編號叫做序列號。
- 比如現在發送端要發送3000字節的數據,如果發送端每次發送1000字節,那么就需要用三個TCP報文來發送這3000字節的數據。
- 此時這三個TCP報文當中的32位序號填的就是發送數據中首個字節的序列號,因此分別填的是1、1001和2001。
此時接收端收到了這三個TCP報文后,就可以根據TCP報頭當中的32位序列號對這三個報文進行順序重排(該動作在傳輸層進行),重排后將其放到TCP的接收緩沖區當中,此時接收端這里報文的順序就和發送端發送報文的順序是一樣的了。
- 接收端在進行報文重排時,可以根據當前報文的32位序號與其有效載荷的字節數,進而確定下一個報文對應的序號。
32位確認序號
TCP報頭當中的32位確認序號是告訴對端,我當前已經收到了哪些數據,你的數據下一次應該從哪里開始發。
以剛才的例子為例,當主機B收到主機A發送過來的32位序號為1的報文時,由于該報文當中包含1000字節的數據,因此主機B已經收到序列號為1-1000的字節數據,于是主機B發給主機A的響應數據的報頭當中的32位確認序號的值就會填成1001。
- 一方面是告訴主機A,序列號在1001之前的字節數據我已經收到了。
- 另一方面是告訴主機A,下次向我發送數據時應該從序列號為1001的字節數據開始進行發送。
之后主機B對主機A發來的其他報文進行響應時,發給主機A的響應當中的32為確認序號的填法也是類似的道理。
注意:
- 響應數據與其他數據一樣,也是一個完整的TCP報文,盡管該報文可能不攜帶有效載荷,但至少是一個TCP報頭。
報文丟失怎么辦?
還是以剛才的例子為例,主機A發送了三個報文給主機B,其中每個報文的有效載荷都是1000字節,這三個報文的32位序號分別是1、1001、2001。
如果這三個報文在網絡傳輸過程中出現了丟包,最終只有序號為1和2001的報文被主機B收到了,那么當主機B在對報文進行順序重排的時候,就會發現只收到了1-1000和2001-3000的字節數據。此時主機B在對主機A進行響應時,其響應報頭當中的32位確認序號填的就是1001,告訴主機A下次向我發送數據時應該從序列號為1001的字節數據開始進行發送。
注意:
- 此時主機B在給主機A響應時,其32位確認序號不能填3001,因為1001-2000是在3001之前的,如果直接給主機A響應3001,就說明序列號在3001之前的字節數據全都收到了。
- 因此主機B只能給主機A響應1001,當主機A收到該確認序號后就會判定序號為1001的報文丟包了,此時主機A就可以選擇進行數據重傳。
因此發送端可以根據對端發來的確認序號,來判斷是否某個報文可能在傳輸過程中丟失了。
為什么要用兩套序號機制?
如果通信雙方只是一端發送數據,另一端接收數據,那么只用一套序號就可以了。
- 發送端在發送數據時,將該序號看作是32位序號。
- 接收端在對發送端發來的數據進行響應時,將該序號看作是32位確認序號。
但實際TCP卻沒有這么做,根本原因就是因為TCP是全雙工的,雙方可能同時想給對方發送消息。
- 雙方發出的報文當中,不僅需要填充32位序號來表明自己當前發送數據的序號。
- 還需要填充32位確認序號,對對方上一次發送的數據進行確認,告訴對方下一次應該從哪一字節序號開始進行發送。
因此在進行TCP通信時,雙方都需要有確認應答機制,此時一套序號就無法滿足需求了,因此需要TCP報頭當中出現了兩套序號。
總結一下:
- 32位序號的作用是,保證數據的按序到達,同時這個序號也是作為對端發送報文時填充32位確認序號的根據。
- 32位確認序號的作用是,告訴對端當前已經收到的字節數據有哪些,對端下一次發送數據時應該從哪一字節序號開始進行發送。
- 序號和確認序號是確認應答機制的數據化表示,確認應答機制就是由序號和確認序號來保證的。
- 此外,通過序號和確認序號還可以判斷某個報文是否丟失。
窗口大小
TCP的接收緩沖區和發送緩沖區
TCP本身是具有接收緩沖區和發送緩沖區的:
- 接收緩沖區用來暫時保存接收到的數據。
- 發送緩沖區用來暫時保存還未發送的數據。
- 這兩個緩沖區都是在TCP傳輸層內部實現的。
- TCP發送緩沖區當中的數據由上層應用應用層進行寫入。當上層調用write/send這樣的系統調用接口時,實際不是將數據直接發送到了網絡當中,而是將數據從應用層拷貝到了TCP的發送緩沖區當中。
- TCP接收緩沖區當中的數據最終也是由應用層來讀取的。當上層調用read/recv這樣的系統調用接口時,實際也不是直接從網絡當中讀取數據,而是將數據從TCP的接收緩沖區拷貝到了應用層而已。
- 就好比調用read和write進行文件讀寫時,并不是直接從磁盤讀取數據,也不是直接將數據寫入到磁盤上,而對文件緩沖區進行的讀寫操作。
當數據寫入到TCP的發送緩沖區后,對應的write/send函數就可以返回了,至于發送緩沖區當中的數據具體什么時候發,怎么發等問題實際都是由TCP決定的。
我們之所以稱TCP為傳輸層控制協議,就是因為最終數據的發送和接收方式,以及傳輸數據時遇到的各種問題應該如何解決,都是由TCP自己決定的,用戶只需要將數據拷貝到TCP的發送緩沖區,以及從TCP的接收緩沖區當中讀取數據即可。
需要注意的是,通信雙方的TCP層都是一樣的,因此通信雙方的TCP層都是既有發送緩沖區又有接收緩沖區。
TCP的發送緩沖區和接收緩沖區存在的意義
發送緩沖區和接收緩沖區的作用:
- 數據在網絡中傳輸時可能會出現某些錯誤,此時就可能要求發送端進行數據重傳,因此TCP必須提供一個發送緩沖區來暫時保存發送出去的數據,以免需要進行數據重傳。只有當發出去的數據被對端可靠的收到后,發送緩沖區中的這部分數據才可以被覆蓋掉。
- 接收端處理數據的速度是有限的,為了保證沒來得及處理的數據不會被迫丟棄,因此TCP必須提供一個接收緩沖區來暫時保存未被處理的數據,因為數據傳輸是需要耗費資源的,我們不能隨意丟棄正確的報文。此外,TCP的數據重排也是在接收緩沖區當中進行的。
經典的生產者消費者模型:
- 對于發送緩沖區來說,上層應用不斷往發送緩沖區當中放入數據,下層網絡層不斷從發送緩沖區當中拿出數據準備進一步封裝。此時上層應用扮演的就是生產者的角色,下層網絡層扮演的就是消費者的角色,而發送緩沖區對應的就是“交易場所”。
- 對于接收緩沖區來說,上層應用不斷從接收緩沖區當中拿出數據進行處理,下層網絡層不斷往接收緩沖區當中放入數據。此時上層應用扮演的就是消費者的角色,下層網絡層扮演的就是生產者的角色,而接收緩沖區對應的就是“交易場所”。
- 因此引入發送緩沖區和接收緩沖區相當于引入了兩個生產者消費者模型,該生產者消費者模型將上層應用與底層通信細節進行了解耦,此外,生產者消費者模型的引入同時也支持了并發和忙閑不均。
窗口大小
當發送端要將數據發送給對端時,本質是把自己發送緩沖區當中的數據發送到對端的接收緩沖區當中。但緩沖區是有大小的,如果接收端處理數據的速度小于發送端發送數據的速度,那么總有一個時刻接收端的接收緩沖區會被打滿,這時發送端再發送數據過來就會造成數據丟包,進而引起丟包重傳等一系列的連鎖反應。
因此TCP報頭當中就有了16位的窗口大小,這個16位窗口大小當中填的是自身接收緩沖區中剩余空間的大小,也就是當前主機接收數據的能力。
接收端在對發送端發來的數據進行響應時,就可以通過16位窗口大小告知發送端自己當前接收緩沖區剩余空間的大小,此時發送端就可以根據這個窗口大小字段來調整自己發送數據的速度。
- 窗口大小字段越大,說明接收端接收數據的能力越強,此時發送端可以提高發送數據的速度。
- 窗口大小字段越小,說明接收端接收數據的能力越弱,此時發送端可以減小發送數據的速度。
- 如果窗口大小的值為0,說明接收端接收緩沖區已經被打滿了,此時發送端就不應該再發送數據了。
理解現象:
- 在編寫TCP套接字時,我們調用read/recv函數從套接字當中讀取數據時,可能會因為套接字當中沒有數據而被阻塞住,本質是因為TCP的接收緩沖區當中沒有數據了,我們實際是阻塞在接收緩沖區當中了。
- 而我們調用write/send函數往套接字中寫入數據時,可能會因為套接字已經寫滿而被阻塞住,本質是因為TCP的發送緩沖區已經被寫滿了,我們實際是阻塞在發送緩沖區當中了。
- 在生產者消費者模型當中,如果生產者生產數據時被阻塞,或消費者消費數據時被阻塞,那么一定是因為某些條件不就緒而被阻塞。
六個標志位
為什么會存在標志位?
- TCP報文的種類多種多樣,除了正常通信時發送的普通報文,還有建立連接時發送的請求建立連接的報文,以及斷開連接時發送的斷開連接的報文等等。
- 收到不同種類的報文時完美需要對應執行動作,比如正常通信的報文需要放到接收緩沖區當中等待上層應用進行讀取,而建立和斷開連接的報文本質不是交給用戶處理的,而是需要讓操作系統在TCP層執行對應的握手和揮手動作。
- 也就是說不同種類的報文對應的是不同的處理邏輯,所以我們要能夠區分報文的種類。而TCP就是使用報頭當中的六個標志字段來進行區分的,這六個標志位都只占用一個比特位,為0表示假,為1表示真。
SYN
- 報文當中的SYN被設置為1,表明該報文是一個連接建立的請求報文。
- 只有在連接建立階段,SYN才被設置,正常通信時SYN不會被設置。
ACK
- 報文當中的ACK被設置為1,表明該報文可以對收到的報文進行確認。
- 一般除了第一個請求報文沒有設置ACK以外,其余報文基本都會設置ACK,因為發送出去的數據本身就對對方發送過來的數據具有一定的確認能力,因此雙方在進行數據通信時,可以順便對對方上一次發送的數據進行響應。
FIN
- 報文當中的FIN被設置為1,表明該報文是一個連接斷開的請求報文。
- 只有在斷開連接階段,FIN才被設置,正常通信時FIN不會被設置。
URG
雙方在進行網絡通信的時候,由于TCP是保證數據按序到達的,即便發送端將要發送的數據分成了若干個TCP報文進行發送,最終到達接收端時這些數據也都是有序的,因為TCP可以通過序號來對這些TCP報文進行順序重排,最終就能保證數據到達對端接收緩沖區中時是有序的。
TCP按序到達本身也是我們的目的,此時對端上層在從接收緩沖區讀取數據時也必須是按順序讀取的。但是有時候發送端可能發送了一些“緊急數據”,這些數據需要讓對方上層提取進行讀取,此時應該怎么辦呢?
此時就需要用到URG標志位,以及TCP報頭當中的16位緊急指針。
- 當URG標志位被設置為1時,需要通過TCP報頭當中的16位緊急指針來找到緊急數據,否則一般情況下不需要關注TCP報頭當中的16位緊急指針。
- 16位緊急指針代表的就是緊急數據在報文中的偏移量。
- 因為緊急指針只有一個,它只能標識數據段中的一個位置,因此緊急數據只能發送一個字節,而至于這一個字節的具體含義這里就不展開討論了。
recv函數的第四個參數flags有一個叫做MSG_OOB的選項可供設置,其中OOB是帶外數據(out-of-band)的簡稱,帶外數據就是一些比較重要的數據,因此上層如果想讀取緊急數據,就可以在使用recv函數進行讀取,并設置MSG_OOB選項。
與之對應的send函數的第四個參數flags也提供了一個叫做MSG_OOB的選項,上層如果想發送緊急數據,就可以使用send函數進行寫入,并設置MSG_OOB選項。
PSH
報文當中的PSH被設置為1,是在告訴對方盡快將你的接收緩沖區當中的數據交付給上層。
我們一般認為:
- 當使用read/recv從緩沖區當中讀取數據時,如果緩沖區當中有數據read/recv函數就能夠讀到數據進行返回,而如果緩沖區當中沒有數據,那么此時read/recv函數就會阻塞住,直到當緩沖區當中有數據時才會讀取到數據進行返回。
實際這種說法是不太準確的,其實接收緩沖區和發送緩沖區都有一個水位線的概念。
- 比如我們假設TCP接收緩沖區的水位線是100字節,那么只有當接收緩沖區當中有100字節時才讓read/recv函數讀取這100字節的數據進行返回。
- 如果接收緩沖區當中有一點數據就讓read/recv函數讀取返回了,此時read/recv就會頻繁的進行讀取和返回,進而影響讀取數據的效率(在內核態和用戶態之間切換也是有成本的)。
- 因此不是說接收緩沖區當中只要有數據,調用read/recv函數時就能讀取到數據進行返回,而是當緩沖區當中的數據量達到一定量時才能進行讀取。
當報文當中的PSH被設置為1時,實際就是在告知對方操作系統,盡快將接收緩沖區當中的數據交付給上層,盡管接收緩沖區當中的數據還沒到達所指定的水位線。這也就是為什么我們使用read/recv函數讀取數據時,期望讀取的字節數和實際讀取的字節數是不一定吻合的。
RST
- 報文當中的RST被設置為1,表示需要讓對方重新建立連接。
- 在通信雙方在連接未建立好的情況下,一方向另一方發數據,此時另一方發送的響應報文當中的RST標志位就會被置1,表示要求對方重新建立連接。
- 在雙方建立好連接進行正常通信時,如果通信中途發現之前建立好的連接出現了異常也會要求重新建立連接。
確認應答機制(ACK)
TCP保證可靠性的機制之一就是確認應答機制。
確認應答機制就是由TCP報頭當中的,32位序號和32位確認序號來保證的。需要再次強調的是,確認應答機制不是保證雙方通信的全部消息的可靠性,而是通過收到對方的應答消息,來保證自己曾經發送給對方的某一條消息被對方可靠的收到了。
如何理解TCP將每個字節的數據都進行了編號?
TCP是面向字節流的,我們可以將TCP的發送緩沖區和接收緩沖區都想象成一個字符數組。
- 此時上層應用拷貝到TCP發送緩沖區當中的每一個字節數據天然有了一個序號,這個序號就是字符數組的下標,只不過這個下標不是從0開始的,而是從1開始往后遞增的。
- 而雙方在通信時,本質就是將自己發送緩沖區當中的數據拷貝到對方的接收緩沖區當中。
- 發送方發送數據時報頭當中所填的序號,實際就是發送的若干字節數據當中,首個字節數據在發送緩沖區當中對應的下標。
- 接收方接收到數據進行響應時,響應報頭當中的確認序號實際就是,接收緩沖區中接收到的最后一個有效數據的下一個位置所對應的下標。
- 當發送方收到接收方的響應后,就可以從下標為確認序號的位置繼續進行發送了。
超時重傳機制
雙方在進行網絡通信時,發送方發出去的數據在一個特定的事件間隔內如果得不到對方的應答,此時發送方就會進行數據重發,這就是TCP的超時重傳機制。
需要注意的是,TCP保證雙方通信的可靠性,一部分是通過TCP的協議報頭體現出來的,還有一部分是通過實現TCP的代碼邏輯體現出來的。
比如超時重傳機制實際就是發送方在發送數據后開啟了一個定時器,若是在這個時間內沒有收到剛才發送數據的確認應答報文,則會對該報文進行重傳,這就是通過TCP的代碼邏輯實現的,而在TCP報頭當中是體現不出來的。
丟包的兩種情況
丟包分為兩種情況,一種是發送的數據報文丟失了,此時發送端在一定時間內收不到對應的響應報文,就會進行超時重傳。
丟包的另一種情況其實不是發送端發送的數據丟包了,而是對方發來的響應報文丟包了,此時發送端也會因為收不到對應的響應報文,而進行超時重傳。
- 當出現丟包時,發送方是無法辨別是發送的數據報文丟失了,還是對方發來的響應報文丟失了,因為這兩種情況下發送方都收不到對方發來的響應報文,此時發送方就只能進行超時重傳。
- 如果是對方的響應報文丟失而導致發送方進行超時重傳,此時接收方就會再次收到一個重復的報文數據,但此時也不用擔心,接收方可以根據報頭當中的32位序號來判斷曾經是否收到過這個報文,從而達到報文去重的目的。
- 需要注意的是,當發送緩沖區當中的數據被發送出去后,操作系統不會立即將該數據從發送緩沖區當中刪除或覆蓋,而會讓其保留在發送緩沖區當中,以免需要進行超時重傳,直到收到該數據的響應報文后,發送緩沖區中的這部分數據才可以被刪除或覆蓋。
超時重傳的等待時間
超時重傳的時間不能設置的太長也不能設置的太短。
- 超時重傳的時間設置的太長,會導致丟包后對方長時間收不到對應的數據,進而影響整體重傳的效率。
- 超時重傳的時間設置的太短,會導致對方收到大量的重復報文,可能對方發送的響應報文還在網絡中傳輸而并沒有丟包,但此時發送方就開始進行數據重傳了,并且發送大量重復報文會也是對網絡資源的浪費。
因此超時重傳的時間一定要是合理的,最理想的情況就是找到一個最小的時間,保證“確認應答一定能在這個時間內返回”。但這個時間的長短,是與網絡環境有關的。網好的時候重傳的時間可以設置的短一點,網卡的時候重傳的時間可以設置的長一點,也就是說超時重傳設置的等待時間一定是上下浮動的,因此這個時間不可能是固定的某個值。
TCP為了保證無論在任何環境下都能有比較高性能的通信,因此會動態計算這個最大超時時間。
- Linux中(BSD Unix和Windows也是如此),超時以500ms為一個單位進行控制,每次判定超時重發的超時時間都是500ms的整數倍。
- 如果重發一次之后,仍然得不到應答,下一次重傳的等待時間就是 2 × 500 2\times500 2×500ms。
- 如果仍然得不到應答,那么下一次重傳的等待時間就是 4 × 500 4\times500 4×500ms。以此類推,以指數的形式遞增。
- 當累計到一定的重傳次數后,TCP就會認為是網絡或對端主機出現了異常,進而強轉關閉連接。
連接管理機制
TCP是面向連接的
TCP的各種可靠性機制實際都不是從主機到主機的,而是基于連接的,與連接是強相關的。比如一臺服務器啟動后可能有多個客戶端前來訪問,如果TCP不是基于連接的,也就意味著服務器端只有一個接收緩沖區,此時各個客戶端發來的數據都會拷貝到這個接收緩沖區當中,此時這些數據就可能會互相干擾。
而我們在進行TCP通信之前需要先建立連接,就是因為TCP的各種可靠性保證都是基于連接的,要保證傳輸數據的可靠性的前提就是先建立好連接。
操作系統對連接的管理
面向連接是TCP可靠性的一種,只有在通信建立好連接才會有各種可靠性的保證,而一臺機器上可能會存在大量的連接,此時操作系統就不得不對這些連接進行管理。
- 操作系統在管理這些連接時需要“先描述,再組織”,在操作系統中一定有一個描述連接的結構體,該結構體當中包含了連接的各種屬性字段,所有定義出來的連接結構體最終都會以某種數據結構組織起來,此時操作系統對連接的管理就變成了對該數據結構的增刪查改。
- 建立連接,實際就是在操作系統中用該結構體定義一個結構體變量,然后填充連接的各種屬性字段,最后將其插入到管理連接的數據結構當中即可。
- 斷開連接,實際就是將某個連接從管理連接的數據結構當中刪除,釋放該連接曾經占用的各種資源。
- 因此連接的管理也是有成本的,這個成本就是管理連接結構體的時間成本,以及存儲連接結構體的空間成本。
三次握手
三次握手的過程
雙方在進行TCP通信之前需要先建立連接,建立連接的這個過程我們稱之為三次握手。
以服務器和客戶端為例,當客戶端想要與服務器進行通信時,需要先與服務器建立連接,此時客戶端作為主動方會先向服務器發送連接建立請求,然后雙方TCP在底層會自動進行三次握手。
- 第一次握手:客戶端向服務器發送的報文當中的SYN位被設置為1,表示請求與服務器建立連接。
- 第二次握手:服務器收到客戶端發來的連接請求報文后,緊接著向客戶端發起連接建立請求并對客戶端發來的連接請求進行響應,此時服務器向客戶端發送的報文當中的SYN位和ACK位均被設置為1。
- 第三次握手:客戶端收到服務器發來的報文后,得知服務器收到了自己發送的連接建立請求,并請求和自己建立連接,最后客戶端再向服務器發來的報文進行響應。
需要注意的是,客戶端向服務器發起的連接建立請求,是請求建立從客戶端到服務器方向的通信連接,而TCP是全雙工通信,因此服務器在收到客戶端發來的連接建立請求后,服務器也需要向客戶端發起連接建立請求,請求建立從服務器到客戶端方法的通信連接。
為什么是三次握手?
首先我們需要知道,連接建立不是百分之百能成功的,通信雙方在進行三次握手時,其中前兩次握手能夠保證被對方收到,因為前兩次握手都有對應的下一次握手對其進行響應,但第三次握手是沒有對應的響應報文的,如果第三次握手時客戶端發送的ACK報文丟失了,那么連接建立就會失敗。
雖然客戶端發起第三次握手后就完成了三次握手,但服務器卻沒有收到客戶端發來的第三次握手,此時服務器端就不會建立對應的連接。所以建立連接時不管采用幾次握手,最后一次握手的可靠性都是不能保證的。
既然連接的建立都不是百分之百成功的,因此建立連接時具體采用幾次握手的依據,實際是看幾次握手時的優點更多。
三次握手是驗證雙方通信信道的最小次數:
- 因為TCP是全雙工通信的,因此連接建立的核心要務實際是,驗證雙方的通信信道是否是連通的。
- 而三次握手恰好是驗證雙方通信信道的最小次數,通過三次握手后雙方就都能知道自己和對方是否都能夠正常發送和接收數據。
- 在客戶端看來,當它收到服務器發來第二次握手時,說明自己發出的第一次握手被對方可靠的收到了,證明自己能發以及服務器能收,同時當自己收到服務器發來的第二次握手時,也就證明服務器能發以及自己能收,此時就證明自己和服務器都是能發能收的。
- 在服務器看來,當它收到客戶端發來第一次握手時,證明客戶端能發以及自己能收,而當它收到客戶端發來的第三次握手時,說明自己發出的第二次握手被對方可靠的收到了,也就證明自己能發以及客戶端能收,此時就證明自己和客戶端都是能發能收的。
- 既然三次握手已經能夠驗證雙方通信信道是否正常了,那么三次以上的握手當然也是可以驗證的,但既然三次已經能驗證了就沒有必要再進行更多次的握手了。
三次握手能夠保證連接建立時的異常連接掛在客戶端:
- 當客戶端收到服務器發來的第二次握手時,客戶端就已經證明雙方通信信道是連通的了,因此當客戶端發出第三次握手后,這個連接就已經在客戶端建立了。
- 而只有當服務器收到客戶端發來的第三次握手后,服務器才知道雙方通信信道是連通的,此時在服務器端才會建立對應的連接。
- 因此雙方在進行三次握手建立連接時,雙方建立連接的時間點是不一樣的。如果客戶端最后發出的第三次握手丟包了,此時在服務器端就不會建立對應的連接,而在客戶端就需要短暫的維護一個異常的連接。
- 而維護連接是需要時間成本和空間成本的,因此三次握手還有一個好處就是能夠保證連接建立異常時,這個異常連接是掛在客戶端的,而不會影響到服務器。
- 雖然此時客戶端也需要短暫維護這個異常,但客戶端的異常連接不會特別多,不像服務器,一旦多個客戶端建立連接時都建立失敗了,此時服務器端就需要耗費大量資源來維護這些異常連接。
- 此外,建立連接失敗時的異常連接不會一直維護下去。如果服務器端長時間收不到客戶端發來的第三次握手,就會將第二次握手進行超時重傳,此時客戶端就有機會重新發出第三次握手。或者當客戶端認為連接建立好后向服務器發送數據時,此時服務器會發現沒有和該客戶端建立連接而要求客戶端重新建立連接。
因此,這里給出兩個建立連接時采用三次握手的理由:
- 三次握手是驗證雙方通信信道的最小次數,能夠讓能建立的連接盡快建立起來。
- 三次握手能夠保證連接建立時的異常連接掛在客戶端(風險轉移)。
三次握手時的狀態變化
三次握手時的狀態變化如下:
- 最開始時客戶端和服務器都處于CLOSED狀態。
- 服務器為了能夠接收客戶端發來的連接請求,需要由CLOSED狀態變為LISTEN狀態。
- 此時客戶端就可以向服務器發起三次握手了,當客戶端發起第一次握手后,狀態變為SYN_SENT狀態。
- 處于LISTEN狀態的服務器收到客戶端的連接請求后,將該連接放入內核等待隊列中,并向客戶端發起第二次握手,此時服務器的狀態變為SYN_RCVD。
- 當客戶端收到服務器發來的第二次握手后,緊接著向服務器發送最后一次握手,此時客戶端的連接已經建立,狀態變為ESTABLISHED。
- 而服務器收到客戶端發來的最后一次握手后,連接也建立成功,此時服務器的狀態也變成ESTABLISHED。
至此三次握手結束,通信雙方可以開始進行數據交互了。
套接字和三次握手之間的關系
- 在客戶端發起連接建立請求之前,服務器需要先進入LISTEN狀態,此時就需要服務器調用對應listen函數。
- 當服務器進入LISTEN狀態后,客戶端就可以向服務器發起三次握手了,此時客戶端對應調用的就是connect函數。
- 需要注意的是,connect函數不參與底層的三次握手,connect函數的作用只是發起三次握手。當connect函數返回時,要么是底層已經成功完成了三次握手連接建立成功,要么是底層三次握手失敗。
- 如果服務器端與客戶端成功完成了三次握手,此時在服務器端就會建立一個連接,但這個連接在內核的等待隊列當中,服務器端需要通過調用accept函數將這個建立好的連接獲取上來。
- 當服務器端將建立好的連接獲取上來后,雙方就可以通過調用read/recv函數和write/send函數進行數據交互了。
四次揮手
四次揮手的過程
由于雙方維護連接都是需要成本的,因此當雙方TCP通信結束之后就需要斷開連接,斷開連接的這個過程我們稱之為四次揮手。
還是以服務器和客戶端為例,當客戶端與服務器通信結束后,需要與服務器斷開連接,此時就需要進行四次揮手。
- 第一次揮手:客戶端向服務器發送的報文當中的FIN位被設置為1,表示請求與服務器斷開連接。
- 第二次揮手:服務器收到客戶端發來的斷開連接請求后對其進行響應。
- 第三次揮手:服務器收到客戶端斷開連接的請求,且已經沒有數據需要發送給客戶端的時候,服務器就會向客戶端發起斷開連接請求。
- 第四次揮手:客戶端收到服務器發來的斷開連接請求后對其進行響應。
四次揮手結束后雙方的連接才算真正斷開。
為什么是四次揮手?
- 由于TCP是全雙工的,建立連接的時候需要建立雙方的連接,斷開連接時也同樣如此。在斷開連接時不僅要斷開從客戶端到服務器方向的通信信道,也要斷開從服務器到客戶端的通信信道,其中每兩次揮手對應就是關閉一個方向的通信信道,因此斷開連接時需要進行四次揮手。
- 需要注意的是,四次揮手當中的第二次和第三次揮手不能合并在一起,因為第三次握手是服務器端想要與客戶端斷開連接時發給客戶端的請求,而當服務器收到客戶端斷開連接的請求并響應后,服務器不一定會馬上發起第三次揮手,因為服務器可能還有某些數據要發送給客戶端,只有當服務器端將這些數據發送完后才會向客戶端發起第三次揮手。
四次揮手時的狀態變化
四次揮手時的狀態變化如下:
- 在揮手前客戶端和服務器都處于連接建立后的ESTABLISHED狀態。
- 客戶端為了與服務器斷開連接主動向服務器發起連接斷開請求,此時客戶端的狀態變為FIN_WAIT_1。
- 服務器收到客戶端發來的連接斷開請求后對其進行響應,此時服務器的狀態變為CLOSE_WAIT。
- 當服務器沒有數據需要發送給客戶端的時,服務器會向客戶端發起斷開連接請求,等待最后一個ACK到來,此時服務器的狀態變為LASE_ACK。
- 客戶端收到服務器發來的第三次揮手后,會向服務器發送最后一個響應報文,此時客戶端進入TIME_WAIT狀態。
- 當服務器收到客戶端發來的最后一個響應報文時,服務器會徹底關閉連接,變為CLOSED狀態。
- 而客戶端則會等待一個2MSL(Maximum Segment Lifetime,報文最大生存時間)才會進入CLOSED狀態。
至此四次揮手結束,通信雙方成功斷開連接。
套接字和四次揮手之間的關系
- 客戶端發起斷開連接請求,對應就是客戶端主動調用close函數。
- 服務器發起斷開連接請求,對應就是服務器主動調用close函數。
- 一個close對應的就是兩次揮手,雙方都要調用close,因此就是四次揮手。
CLOSE_WAIT
- 雙方在進行四次揮手時,如果只有客戶端調用了close函數,而服務器不調用close函數,此時服務器就會進入CLOSE_WAIT狀態,而客戶端則會進入到FIN_WAIT_2狀態。
- 但只有完成四次揮手后連接才算真正斷開,此時雙方才會釋放對應的連接資源。如果服務器沒有主動關閉不需要的文件描述符,此時在服務器端就會存在大量處于CLOSE_WAIT狀態的連接,而每個連接都會占用服務器的資源,最終就會導致服務器可用資源越來越少。
- 因此如果不及時關閉不用的文件描述符,除了會造成文件描述符泄漏以外,可能也會導致連接資源沒有完全釋放,這其實也是一種內存泄漏的問題。
- 因此在編寫網絡套接字代碼時,如果發現服務器端存在大量處于CLOSE_WAIT狀態的連接,此時就可以檢查一下是不是服務器沒有及時調用close函數關閉對應的文件描述符。
TIME_WAIT
四次揮手中前三次揮手丟包時的解決方法:
- 第一次揮手丟包:客戶端收不到服務器的應答,進而進行超時重傳。
- 第二次揮手丟包:客戶端收不到服務器的應答,進而進行超時重傳。
- 第三次揮手丟包:服務器收不到客戶端的應答,進而進行超時重傳。
- 第四次揮手丟包:服務器收不到客戶端的應答,進而進行超時重傳。
如果客戶端在發出第四次揮手后立即進入CLOSED狀態,此時服務器雖然進行了超時重傳,但已經得不到客戶端的響應了,因為客戶端已經將連接關閉了。
服務器在經過若干次超時重發后得不到響應,最終也一定會將對應的連接關閉,但在服務器不斷進行超時重傳期間還需要維護這條廢棄的連接,這樣對服務器是非常不友好的。
為了避免這種情況,因此客戶端在四次揮手后沒有立即進入CLOSED狀態,而是進入到了TIME_WAIT狀態進行等待,此時要是第四次揮手的報文丟包了,客戶端也能收到服務器重發的報文然后進行響應。
TIME_WAIT狀態存在的必要性:
- 客戶端在進行四次揮手后進入TIME_WAIT狀態,如果第四次揮手的報文丟包了,客戶端在一段時間內仍然能夠接收服務器重發的FIN報文并對其進行響應,能夠較大概率保證最后一個ACK被服務器收到。
- 客戶端發出最后一次揮手時,雙方歷史通信的數據可能還沒有發送到對方。因此客戶端四次揮手后進入TIME_WAIT狀態,還可以保證雙方通信信道上的數據在網絡中盡可能的消散。
實際第四次揮手丟包后,可能雙方網絡狀態出現了問題,盡管客戶端還沒有關閉連接,也收不到服務器重發的連接斷開請求,此時客戶端TIME_WAIT等若干時間最終會關閉連接,而服務器經過多次超時重傳后也會關閉連接。這種情況雖然也讓服務器維持了閑置的連接,但畢竟是少數,引入TIME_WAIT狀態就是爭取讓主動發起四次揮手的客戶端維護這個成本。
因此TCP并不能完全保證建立連接和斷開連接的可靠性,TCP保證的是建立連接之后,以及斷開連接之前雙方通信數據的可靠性。
TIME_WAIT的等待時長是多少?
TIME_WAIT的等待時長既不能太長也不能太短。
- 太長會讓等待方維持一個較長的時間的TIME_WAIT狀態,在這個時間內等待方也需要花費成本來維護這個連接,這也是一種浪費資源的現象。
- 太短可能沒有達到我們最初目的,沒有保證ACK被對方較大概率收到,也沒有保證數據在網絡中消散,此時TIME_WAIT的意義也就沒有了。
TCP協議規定,主動關閉連接的一方在四次揮手后要處于TIME_WAIT狀態,等待兩個MSL(Maximum Segment Lifetime,報文最大生存時間)的時間才能進入CLOSED狀態。
MSL在RFC1122中規定為兩分鐘,但是各個操作系統的實現不同,比如在Centos7上默認配置的值是60s。我們可以通過cat /proc/sys/net/ipv4/tcp_fin_timeout
命令來查看MSL的值。
TIME_WAIT的等待時長設置為兩個MSL的原因:
- MSL是TCP報文的最大生存時間,因此TIME_WAIT狀態持續存在2MSL的話,就能保證在兩個傳輸方向上的尚未被接收或遲到的報文段都已經消失。
- 同時也是在理論上保證最后一個報文可靠到達的時間。
[流量控制]
TCP支持根據接收端的接收數據的能力來決定發送端發送數據的速度,這個機制叫做流量控制(Flow Control)。
接收端處理數據的速度是有限的,如果發送端發的太快,導致接收端的緩沖區被打滿,此時發送端繼續發送數據,就會造成丟包,進而引起丟包重傳等一系列連鎖反應。
因此接收端可以將自己接收數據的能力告知發送端,讓發送端控制自己發送數據的速度。
- 接收端將自己可以接收的緩沖區大小放入TCP首部中的“窗口大小”字段,通過ACK通知發送端。
- 窗口大小字段越大,說明網絡的吞吐量越高。
- 接收端一旦發現自己的緩沖區快滿了,就會將窗口大小設置成一個更小的值通知給發送端。
- 發送端接收到這個窗口之后,就會減慢自己發送的速度。
- 如果接收端緩沖區滿了,就會將窗口值設置為0,這時發送方不再發送數據,但需要定期發送一個窗口探測數據段,使接收端把窗口大小告訴發送端。
當發送端得知接收端接收數據的能力為0時會停止發送數據,此時發送端會通過以下兩種方式來得知何時可以繼續發送數據。
- 等待告知。接收端上層將接收緩沖區當中的數據讀走后,接收端向發送端發送一個TCP報文,主動將自己的窗口大小告知發送端,發送端得知接收端的接收緩沖區有空間后就可以繼續發送數據了。
- 主動詢問。發送端每隔一段時間向接收端發送報文,該報文不攜帶有效數據,只是為了詢問發送端的窗口大小,直到接收端的接收緩沖區有空間后發送端就可以繼續發送數據了。
16為數字最大表示65535,那TCP窗口最大就是65535嗎?
理論上確實是這樣的,但實際上TCP報頭當中40字節的選項字段中包含了一個窗口擴大因子M,實際窗口大小是窗口字段的值左移M位得到的。
第一次向對方發送數據時如何得知對方的窗口大小?
雙方在進行TCP通信之前需要先進行三次握手建立連接,而雙方在握手時除了驗證雙方通信信道是否通暢以外,還進行了其他信息的交互,其中就包括告知對方自己的接收能力,因此在雙方還沒有正式開始通信之前就已經知道了對方接收數據能力,所以雙方在發送數據時是不會出現緩沖區溢出的問題的。
滑動窗口
連續發送多個數據
雙方在進行TCP通信時可以一次向對方發送多條數據,這樣可以將等待多個響應的時間重疊起來,進而提高數據通信的效率。
需要注意的是,雖然雙方在進行TCP通信時可以一次向對方發送大量的報文,但不能將自己發送緩沖區當中的數據全部打包發送給對端,在發送數據時還要考慮對方的接收能力。
滑動窗口
發送方可以一次發送多個報文給對方,此時也就意味著發送出去的這部分報文當中有相當一部分數據是暫時沒有收到應答的。
其實可以將發送緩沖區當中的數據分為三部分:
- 已經發送并且已經收到ACK的數據。
- 已經發送還但沒有收到ACK的數據。
- 還沒有發送的數據。
這里發送緩沖區的第二部分就叫做滑動窗口。(也有人把這三部分整體稱之為滑動窗口,而將其中的第二部分稱之為窗口大小)
而滑動窗口描述的就是,發送方不用等待ACK一次所能發送的數據最大量。
滑動窗口存在的最大意義就是可以提高發送數據的效率:
- 滑動窗口的大小等于對方窗口大小與自身擁塞窗口大小的較小值,因為發送數據時不僅要考慮對方的接收能力,還要考慮當前網絡的狀況。
- 我們這里先不考慮擁塞窗口,并且假設對方的窗口大小一直固定為4000,此時發送方不用等待ACK一次所能發送的數據就是4000字節,因此滑動窗口的大小就是4000字節。(四個段)
- 現在連續發送1001-2000、2001-3000、3001-4000、4001-5000這四個段的時候,不需要等待任何ACK,可以直接進行發送。
- 當收到對方響應的確認序號為2001時,說明1001-2000這個數據段已經被對方收到了,此時該數據段應該被納入發送緩沖區當中的第一部分,而由于我們假設對方的窗口大小一直是4000,因此滑動窗口現在可以向右移動,繼續發送5001-6000的數據段,以此類推。
- 滑動窗口越大,則網絡的吞吐率越高,同時也說明對方的接收能力很強。
當發送方發送出去的數據段陸陸續續收到對應的ACK時,就可以將收到ACK的數據段歸置到滑動窗口的左側,并根據當前滑動窗口的大小來決定,是否需要將滑動窗口右側的數據歸置到滑動窗口當中。
TCP的重傳機制要求暫時保存發出但未收到確認的數據,而這部分數據實際就位于滑動窗口當中,只有滑動窗口左側的數據才是可以被覆蓋或刪除的,因為這部分數據才是發送并被對方可靠的收到了,所以滑動窗口除了限定不收到ACK而可以直接發送的數據之外,滑動窗口也可以支持TCP的重傳機制。
滑動窗口一定會整體右移嗎?
滑動窗口不一定會整體右移的,以剛才的例子為例,假設對方已經收到了1001-2000的數據段并進行了響應,但對方上層一直不從接收緩沖區當中讀取數據,此時當對方收到1001-2000的數據段時,對方的窗口大小就由4000變為了3000。
當發送端收到對方的響應序號為2001時,就會將1001-2000的數據段歸置到滑動窗口的左側,但此時由于對方的接收能力變為了3000,而當1001-2000的數據段歸置到滑動窗口的左側后,滑動窗口的大小剛好就是3000,因此滑動窗口的右側不能繼續向右進行擴展。
因此滑動窗口在向右移動的過程中并不一定是整體右移的,因為對方接收能力可能不斷在變化,從而滑動窗口也會隨之不斷變寬或者變窄。
如何實現滑動窗口
TCP接收和發送緩沖區都看作一個字符數組,而滑動窗口實際就可以看作是兩個指針限定的一個范圍,比如我們用 s t a r t start start指向滑動窗口的左側, e n d end end指向的是滑動窗口的右側,此時在 s t a r t start start和 e n d end end區間范圍內的就可以叫做滑動窗口。
當發送端收到對方的響應時,如果響應當中的確認序號為 x x x,窗口大小為 w i n win win,此時就可以將start更新為 x x x,而將 e n d end end更新為 s t a r t + w i n start+win start+win。
丟包問題
當發送端一次發送多個報文數據時,此時的丟包情況也可以分為兩種。
情況一: 數據包已經抵達,ACK丟包。
在發送端連續發送多個報文數據時,部分ACK丟包并不要緊,此時可以通過后續的ACK進行確認。
比如圖中2001-3000和4001-5000的數據包對應的ACK丟失了,但只要發送端收到了最后5001-6000數據包的響應,此時發送端也就知道2001-3000和4001-5000的數據包實際上被接收端收到了的,因為如果接收方沒有收到2001-3000和4001-5000的數據包是設置確認序號為6001的,確認序號為6001的含義就是序號為1-6000的字節數據我都收到了,你下一次應該從序號為6001的字節數據開始發送。
情況二: 數據包丟了。
- 當1001-2000的數據包丟失后,發送端會一直收到確認序號為1001的響應報文,就是在提醒發送端“下一次應該從序號為1001的字節數據開始發送”。
- 如果發送端連續收到三次確認序號為1001的響應報文,此時就會將1001-2000的數據包重新進行發送。
- 此時當接收端收到1001-2000的數據包后,就會直接發送確認序號為6001的響應報文,因為2001-6000的數據接收端其實在之前就已經收到了。
這種機制被稱為“高速重發控制”,也叫做“快重傳”。
需要注意的是,快重傳需要在大量的數據重傳和個別的數據重傳之間做平衡,實際這個例子當中發送端并不知道是1001-2000這個數據包丟了,當發送端重復收到確認序號為1001的響應報文時,理論上發送端應該將1001-7000的數據全部進行重傳,但這樣可能會導致大量數據被重復傳送,所以發送端可以嘗試先把1001-2000的數據包進行重發,然后根據重發后的得到的確認序號繼續決定是否需要重發其它數據包。
滑動窗口中的數據一定都沒有被對方收到嗎?
滑動窗口當中的數據是可以暫時不用收到對方確認的數據,而不是說滑動窗口當中的數據一定都沒有被對方收到,滑動窗口當中可能有一部分數據已經被對方收到了,但可能因為滑動窗口內靠近滑動窗口左側的一部分數據,在傳輸過程中出現了丟包等情況,導致后面已經被對方收到的數據得不到響應。
例如圖中的1001-2000的數據包如果在傳輸過程中丟包了,此時雖然2001-5000的數據都被對方收到了,此時對方發來的確認序號也只能是1001,當發送端補發了1001-2000的數據包后,對方發來的確認序號就會變為5001,此時發送緩沖區當中1001-5000的數據也會立馬被歸置到滑動窗口的左側。
快重傳 VS 超時重傳
- 快重傳是能夠快速進行數據的重發,當發送端連續收到三次相同的應答時就會觸發快重傳,而不像超時重傳一樣需要通過設置重傳定時器,在固定的時間后才會進行重傳。
- 雖然快重傳能夠快速判定數據包丟失,但快重傳并不能完全取待超時重傳,因為有時數據包丟失后可能并沒有收到對方三次重復的應答,此時快重傳機制就觸發不了,而只能進行超時重傳。
- 因此快重傳雖然是一個效率上的提升,但超時重傳卻是所有重傳機制的保底策略,也是必不可少的。
擁塞控制
為什么會有擁塞控制?
兩個主機在進行TCP通信的過程中,出現個別數據包丟包的情況是很正常的,此時可以通過快重傳或超時重發對數據包進行補發。但如果雙方在通信時出現了大量丟包,此時就不能認為是正常現象了。
TCP不僅考慮了通信雙端主機的問題,同時也考慮了網絡的問題。
- 流量控制:考慮的是對端接收緩沖區的接收能力,進而控制發送方發送數據的速度,避免對端接收緩沖區溢出。
- 滑動窗口:考慮的是發送端不用等待ACK一次所能發送的數據最大量,進而提高發送端發送數據的效率。
- 擁塞窗口:考慮的是雙方通信時網絡的問題,如果發送的數據超過了擁塞窗口的大小就可能會引起網絡擁塞。
雙方網絡通信時出現少量的丟包TCP是允許的,但一旦出現大量的丟包,此時量變引起質變,這件事情的性質就變了,此時TCP就不再推測是雙方接收和發送數據的問題,而判斷是雙方通信信道網絡出現了擁塞問題。
如何解決網絡擁塞問題?
網絡出現大面積癱瘓時,通信雙方作為網絡當中兩臺小小的主機,看似并不能為此做些什么,但“雪崩的時候沒有一片雪花是無辜的”,網絡出現問題一定是網絡中大部分主機共同作用的結果。
- 如果網絡中的主機在同一時間節點都大量向網絡當中塞數據,此時位于網絡中某些關鍵節點的路由器下就可能排了很長的報文,最終導致報文無法在超時時間內到達對端主機,此時也就導致了丟包問題。
- 當網絡出現擁塞問題時,通信雙方雖然不能提出特別有效的解決方案,但雙方主機可以做到不加重網絡的負擔。
- 雙方通信時如果出現大量丟包,不應該立即將這些報文進行重傳,而應該少發數據甚至不發數據,等待網絡狀況恢復后雙方再慢慢恢復數據的傳輸速率。
需要注意的是,網絡擁塞時影響的不只是一臺主機,而幾乎是該網絡當中的所有主機,此時所有使用TCP傳輸控制協議的主機都會執行擁塞避免算法。
因此擁塞控制看似只是談論的一臺主機上的通信策略,實際這個策略是所有主機在網絡崩潰后都會遵守的策略。一旦出現網絡擁塞,該網絡當中的所有主機都會受到影響,此時所有主機都要執行擁塞避免,這樣才能有效緩解網絡擁塞問題。通過這樣的方式就能保證雪崩不會發生,或雪崩發生后可以盡快恢復。
擁塞控制
雖然滑動窗口能夠高效可靠的發送大量的數據,但如果在剛開始階段就發送大量的數據,就可能會引發某些問題。因為網絡上有很多的計算機,有可能當前的網絡狀態就已經比較擁塞了,因此在不清楚當前網絡狀態的情況下,貿然發送大量的數據,就可能會引起網絡擁塞問題。
因此TCP引入了慢啟動機制,在剛開始通信時先發少量的數據探探路,摸清當前的網絡擁堵狀態,再決定按照多大的速度傳輸數據。
- TCP除了有窗口大小和滑動窗口的概念以外,還有一個窗口叫做擁塞窗口。擁塞窗口是可能引起網絡擁塞的閾值,如果一次發送的數據超過了擁塞窗口的大小就可能會引起網絡擁塞。
- 剛開始發送數據的時候擁塞窗口大小定義以為1,每收到一個ACK應答擁塞窗口的值就加一。
- 每次發送數據包的時候,將擁塞窗口和接收端主機反饋的窗口大小做比較,取較小的值作為實際發送數據的窗口大小,即滑動窗口的大小。
每收到一個ACK應答擁塞窗口的值就加一,此時擁塞窗口就是以指數級別進行增長的,如果先不考慮對方接收數據的能力,那么滑動窗口的大家就只取決于擁塞窗口的大小,此時擁塞窗口的大小變化情況如下:
擁塞窗口 | 滑動窗口 |
---|---|
1= 2 0 2^0 20 | 1 |
1+1= 2 1 2^1 21 | 2 |
2+2= 2 2 2^2 22 | 4 |
4+4= 2 3 2^3 23 | 8 |
… | … |
但指數級增長是非常快的,因此“慢啟動”實際只是初始時比較慢,但越往后增長的越快。如果擁塞窗口的值一直以指數的方式進行增長,此時就可能在短時間內再次導致網絡出現擁塞。
- 為了避免短時間內再次導致網絡擁塞,因此不能一直讓擁塞窗口按指數級的方式進行增長。
- 此時就引入了慢啟動的閾值,當擁塞窗口的大小超過這個閾值時,就不再按指數的方式增長,而按線性的方式增長。
- 當TCP剛開始啟動的時候,慢啟動閾值設置為對方窗口大小的最大值。
- 在每次超時重發的時候,慢啟動閾值會變成當前擁塞窗口的一半,同時擁塞窗口的值被重新置為1,如此循環下去。
如下圖:
圖示說明:
- 指數增長。剛開始進行TCP通信時擁塞窗口的值為1,并不斷按指數的方式進行增長。
- 加法增大。慢啟動的閾值初始時為對方窗口大小的最大值,圖中慢啟動閾值的初始值為16,因此當擁塞窗口的值增大到16時就不再按指數形式增長了,而變成了的線性增長。
- 乘法減小。擁塞窗口在線性增長的過程中,在增大到24時如果發生了網絡擁塞,此時慢啟動的閾值將變為當前擁塞窗口的一半,也就是12,并且擁塞窗口的值被重新設置為1,所以下一次擁塞窗口由指數增長變為線性增長時擁塞窗口的值應該是12。
主機在進行網絡通信時,實際就是在不斷進行指數增長、加法增大和乘法減小。
需要注意的是,不是所有的主機都是同時在進行指數增長、加法增大和乘法減小的。每臺主機認為擁塞窗口的大小不一定是一樣的,即便是同區域的兩臺主機在同一時刻認為擁塞窗口的大小也不一定是完全相同的。因此在同一時刻,可能一部分主機正在進行正常通信,而另一部分主機可能已經發生網絡擁塞了。
延遲應答
如果接收數據的主機收到數據后立即進行ACK應答,此時返回的窗口可能比較小。
- 假設對方接收端緩沖區剩余空間大小為1M,對方一次收到500K的數據后,如果立即進行ACK應答,此時返回的窗口就是500K。
- 但實際接收端處理數據的速度很快,10ms之內就將接收緩沖區中500K的數據消費掉了。
- 在這種情況下,接收端處理還遠沒有達到自己的極限,即使窗口再放大一些,也能處理過來。
- 如果接收端稍微等一會再進行ACK應答,比如等待200ms再應答,那么這時返回的窗口大小就是1M。
需要注意的是,延遲應答的目的不是為了保證可靠性,而是留出一點時間讓接收緩沖區中的數據盡可能被上層應用層消費掉,此時在進行ACK響應的時候報告的窗口大小就可以更大,從而增大網絡吞吐量,進而提高數據的傳輸效率。
此外,不是所有的數據包都可以延遲應答。
- 數量限制:每個N個包就應答一次。
- 時間限制:超過最大延遲時間就應答一次(這個時間不會導致誤超時重傳)。
延遲應答具體的數量和超時時間,依操作系統不同也有差異,一般N取2,超時時間取200ms。
捎帶應答
捎帶應答其實是TCP通信時最常規的一種方式,就好比主機A給主機B發送了一條消息,當主機B收到這條消息后需要對其進行ACK應答,但如果主機B此時正好也要給主機A發生消息,此時這個ACK就可以搭順風車,而不用單獨發送一個ACK應答,此時主機B發送的這個報文既發送了數據,又完成了對收到數據的響應,這就叫做捎帶應答。
捎帶應答最直觀的角度實際也是發送數據的效率,此時雙方通信時就可以不用再發送單純的確認報文了。
此外,由于捎帶應答的報文攜帶了有效數據,因此對方收到該報文后會對其進行響應,當收到這個響應報文時不僅能夠確保發送的數據被對方可靠的收到了,同時也能確保捎帶的ACK應答也被對方可靠的收到了。
面向字節流
當創建一個TCP的socket時,同時在內核中會創建一個發送緩沖區和一個接收緩沖區。
- 調用write函數就可以將數據寫入發送緩沖區中,此時write函數就可以進行返回了,接下來發送緩沖區當中的數據就是由TCP自行進行發送的。
- 如果發送的字節數太長,TCP會將其拆分成多個數據包發出。如果發送的字節數太短,TCP可能會先將其留在發送緩沖區當中,等到合適的時機再進行發送。
- 接收數據的時候,數據也是從網卡驅動程序到達內核的接收緩沖區,可以通過調用read函數來讀取接收緩沖區當中的數據。
- 而調用read函數讀取接收緩沖區中的數據時,也可以按任意字節數進行讀取。
由于緩沖區的存在,TCP程序的讀和寫不需要一一匹配,例如:
- 寫100個字節數據時,可以調用一次write寫100字節,也可以調用100次write,每次寫一個字節。
- 讀100個字節數據時,也完全不需要考慮寫的時候是怎么寫的,既可以一次read100個字節,也可以一次read一個字節,重復100次。
實際對于TCP來說,它并不關心發送緩沖區當中的是什么數據,在TCP看來這些只是一個個的字節數據,它的任務就是將這些數據準確無誤的發送到對方的接收緩沖區當中就行了,而至于如何解釋這些數據完全由上層應用來決定,這就叫做面向字節流。
粘包問題
什么是粘包?
- 首先要明確,粘包問題中的“包”,是指的應用層的數據包。
- 在TCP的協議頭中,沒有如同UDP一樣的“報文長度”這樣的字段。
- 站在傳輸層的角度,TCP是一個一個報文過來的,按照序號排好序放在緩沖區中。
- 但站在應用層的角度,看到的只是一串連續的字節數據。
- 那么應用程序看到了這么一連串的字節數據,就不知道從哪個部分開始到哪個部分,是一個完整的應用層數據包。
如何解決粘包問題
要解決粘包問題,本質就是要明確報文和報文之間的邊界。
- 對于定長的包,保證每次都按固定大小讀取即可。
- 對于變長的包,可以在報頭的位置,約定一個包總長度的字段,從而就知道了包的結束位置。比如HTTP報頭當中就包含Content-Length屬性,表示正文的長度。
- 對于變長的包,還可以在包和包之間使用明確的分隔符。因為應用層協議是程序員自己來定的,只要保證分隔符不和正文沖突即可。
UDP是否存在粘包問題?
- 對于UDP,如果還沒有上層交付數據,UDP的報文長度仍然在,同時,UDP是一個一個把數據交付給應用層的,有很明確的數據邊界。
- 站在應用層的角度,使用UDP的時候,要么收到完整的UDP報文,要么不收,不會出現“半個”的情況。
因此UDP是不存在粘包問題的,根本原因就是UDP報頭當中的16位UDP長度記錄的UDP報文的長度,因此UDP在底層的時候就把報文和報文之間的邊界明確了,而TCP存在粘包問題就是因為TCP是面向字節流的,TCP報文之間沒有明確的邊界。
TCP異常情況
進程終止
當客戶端正常訪問服務器時,如果客戶端進程突然崩潰了,此時建立好的連接會怎么樣?
當一個進程退出時,該進程曾經打開的文件描述符都會自動關閉,因此當客戶端進程退出時,相當于自動調用了close函數關閉了對應的文件描述符,此時雙方操作系統在底層會正常完成四次揮手,然后釋放對應的連接資源。也就是說,進程終止時會釋放文件描述符,TCP底層仍然可以發送FIN,和進程正常退出沒有區別。
機器重啟
當客戶端正常訪問服務器時,如果將客戶端主機重啟,此時建立好的連接會怎么樣?
當我們選擇重啟主機時,操作系統會先殺掉所有進程然后再進行關機重啟,因此機器重啟和進程終止的情況是一樣的,此時雙方操作系統也會正常完成四次揮手,然后釋放對應的連接資源。
機器掉電/網線斷開
當客戶端正常訪問服務器時,如果將客戶端突然掉線了,此時建立好的連接會怎么樣?
當客戶端掉線后,服務器端在短時間內無法知道客戶端掉線了,因此在服務器端會維持與客戶端建立的連接,但這個連接也不會一直維持,因為TCP是有保活策略的。
- 服務器會定期客戶端客戶端的存在狀況,檢查對方是否在線,如果連續多次都沒有收到ACK應答,此時服務器就會關閉這條連接。
- 此外,客戶端也可能會定期向服務器“報平安”,如果服務器長時間沒有收到客戶端的消息,此時服務器也會將對應的連接關閉。
其中服務器定期詢問客戶端的存在狀態的做法,叫做基于保活定時器的一種心跳機制,是由TCP實現的。此外,應用層的某些協議,也有一些類似的檢測機制,例如基于長連接的HTTP,也會定期檢測對方的存在狀態。
TCP小結
TCP協議這么復雜就是因為TCP既要保證可靠性,同時又盡可能的提高性能。
可靠性:
- 檢驗和。
- 序列號。
- 確認應答。
- 超時重傳。
- 連接管理。
- 流量控制。
- 擁塞控制。
提高性能:
- 滑動窗口。
- 快速重傳。
- 延遲應答。
- 捎帶應答。
需要注意的是,TCP的這些機制有些能夠通過TCP報頭體現出來的,但還有一些是通過代碼邏輯體現出來的。
TCP定時器
此外,TCP當中還設置了各種定時器。
- 重傳定時器:為了控制丟失的報文段或丟棄的報文段,也就是對報文段確認的等待時間。
- 堅持定時器:專門為對方零窗口通知而設立的,也就是向對方發送窗口探測的時間間隔。
- 保活定時器:為了檢查空閑連接的存在狀態,也就是向對方發送探查報文的時間間隔。
- TIME_WAIT定時器:雙方在四次揮手后,主動斷開連接的一方需要等待的時長。
理解傳輸控制協議
TCP的各種機制實際都沒有談及數據真正的發送,這些都叫做傳輸數據的策略。TCP協議是在網絡數據傳輸當中做決策的,它提供的是理論支持,比如TCP要求當發出的報文在一段時間內收不到ACK應答就應該進行超時重傳,而數據真正的發送實際是由底層的IP和MAC幀完成的。
TCP做決策和IP+MAC做執行,我們將它們統稱為通信細節,它們最終的目的就是為了將數據傳輸到對端主機。而傳輸數據的目的是什么則是由應用層決定的。因此應用層決定的是通信的意義,而傳輸層及其往下的各層決定的是通信的方式。
基于TCP的應用層協議
常見的基于TCP的應用層協議如下:
- HTTP(超文本傳輸協議)。
- HTTPS(安全數據傳輸協議)。
- SSH(安全外殼協議)。
- Telnet(遠程終端協議)。
- FTP(文件傳輸協議)。
- SMTP(電子郵件傳輸協議)。
當然,也包括你自己寫TCP程序時自定義的應用層協議。
談談云服務器
SSH也就是Xshell的底層協議,我們使用Xshell時實際就是使用Xshell的ssh客戶端連接我們的云服務器。
我們在使用Xshell時,可以通過ssh 用戶名@主機名(IP地址)
方式連接云服務器。實際就是因為我們的云服務器當中存在sshd這樣的服務。
這實際就是ssh服務的服務器端,我們使用的ssh 用戶名@主機名(IP地址)
命令當中的ssh實際是ssh的客戶端,因此我們連接云服務器時本質是在用ssh的客戶端連接ssh的服務器。
使用netstat
命令可以看到對應的ssh服務。
我們在云服務器上敲出的各種命令,最終會通過網絡套接字的方式發送給服務器,由服務器來對我們的命令進行各種解釋,進而執行對應的動作。