TCP協議
TCP協議格式

確認序號:沒有收到就應該重新發送,直到送達。需要不能從1(泛指固定值)開始,假如A發送1、2、3,包超時了,然后A重新發數據包,本次只發送1、2,假如超時的3突然在這時候返回,B就會認為A發過來的數據是1、2、3,則序號的起始序號是隨著時間變化的,可以看成一個32位的計數器,每4ms加1,我們知道IP包頭部里面一個TTL,也即是生存時間。
窗口大小:tcp要做流量控制,通訊雙方各生命一個窗口,標示自己當前能夠的處理能力,別發送的太快,也別發的太慢。TCP還會做擁塞控制,控制發送速度。
狀態位:SYN發起一個連接;ACK回復; FIN結束連接;RST重新連接;PSH表示有data數據傳輸;
ACK是可能與SYN,FIN等同時使用的,比如SYN和ACK同時為1時,表示的就是建立連接之后的響應。如果只是一個單獨的SYN,表示的只是建立連接。
校驗和:目的是為了發現TCP首部和數據在發送端到接收端之間發生的任何改動。如果接收方檢測到檢驗和有差錯,則TCP段會被直接丟棄。
緊急指針:有時一些應用程序在某些緊急情況下(如在某些連接中進行強制中斷)要求在接收方在沒有處理完數據之前就能夠發送一些緊急數據,這就使得發送方將CODE字段的URG置為1 即緊急指針字段有效這樣可以不必考慮你發送的緊急數據在數據流中的位置,也就是相當于優先級最高。
Option: 這部分最多包含40字節,我們常用的有7種,kind0到kind7。
kind0:放在末尾用于填充,說明首部沒有更多的信息了。應用數據在下一個32位處開始。
kind1:空操作,一般用于將TCP選項的總長度填充為4字節的整數倍
kind2:協商最大報文段長度MSS
kind3:TCP連接初始化時,通信雙方使用該選項來協商接收窗口的大小。在TCP的頭部中,接收窗口大小是用16位表示的,故最大為65535字節,但實際上TCP模塊允許的接收窗口大小遠不止這個數(為了提高TCP通信的吞吐量)。窗口擴大因子解決了這個問題。
kind4:選擇性確認(SACK)選項
kind5:SACK實際工作的選項該選項的參數告訴發送方本端已經收到并緩存的不連續的數據塊,從而讓發送端可以據此檢查并重發丟失的數據塊
kind8:時間戳,為了較為準確的計算通訊雙方的回路時間RTT,為TCP的流量控制提供重要信息。
順序問題,穩重不亂
丟包問題,承諾靠譜
連接維護,有始有終
流量控制,把握分寸
擁塞控制,知進知退
三次握手,請求->應答->應答之應答

如果B不樂意建議連接,則A會重試一陣后放棄,如果B樂意建立連接,則A會發送應答給A.
B的請求應答可能也會發送很多次,只要一次到達A,A就認為建立了連接。
第一次握手:
客戶端發送一個TCP的SYN標志位置1的包指明客戶打算連接的服務器的端口,以及初始序號ISN為X,保存在包頭的序列號字段里。

第二次握手:
服務器發回確認包(ACK)應答。即SYN標志位和ACK標志位均為1同時,將確認序號設置為客戶的ISN加1以.即X+1。

第三次握手:
客戶端再次發送確認包(ACK) SYN標志位為0,ACK標志位為1.并且把服務器發來ACK的序號字段+1,放在確定字段中發送給對方.并且在數據段寫ISN+1。

--------------------
傳輸數據的簡要過程如下:
1) 發送數據 :服務器向客戶端發送一個帶有數據的數據包,該數據包中的序列號和確認號與建立連接第三步的數據包中的序列號和確認號相同;
2) 確認收到 :客戶端收到該數據包,向服務器發送一個確認數據包,該數據包中,序列號是為上一個數據包中的確認號值,而確認號為服務器發送的上一個數據包中的序列號+所該數據包中所帶數據的大小。
數據分段中的序列號可以保證所有傳輸的數據按照正常的次序進行重組,而且通過確認保證數據傳輸的完整性。
------------------
那 TCP 在三次握手的時候,IP 層和 MAC 層在做什么呢?
TCP 發送每一個消息,都會帶著 IP 層和 MAC 層了。因為,TCP 每發送一個消息,IP 層和 MAC 層的所有機制都要運行一遍。而你只看到 TCP 三次握手了,其實,IP 層和 MAC 層為此也忙活好久了。
連接建立好后,就開始進行發包。
我們的數據經過七層協議的過程中就像包粽子一樣,每過一層就需要增加數據的大小。
MTU:最大傳輸單元數據域1500Bytes, 以太網的最大數據幀是1518Bytes。【以太網的幀頭148Bytes,一共18B:MAC目的地址6Bytes MAC源地址6Bytes,Type域2Bytes、幀尾校驗4Bytes;數據域只剩:1518-8 = 1500Bytes】
IP層最大傳輸長度:1500B – 20B =1480Bytes,超過此數值,都需要被IP層分片,在到達目的前會自己重組。
MSS:最大報文長度,為每次TCP數據包每次傳輸的最大數據的分段大小,由發送端通知接收端,發送大于MTU就會被分片。
TCP最大數據長度為1460Bytes,MTU- IP頭(20B)- TCP頭(20B) = 1460B 這也是最大的報文長度
UDP最大數據長度為1472B(UDP數據包 MTU - IP頭(20B) - UDP頭(8B) = 1472B)
而ip層分片會導致,如果其中的某一個分片丟失,因為tcp層不知道哪個ip數據片丟失,所以就需要重傳整個數據段,這樣就造成了很大空間和時間資源的浪費,為了解決這個問題,就有了tcp分組和MSS(最長報文大小)概念,利用tcp三次握手建立鏈接的過程,交互各自的MTU,然后用小的那個MTU-20-20 , 得到MSS,這樣就避免在ip層被分片。
備注:我們標注的都是最大長度,不包括各個協議中的可變長度的選項信息等。
既然出了網關,那就是在公網上傳輸數據,公網往往是不可靠的,因而需要很多的機制去保證傳輸的可靠性,這里需要恒心(重傳策略)還需要智慧(大量的算法)。
客戶端每發送一個包,服務端都要有回復,如果服務端超過一定的時間沒有回復,則客戶端會重新發送這個包,直到有回復。
舉例:領導交代下屬問題(領導交代下屬一些事情,需要準備一個本子,領導每交代下屬一件事情,雙方都要記錄一下, 當下屬做完一件事情,就回復你辦好了,你就在本子上將這個事情劃去。同時你的本子上的每件事情都有時限,如果超過了時限下屬沒有回復,你就要主動交代一下,上次的那件事情,你還沒有回復我,咋樣啦?既然很多事情一起做,就需要給每個事情編個號,防止弄錯了,大部分對于事情的處理都是按照順序來的,先來的先處理…)
TCP協議為了保證順序,每一個包都有一個ID,在建立連接的時候會協商起始ID是什么,然后按照ID一個一個的發。為了保證不丟包,對于發送的數據包都要進行應答,這個應答肯定不是一個一個來的,而是會應答某個之前的ID,這種模式就叫累計應答|累計確認。
為了記錄所有發送的包和接受的包,TCP也需要發送端和接收端分別都有緩存來保存這些記錄。發送端的緩存是按照包的ID一個個排列的,如下:
發送端數據結構:

第一部分:發送了已經確認了,就是交代下屬的,并且也做完了,應該劃掉的。
第二部分:發送了并且尚未確認的。這部分是交代下屬的,但是還沒有做完的,需要等到回復后,才能劃掉的。
第三部分:沒有發送,等待發送了,還沒有交代給下屬,但是馬上能交代的。
第四部分:沒有發送,暫時也不能發送的。暫時沒有交代給下屬的。
這里為什么要控制第三部分、第四部分呢?沒交代的一下子交代了不就得了?
這就是我們說的“流量控制,把握分寸”。
在TCP里,接收端會給發送報一個窗口的大小,就是AdvertisedWindow=(第二部分+第三部分)超過這個窗口接收端做不過來,就不能發送了。
接收端數據結構:

第一部分:接受并且確認過的,等著上層應用讀取的數據。就是領會交代給我的我做完的,并且我也發了ack的。
第二部分:還沒有接受,但是馬上能接受的,也就是我還能承受的最大工作量。
第三部分:沒有辦法接受的,實在做不完的,一接受就猝死的工作量。
所以發送端的AdvertisedWindows=(MaxRcvBuffer-A)。
順序與丟包問題
1、2、3沒有問題,雙方達成了一致。
4、5接收方說ACK了,但是發送發還沒有收到,有可能丟了,有可能還在路上。
6、7、8、9肯定都發了,但是8、9已經到了,但是6、7沒到,出現了亂序,緩存著但是沒有辦法ACK。
假如4的確認到了,5的ACK丟了,6、7的數據包丟了,該怎么辦?
超時重試
對每一個發送了但是沒有ACK的包都設置一個定時器,超過了一定的時間,就要重新嘗試。但是這個超時時間如何評估呢?這個時間不宜過短時間必須大于往返時間RTT(Round Trip Time,也就是一個數據包從發出去到回來的時間),否則會引起不必要的重傳。也不宜過長,這樣超時時間變長影響性能,訪問變慢。根據往返時間需要TCP通過采樣RTT的時間,然后進行加權求平均,算出一個值,而且這個值還要根據網絡的不斷的變化而不斷變化的,我們稱為自適應重傳算法。
如果過了一段時間,5、6、7都超時了,就會重新發送。接收方發現5原來接受過,就丟棄,6收到了,發送ACK,又發送7,不幸的是7又丟了,當7再次超時的時候,就需要重傳,TCP的策略是超時重傳加倍,兩次超時就說明網絡環境差,不宜頻繁反復發送。
超時重傳的問題是,超時周期可能相對較長,又沒有更快的方式呢?
有一個快速重傳機制,當接收方收到一個序號大于下一個所期望的報文段時,就檢測到了數據流中的一個間隔,于是發送3個的ACK,(例如發送如上丟失的7的ACK),客戶端接受后,就在定時器超時前,重傳丟失的報文段。
還有一個種方式叫SACK,需要在TCP的頭部加一個SACK的東西,可以將緩存的地圖發送給發送方。例如ACK6、SACK8、SACK9,發送方一下子就知道是7丟了.
流量控制問題
我們假設環境良好,窗口不變的情況下,窗口始終為4-12=9,4的確認來了之后,會右移一個,這個時候13號的包也就可以發送了

假設這個時候發送過猛,會將第三部分的10-13全部發送之后停止發送,使未發送的可發送部分為0.

當對于5的確認到達后,在客戶端相當于窗口再滑動一格,這個時候才有更多的包可以發送

如果接收方處理的實在太慢,導致緩存中沒有空間,可以通過確認信息修改窗口的大小甚至可以設置為0,則發送方將暫停發送。假設接收端的應用一直不讀取緩存中的數據,當數據包6確認后,窗口就可以縮小一個(9->8)。

如果接受端的應用一直不去讀取數據,則隨著確認的包越來越多,窗口越來越小,直到為0,則停止發送。

如果這樣情況的話,發送方會定時發送窗口的探測數據包,看看是否有機會調整窗口的大小,當接收方比較慢的時候,要防止低能窗口綜合征,被空出一個字節就趕快告訴發送方,然后馬上又填滿了,可以當窗口太小的時候,不更新窗口,直到達到一定大小或者緩沖區的一半為空的時候才更新窗口。
擁塞控制問題
擁塞控制問題,通過窗口的大小來控制,前面的滑動窗口rwnd是怕發送發把接收方的緩存塞滿,而擁塞控制窗口cwnd,是怕把網絡塞滿(造成丟包、超時重傳)。發送未確認的<=min{cwnd,rwnd},擁塞窗口和滑動串口共同控制發送的速度。
那發送方怎么判斷網絡是不是滿呢?這其實是個挺難的事情,因為對于 TCP 協議來講,他壓根不知道整個網絡路徑都會經歷什么,對他來講就是一個黑盒。TCP 的擁塞控制就是在不堵塞,不丟包的情況下,盡量發揮帶寬。
水管有粗細,網絡有帶寬,也即每秒鐘能夠發送多少數據;水管有長度,端到端有時延。在理想狀態下,水管里面水的量 = 水管粗細 x 水管長度。對于到網絡上,通道的容量 = 帶寬 × 往返延遲。
linux3.0以后,采取了Google的建議,把初始擁塞控制窗口調到了10。
如果我們設置發送窗口,使得發送但未確認的包為通道的容量,就能夠撐滿整個管道。

如圖所示,假設往返時間為 8s,去 4s,回 4s,每秒發送一個包,每個包 1024byte。已經過去了 8s,則 8 個包都發出去了,其中前 4 個包已經到達接收端,但是 ACK 還沒有返回,不能算發送成功。5-8 后四個包還在路上,還沒被接收。這個時候,整個管道正好撐滿,在發送端,已發送未確認的為 8 個包,正好等于帶寬,也即每秒發送 1 個包,乘以來回時間 8s。
如果我們在這個基礎上再調大窗口,使得單位時間內更多的包可以發送,會出現什么現象呢?
我們來想,原來發送一個包,從一端到達另一端,假設一共經過四個設備,每個設備處理一個包時間耗費 1s,所以到達另一端需要耗費 4s,如果發送的更加快速,則單位時間內,會有更多的包到達這些中間設備,這些設備還是只能每秒處理一個包的話,多出來的包就會被丟棄,這是我們不想看到的。
這個時候,我們可以想其他的辦法,例如這個四個設備本來每秒處理一個包,但是我們在這些設備上加緩存,處理不過來的在隊列里面排著,這樣包就不會丟失,但是缺點是會增加時延,這個緩存的包,4s 肯定到達不了接收端了,如果時延達到一定程度,就會超時重傳,也是我們不想看到的。
于是 TCP 的擁塞控制主要來避免兩種現象,包丟失和超時重傳。一旦出現了這些現象就說明,發送速度太快了,要慢一點。但是一開始我怎么知道速度多快呢,我怎么知道應該把窗口調整到多大呢?
如果我們通過漏斗往瓶子里灌水,我們就知道,不能一桶水一下子倒進去,肯定會濺出來,要一開始慢慢的倒,然后發現總能夠倒進去,就可以越倒越快。這叫作慢啟動。
一條 TCP 連接開始,cwnd 設置為一個報文段,一次只能發送一個;當收到這一個確認的時候,cwnd 加一,于是一次能夠發送兩個;當這兩個的確認到來的時候,每個確認 cwnd 加一,兩個確認 cwnd 加二,于是一次能夠發送四個;當這四個的確認到來的時候,每個確認 cwnd 加一,四個確認 cwnd 加四,于是一次能夠發送八個。可以看出這是指數性的增長。
漲到什么時候是個頭呢?有一個值 慢啟動閥值(sshresh) 為 65535 個字節,當超過這個值的時候,就要小心一點了,不能倒這么快了,可能快滿了,再慢下來。
每收到一個確認后,cwnd 增加 1/cwnd,我們接著上面的過程來,一次發送八個,當八個確認到來的時候,每個確認增加 1/8,八個確認一共 cwnd 增加 1,于是一次能夠發送九個,變成了線性增長。
但是線性增長還是增長,還是越來越多,直到有一天,水滿則溢,出現了擁塞,這時候一般就會一下子降低倒水的速度,等待溢出的水慢慢滲下去。
擁塞的一種表現形式是丟包,需要超時重傳,這個時候,將 sshresh 設為 (cwnd/2),將閥值減半,然后將 cwnd 設為 1,重新開始慢啟動。這真是一旦超時重傳,馬上回到解放前。但是這種方式太激進了,將一個高速的傳輸速度一下子停了下來,會造成網絡卡頓。
前面我們講過快速重傳算法。當接收端發現丟了一個中間包的時候,發送三次前一個包的 ACK,于是發送端就會快速的重傳,不必等待超時再重傳。TCP 認為這種情況不嚴重,因為大部分沒丟,只丟了一小部分,cwnd 減半為 cwnd/2,然后 sshthresh = cwnd,當三個包返回的時候,cwnd = sshthresh + 3,也就是沒有一夜回到解放前,而是還在比較高的值,呈線性增長。
就像前面說的一樣,正是這種知進退,使得時延很重要的情況下,反而降低了速度。但是如果你仔細想一下,

TCP 的擁塞控制主要來避免的兩個現象都是有問題的。
第一個問題是丟包并不代表著通道滿了,也可能是管子本來就漏水。例如公網上帶寬不滿也會丟包,這個時候就認為擁塞了,退縮了,其實是不對的。
第二個問題是 TCP 的擁塞控制要等到將中間設備都填充滿了,才發生丟包,從而降低速度,這時候已經晚了。其實 TCP 只要填滿管道就可以了,不應該接著填,直到連緩存也填滿。
為了優化這兩個問題,后來有了TCP BBR 擁塞算法。它企圖找到一個平衡點,就是通過不斷的加快發送速度,將管道填滿,但是不要填滿中間設備的緩存,因為這樣時延會增加,在這個平衡點可以很好的達到高帶寬和低時延的平衡。

四次揮手
A:B啊,我不想玩了…
B:那好吧,我知道了…
這個時候B能不能在ack的時候直接關閉呢?當然不可以了,很有可能是A發完了最后的數據就不玩了,但是B還沒有做完自己的事情,還是可以發送數據的,成為半封閉的狀態。
B:A啊,我也不玩了…
A:好的,拜拜…
A說完不玩了,超時后,假如沒有收到回復,A會重新發送不玩了,這回合結束后時候出現異常了….
假如:
1、A說完不玩了,直接跑路,B還沒有發起結束,如果A跑路,B發送結束,也沒有應答….
2、A說完不玩了,B直接跑路….
TCP設置了復雜的狀態機來解決,TCP協議要求A最后等待一段時間Time_wait

TIME-WAIT是2MSL(MSL報文最大生產時間,協議規定是2分鐘,實際應用設為30s,60s,120s)它是任何報文在網絡上存在的最長時間,超過這個時間,則報文被丟棄。TCP報文是基于IP協議的,而IP頭中有一個TTL(生存時間值)域,是IP數據包可以經過的最大路由數,每經過一個路由時就減1,當此值為0則數據報將被丟棄,同時發送ICMP報文通知源主機。假如超過2MSL后,B依然沒有收到FIN的ACK,則B肯定還會重新發送FIN,這個時候超時后,A就直接發送RST,B就知道A早就跑了….
怎么樣出網關?出網關后怎么樣路由下一跳?如何防止拓撲結構中的環路問題?
下次分享,我們再見
浙公網安備 33010602011771號