最近重讀的 Stevens 老先生的TCP/IP詳解,梳理了一下,打算把自己理解的寫出來。
TCP/IP是一種面向連線的、可靠的、基於位元組流的傳輸層通訊協議,它會保證資料不丟包、不亂序。TCP全名是Transmission Control Protocol,它是位於網路OSI模型中的第四層(Transport layer)。
TCP 首部
- Port
每個TCP資料段都包含源埠和目的埠號,用於尋找傳送端和接收端的應用程式。這兩個值加上IP首部中的源端IP和目的端IP地址唯有時候我們也會把它稱為socket四元組(源IP地址、目的IP地址、源埠、目的埠)
- Sequence number
序列號用來標識從TCP傳送端向TCO接收端傳送的資料位元組流,它標識在這個報文段中的第一個資料位元組。序號是32 bit的無符號數,序號到達232-1後又從0開始。TCP為應用層提供全雙工服務。這意味資料能在兩個方向上獨立地進行傳輸。因此,連線的每一端必須保持每個方向上的傳輸資料序號。 - Acknowledgment number
確認號,和序列號類似,不過它是用來確認已經收到的序號並下次想收到的序號。這兩個序號保證了TCP傳輸過程中不亂序、不丟包的問題。 - TCP Flag
NS:隱藏保護。
CWR:傳送主機設定擁塞視窗減少(CWR)標誌,以指示它收到了一個設定了ECE標誌的TCP段,並在擁塞控制機制中作出響應。
ECE:ECN-Echo具有雙重角色,具體取決於SYN標誌的值。它表明:
如果SYN標誌設定為(1),則TCP對等體具有ECN能力。
如果SYN標誌為清除(0),則在正常傳輸期間接收到IP報頭中具有擁塞經歷標誌設定(ECN = 11)的分組。這用作TCP傳送方的網路擁塞(或即將發生的擁塞)的指示。
URG:表示緊急指標欄位是重要的
ACK:表示確認欄位是重要的。客戶端傳送的初始SYN資料包之後的所有資料包都應設定此標誌。
PSH:推送功能。要求將緩衝的資料推送到接收應用程式。
RST:重置連線
SYN:同步序列號。只有從每一端傳送的第一個資料包應該設定此標誌。其他一些標誌和欄位會根據此標誌更改含義,有些僅在設定時有效,有些僅在明確時有效。
FIN:來自傳送方的最後一個資料包。
關於SYN和FIN可以參考我這篇文章TCP三次握手和四次揮手 - Window size
TCP的流量控制是由連線的每一端通過宣告視窗大小來提供。Window size 是一個16bit的欄位,所以視窗最大為65535。 - Checksum 它是有傳送端計算,然後接收端進行驗證。其目的是為了保證在傳輸過程中出現什麼差錯,如果校驗和驗證失敗,TCP直接丟棄這個資料段(校驗過程中會涉及到一個偽首部,偽首部的資料都是從IP資料包頭獲取的,其目的就是為了檢測TCP資料段是否已經正確到達,只是單純用來做校驗的)。
- Urgent pointer 緊急指標,它只在URG標誌設定為1的時候生效。
TCP 資料傳輸
TCP的建立連線之前寫過一篇文章,所以就不在這裡細贅了,我們直接聊TCP資料傳輸中如何保證資料的傳輸順序和丟包問題的,以及怎麼提高TCP傳輸的吞吐量。
- Acknowledgement of delay
通常TCP在收到資料的時候不會立刻傳送一個ACK確認,它會延遲傳送,可以和對方需要的資料一起傳送(資料捎帶ACK)或者是等待第二個資料來了直接回復第二個ACK,通常的實現採用的延遲是200ms(就是說它會等待200ms有沒有資料一起傳送) - Nagle
在資料傳輸過程中,通常會遇到一些小分組的傳輸(比如 41 bit的資料分組,除去TCP首部和IP首部真正傳輸的資料只有1 bit),像這種小分組多的話,在網路上傳輸就加大了造成網路擁塞的可能。為了提傳輸效率,所以提出了Nagle演算法。
這個演算法要求一個TCP連線最多隻能有一個未被確認的未完成的小分組,在該分組到達之前不能傳送其他的小分組。然後,TCP會收集這些小分組,並在確認到來時以一個分組的方式傳送出去,這樣就可以有效的減少了小分組。 在一些實時性要求比較高的場景下,採用了Nagle演算法會讓使用者感覺到時延,所以我們可以選擇關閉Nagle演算法,Socket API 可以用 TCP_NODELAY 選項來關閉,nginx上的 tcp_nodely也是採用的這個系統呼叫。 - Retransmission
TCP為了保證資料不丟失所採用的重傳策略。 TCP超時重傳比較嚴重,它表示已經超時了還沒有收到資料確認的回覆,所以他會進入到慢啟動,而快速重傳則不用。
TCP超時重傳:TCP傳送方首先會維護一個TCP的重傳定時器(有的也叫超時時間RTO),這個定時器是根據往返時間(RTT)進行計算,具體演算法的實現可以參考 RFC 6298,當RTO到了還沒有收到資料的確認,那麼TCP就認為資料已經丟失了。TCP會重傳資料,接著進入到擁塞控制裡的慢啟動(關於擁塞控制會在後面講)。
TCP快速重傳:它主要是收到了三個重複的ACK後(接受方如果收到的資料是亂序的。它會重發自己最近接收到的正確順序的ACK)進行重傳,因為收到重複的ACK代表資料已經傳送過去了,其中的一個資料可能因為其他原因(如資料傳輸中換了比較遠的路由,或者是資料乾脆直接丟了)造成資料沒有收到。所以這個情況不算太嚴重,它不會進入到慢啟動,它會進入到快速恢復。
TCP 在收到連續重複ACK後會重傳最後順序確認包的下一個,這樣原先已經正確傳輸的包可能會重複傳送,降低了TCP效能。為改善這種情況,發展出SACK(Selective Acknowledgement)技術,使用SACK選項可以告知發包方收到了哪些資料,發包方收到這些資訊後就會知道哪些資料丟失,然後立即重傳丟失的部分。
TCP 滑動視窗
- 滑動視窗
TCP在雙方資料傳輸的過程中,都會維護一個視窗,它代表了我還可以接受的資料的大小。如果接收方視窗大小為0,傳送方就會停止傳送。之所以叫滑動視窗(Sliding Window)是因為它是動態可變的,不是固定的(張開、合攏、收縮)。它保證了資料的可靠傳遞、它確保資料按順序傳遞、並且它強制傳送者之間的流量控制。
上圖中我們可以看到:
傳送端的LastByteAcked指向了接收端最後一次順序ACK的位置,LastByteSent指向了傳送出的資料,但是還沒有收到確認ACK。
接收端的NextByteExpected指向了已經收到的最後一個連續資料,LastByteAcked指向了接收到的最後一個資料,其中的空白代表還未收到的資料。
下面看一張滑動視窗的示意圖:
SND.UNA:已傳送但尚未確認的資料的第一個位元組的序列號。 這標誌著傳輸類別#2的第一個位元組; 所有先前的序列號都是指傳輸類別#1中的位元組。
SND.NXT:要傳送到另一個裝置(在這種情況下是伺服器)的下一個資料位元組的序列號。 這標誌著傳輸類別#3的第一個位元組。
SND.WND:傳送視窗的大小。 回想一下,視窗指定任何裝置在任何時候都可能具有“未完成”( 未確認 )的總位元組數。 因此,新增第一個未確認位元組( SND.UNA )和傳送視窗(SND.WND )的序列號標記傳送類別#4的第一個位元組。 SND.UNA:已經傳送但是尚未確認的 SND.NXT:將要傳送的 SND.WND:傳送視窗的大小 #1 表示已經確認過的資料,所以視窗右移,黑色代表視窗大小。
#2 表示已經傳送的,但是還沒有收到確認。
#3 表示還沒有傳送的,接受方可以接收的資料。
#4 表示不能傳送的資料,接收方不能接收的資料。
下面看一張TCP視窗滑動的示意圖:
- 糊塗視窗
我們看到了TCP通過讓接收方指明視窗來進行流量控制,這將有效的組織傳送方放鬆資料,直到視窗變為非0為止。但是其中會遇到一個問題,就是接收方傳送的的的視窗更新資料丟失,這樣會讓傳送方進入到無限等待狀態,因為他要等待視窗更新為非0。為了解決這個問題TCP採用了堅持定時器(persist timer)去探測視窗更新。 這樣又會導致一種被稱為“糊塗視窗綜合症SWS (Silly Window Syndrome)”的狀況。如果發生這種情況,則少量的資料將通過連線進行交換,而不是滿長度的報文段。
該現象可發生在兩端中的任何一段,接受方可以通告一個小的視窗(而不是一直等待有大的視窗才通告),傳送方也可以傳送少量的資料(而不是等待其他的資料以便傳送一個大的資料段)。可以在任何一端採取避免SWS的現象。
1.接收方不通告小視窗。通常的演算法是接收方不通告一個比當前視窗大的視窗(可以為0),除非視窗可以增加一個報文段大小(也就是將要接收的MSS)或者可以增加接收方快取空間的一半,不論實際有多少。
2.傳送方避免出現糊塗視窗綜合症的措施是隻有以下條件之一滿足時才傳送資料:(a)可以傳送一個滿長度的報文段;(b)可以傳送至少是接收方通告視窗大小一半的報文段;(c)可以傳送任何資料並且不希望接收ACK(也就是說,我們沒有還未被確認的資料)或者該連線上不能使用Nagle演算法。
TCP 擁塞控制
TCP不僅可以可以控制端到端的資料傳輸,還可以對網路上的傳輸進行監控。這使得TCP非常強大智慧,它會根據網路情況來調整自己的收發速度。網路順暢時就可以發的快,擁塞時就發的相對慢一些。擁塞控制演算法主要有四種:慢啟動,擁塞避免,快速重傳和快速恢復。
- 慢啟動和擁塞避免
慢啟動和擁塞避免演算法必須被TCP傳送端用來控制正在向網路輸送的資料量。為了 實現這些演算法,必須向TCP每連線狀態加入兩個參量。擁塞視窗(cwnd)是對傳送端收到確 認(ACK)之前能向網路傳送的最大資料量的一個傳送端限制,接收端通知視窗(rwnd)是對 未完成資料量的接收端限制。cwnd和rwnd的最小值決定了資料傳送。 另一個狀態參量,慢啟動閥值(ssthresh),被用來確定是用慢啟動還是用擁塞避免 演算法來控制資料傳送。 在不清楚環境的情況下向網路傳送資料,要求TCP緩慢地探測網路以確定可用流量,避免突然傳送大量資料而使網路擁塞。在開始慢啟動時cwnd為1,每收到一個用於確認新資料的ACK至多增加SMSS(SENDER MAXIMUM SEGMENT SIZE)位元組。 慢啟動演算法在cwnd<ssthresh時使用,擁塞避免演算法在cwnd>ssthresh時使用。當cwnd和ssthresh相等時,傳送端既可以使用慢啟動也可以使用擁塞避免。 當擁塞發生時,ssthresh被設定為當前視窗大小的一半(cwnd和接收方通告視窗大小的最小值,但最少為2個報文段)。如果是超時重傳,cwnd被設定為1個報文段(這就是慢啟動,其實慢啟動也不慢,它是指數性增長,只是它的起始比較低)當達到ssthresh時,進入擁塞避免演算法(擁塞避免是線性增長)。
在該圖中我們可以清楚的看到,ssthresh最初等於8 MSS 。 擁塞視窗在慢啟動期間以指數方式快速上升並在第三次傳輸時達到ssthresh。 然後,擁塞視窗線性地爬升,直到發生丟失(超時),就在傳送7之後。當發生丟失時,擁塞視窗是12 MSS 。 然後將ssthresh設定為6 MSS並且將cwnd設定為1,並且該過程繼續。
- 快速重傳和快速恢復
當接收端收到一個順序混亂的資料,它應該立刻回覆一個重複的ACK。這個ACK的目的是通知傳送端收到了一個順序紊亂的資料段,以及期望的序列號。傳送端收到這個重複的ACK可能有多種原因,可能丟失或者是網路對資料重新排序等。在收到三個重複ACK之後(包含第一次收到的一共四個同樣的ACK),TCP不等重傳定時器超時就重傳看起來已經丟失(可能資料繞路並沒有丟失)的資料段。因為這個在網路上並沒有超時重傳那麼惡劣,所以不會進入慢啟動,而進入快速恢復。快速恢復首先會把ssthresh減半(一般還會四捨五入到資料段的倍數),然後cwnd=ssthresh+收到重複ACK報文段累計的大小。 這個圖上我們可以看出,在三次重複ACK後cwnd並沒有進入到慢啟動,而是進入到了快速重傳。在第二段超時重傳時,進入到了慢啟動cwnd置1。
總結
本來打算以最少的文字去解釋TCP,但是並不是很成功。TCP發展至今已經有幾十年了,其中的技術點都可以出好幾本書了。你可以把它當個索引,快速瀏覽一遍。下面我列一下在寫這篇文章時參考的文件,都很不錯,值得一讀。
TCP Congestion Control
Transmission Control Protocol
TCP Sliding Window
TCP/IP Guide
rfc 5681