UDP基本包括了傳輸層所必須的埠欄位,它相信“網之初,性本善,不丟包,不亂序”,
后來呢,我們都慢慢長大,了解了社會的殘酷,變得復雜而成熟,就像TCP協議一樣,它之所以這么復雜,那是因為它秉承的是“性惡論”,它天然認為網路環境是惡劣的,丟包、亂序、重傳,擁塞都是常有的事情,一言不合就可能送達不了,因而要從演算法層面來保證可靠性,
TCP包頭格式
- TCP頭

源埠號和目標埠號和UDP是一樣的,如果沒有這兩個埠號,資料就不知道應該發給哪個應用,
包的序號,為什么要給包編號呢?
為了解決亂序的問題,不編好號怎么確認哪個應該先來,哪個應該后到呢,編號是為了解決亂序問題,
確認序號,
發出去的包應該有確認,若沒有收到就應該重發,直到送達,以達到不丟包的目的,
TCP是靠譜的協議,但不代表它所處的網路環境很好,
IP層來看,如果網路狀況的確差,無任何可靠性保證,即使是IP的上一層TCP也無能為力,能做的只是更努力,不斷重傳,通過各種演算法盡量保證,
即對于TCP,IP層你丟不丟包,我管不著,但在我TCP層,會盡力保證可靠性,
一些狀態位,例如:
- SYN
發起一個連接 - ACK
回復 - RST
重新連接 - FIN
結束連接
TCP是面向連接的,因而雙方要維護連接的狀態,這些帶狀態位的包的發送,會引起雙方的狀態變更,
視窗大小,
TCP要做流量控制,通信雙方各宣告一個視窗,標識自己當前能夠的處理能力,別發送太快或太慢,
擁塞控制,對于真正的通路堵車不堵車,它無能為力,唯一能做的就是控制自己,也即控制發送的速度,
TCP的三次握手
首先要先建立一個連接,TCP的連接建立,常稱為三次握手,
- A:您好,我是A
- B:您好A,我是B
- A:您好B
也常稱為“請求->應答->應答之應答”的三個回合,
為什么要三次,兩次不夠嗎?
我們生活里兩個人打招呼,一來一回就夠了呀,那既然為了可靠,為啥不是四次?
假設這個通路是非常不可靠的,A要發起一個連接,當發了第一個請求,無回應,會有很多的可能性,比如:
- 第一個請求包丟了
- 沒有丟,但是繞了彎路,超時了
- B沒有回應,不想和我連接
A不能確認結果,于是再發發發,終于有個請求包到了B,但請求包到了B這件事,目前A還是不知道,所以A可能再發,
B收到請求包,就知道了A的存在,并且知道A要和它建立連接,
若B
- 不情愿建立連接,則A會重試一陣后放棄,連接建立失敗,沒有問題
- 樂意建立連接,則會發送應答包給A
對于B,這個應答包也是不知道能不能到達A,這時B自然也不能認為連接是建立好了,因為應答包:
- 會丟
- 會繞彎路
- A掛了
都可能,而且這時B還能碰到一個詭異現象:A和B原來建立了連接,簡單通信后,結束了連接,還記得吧,A建立連接時,請求包可能重復發幾次,有的請求包繞了一大圈又回來了,B會認為這也是一個正常的的請求的話,因此建立了連接,可以想象,這個連接不會進行下去,也沒有個終結的時候,純屬單相思,
所以兩次握手不夠,
B發送的應答可能會發送多次,但只要一次到達A,A就認為連接已建立,因為對于A,他的訊息有去有回,A會給B發送應答之應答,而B也在等這個訊息,才能確認連接的建立,只有等到了這個訊息,對于B來講,才算它的訊息有去有回,
當然A發給B的應答之應答也可能:
- 丟了
- 繞路
- B掛了
看起來應該還有個應答的應答的應答,但這樣下去就沒底了,所以四次握手是可以的,四十次都可以,關鍵四百次你也不能保證就真的可靠了,
所以只要保證:雙方的訊息都有去有回即可,
實際上大部分情況下,A和B建立連接后,A會馬上發送資料,一旦A發送資料,則很多問題都得到解決,
例如A發給B的應答丟了,當A后續發送的資料到達時,B可認為該連接已建立,或B就是掛了,A發送的資料,會報錯,說明B不可達,A就知道B有例外,
當然你可以說A比較壞,就是不發資料,建立連接后一直空著,可以開啟keepalive機制,即使沒有真實資料包,也有探活包,
作為服務端B的開發人員,對于A這種長時間不發包的客戶端,可主動關閉,從而空出資源回應其它客戶端,
三次握手還為了解決
TCP包的序號問題
A要告訴B,我這面發起的包的序號起始是從哪個號開始的,B同樣也要告訴A,B發起的包的序號起始是從哪個號開始的,
為什么序號不能都從1開始?
這樣往往會出現沖突,
例如,A連上B后,發了1、2、3三包,
發送3時,中間丟了或繞路了,于是重發,
后來A掉線了,重連上B后,序號又從1開始,然后發送2,但沒想過發送3,但上次繞路的那個3又回來了,發給了B,B自然認為,這就是下一個包,于是發生錯誤!
所以每個連接都要有不同序號,
序號的起始序號隨著時間變化,可看成一個32位的計數器,每4ms加一,
稍稍計算一下,若重復,需4h+,那個繞路的包也早就沒了,因為IP包頭里有個TTL,生存時間,
最后雙方終于成功建立了連接,為維護該連接,雙方都要維護一個狀態機,在連接建立的程序中,雙方的狀態變化時序圖就像:

- 起初,客戶端、服務端處CLOSED狀態
- 先是服務端主動監聽某個埠,處于LISTEN狀態,
- 然后客戶端主動發起連接SYN,之后處于SYN-SENT狀態
- 服務端收到發起的連接,回傳SYN,并且ACK客戶端的SYN,之后處于SYN-RCVD狀態
- 客戶端收到服務端發送的SYN和ACK之后,發送ACK的ACK,之后處于ESTABLISHED狀態,因為它一發一收成功了
- 服務端收到ACK的ACK之后,處于ESTABLISHED狀態,因為它也一發一收
TCP四次揮手
好說好散,四次揮手,
- A:B啊,我不想玩了
- B:哦,你不想玩了啊,我知道了
這個時候,還只是A不想玩了,即A不會再發資料,但B能不能在ACK時,直接關閉呢?當然不可以,很可能A是發完了最后的資料就準備不玩了,但是B還沒做完自己的事情,還是可發送資料,所以稱為半關閉狀態,
這時A可選擇:
- 不再接收資料
- 或最后再接收一段資料,等待B也主動關閉
B:A啊,好吧,我也不玩了,拜拜
A:好的,拜拜
這樣整個連接就關閉了,這是和平分手,
A開始說“不玩了”,B說“知道了”,這個沒問題,因為在此之前,雙方還處于合作的狀態,
若A說“不玩了”,沒有收到回復,則A會重發“不玩了”,但這個回合結束后,可能出現例外情況,因為已有一方率先撕破臉:
- A說完“不玩了”后,直接跑路,就有問題,因為B還沒發起結束,而若A跑路,B就算發起結束,也得不到回答,B就不知道該怎么辦了
- A說完“不玩了”,B直接跑路,也有問題,因為A不知道B是還有事情要處理,還是過一會兒會發送結束回來
解決這些問題?
TCP協議專門設計了幾個狀態來處理這些問題,我們來看斷開連接的時候的狀態時序圖

斷開時可見:
- 當A說“不玩了”,就進入FIN_WAIT_1狀態
- B收到“A不玩”訊息后,發送知道了,進入CLOSE_WAIT狀態
- A收到“B說知道了”,就進入FIN_WAIT_2狀態,若此時B直接跑路,則A將永遠在這個狀態,TCP協議里面并沒有對這個狀態的處理,但Linux有,可以調整tcp_fin_timeout引數,設定一個超時時間,
- 若B沒有跑路,發送“B也不玩了”請求到達A時
- A發送“知道B也不玩了”ACK后,從FIN_WAIT_2狀態結束,按理說,A現在可以跑路了,但最后的這個ACK萬一B收不到呢?則B會重新發一個“B不玩了”,這時若A已跑路,B就再也收不到ACK,所以TCP協議要求A最后等待一段時間TIME_WAIT,這個時間要足夠長,長到如果B沒收到ACK,“B說不玩了”會重發的,A會重新發一個ACK并且足夠時間到達B,
A直接跑路還有一個問題是,A的埠就直接空出來了,但B不知道,B原來發過的很多包很可能還在路上,此時若A的埠被一個新應用占用,這個新應用會收到上個連接中B發過來的包,雖然序列號是重新生成的,但這里要上一個雙保險,防止產生混亂,因而也需要等足夠長的時間,等到原來B發送的所有的包都死翹翹,再空出埠來,
等待的時間設為2MSL,MSL:Maximum Segment Lifetime,報文最大生存時間,是任何報文在網路上存在的最長時間,超過這個時間報文將被丟棄,因為TCP報文基于是IP協議的,而IP頭中有一個TTL域,是IP資料報可以經過的最大路由數,每經過一個處理他的路由器此值就減1,當此值為0則資料報將被丟棄,同時發送ICMP報文通知源主機,協議規定MSL為2分鐘,實際應用中常用的是30秒,1分鐘和2分鐘等,
若B超過2MSL,依然沒有收到它發的FIN的ACK,怎么辦?
按TCP原理,B會重發FIN,這時A再收到這個包后,A就表示,我已經在這里等了這么長時間了,仁至義盡,之后的我也不認了,于是就直接發送RST,B就知道A早跑了,
TCP狀態機
將連接建立和連接斷開的兩個時序狀態圖綜合起來,就是這個著名的TCP的狀態機,學習的時候比較建議將這個狀態機和時序狀態機對照著看,不然容易暈,

阿拉伯數字序號,是連接程序中的順序,而大寫中文數字的序號,是連接斷開程序中的順序,
- 加粗的實線是客戶端A的狀態變遷
- 加粗的虛線是服務端B的狀態變遷
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/292956.html
標籤:其他
上一篇:MySQL資料庫(基礎)
