TCP/IP實現(十二) TCP 傳輸控制協議
一.TCP首部
TCP首部結構如下圖所示:
TCP協議中用序號來標識每一個位元組,連線的每一端都維護著自己的序號,起始序號是主機選擇的,稱為ISN(隨時間變化,每個連線都具有不同的ISN)。除每個資料位元組消耗序號外,SYN和FIN也會消耗序號。確認序號用於表示傳送確認的一端所期望收到的下一個序號,即以成功接收的最後一個位元組的序號加1,要注意TCP從不會選擇確認(即不會間隔確認),比如,先成功收到1~1024位元組,可下一個報文段為2048 ~ 3072位元組,此時接收端並不會回覆確認序號為3073的ack,而是回覆確認序號為1025的ack。也不會進行否認
二.TCP連線的建立
1.正常情況下的連線建立(三次握手)
三次握手的過程是:客戶端先發送SYN,伺服器端收到後回覆SYN + ACK(即一個報文段中的SYN和ACK標誌都置位,且序號和確認序號都有效),當客戶端收到來自伺服器的SYN+ACK後進行最後確認,回覆一個ACK,當伺服器接收到後則三次握手完成。下面給出一個示意圖以及對應的tcpdunm程式輸出(之後將講解tcpdump命令的使用)。
2.MSS最大報文長度
MSS表示TCP可以傳往另一端的最大資料塊長度,這個值是根據外出介面上的MTU(最大傳輸單元)減去固定的IP首部和TCP首部長度得出的。當建立一個連線時,都要在SYN報文中(也只能在SYN報文中)通告自己的MSS值,一般雙方都會選擇較小的MSS值。
3.同時開啟(每端都進行兩次握手)
當每一方都使用對方所熟知的埠作為本地埠,且彼此同時執行主動開啟,此時便會發生同時開啟的情況。對於同時開啟僅建立一條連線,而非兩條連線。其過程如下圖所示:
【注】:TCP連線是全雙工的,當連線建立後,其實兩端(兩個socket)是對等的,也就是說對於一個TCP連線的兩端而言,事實上是沒有伺服器與客戶端之別的,我們常說的服務端客戶端其實是根據應用的功能而言的。理解了這一點也就能理解其實那一邊發起主動連線都是可行的,也是符合TCP邏輯的(只是不一定符合應用的邏輯)。
4.連線建立超時
當傳送第一個SYN分節後若在6秒(不一定準確)後還未收到對端的ACK回覆,則會發送第二個SYN分節,若第二個分節傳送後的24秒內還未收到回覆,則繼續傳送第三個SYN,之後一直以24秒迴圈。當距第一個SYN分組傳送後的75秒內仍未收到ACK回覆,則放棄連線。
【注】:當使用非阻塞套接字時也應該自己實現相應的重連策略,muduo網路庫中便採用了這種連線策略
三.TCP連線的終止
1.連線的正常終止(四次揮手)
TCP連線是全雙工的,因此每一方的杜凱因應該可以獨立的進行。TCP也正是這樣做的,當一方想要斷開時,會進行兩次揮手來斷開己方至對方的連線,即己方此時無法向對方傳送資料,但此時仍可接收(使用shutdown函式關閉寫便是通過兩次回收這個原理實現的,在說明完四次揮手後接著討論shutdown函式)。其實我們要是明白了四次揮手是由兩次揮手組成的便了解了四次揮手的含義,四次揮手過程的示意圖如下所示:
任意一端都可以先進行主動斷開,所以對於上圖無需在意哪一端是客戶端,哪一端是服務端。
2.shutdown系統呼叫(半關閉)
我們都知道shutdown可以選擇關閉寫於關閉讀,且在關閉寫時會等待將套接字快取中的資料全部發送完畢之後,再發送終止序列FIN。進行上一小節所說的兩次揮手,斷開己方到對方的通道,即不能向對方傳送資料。
那麼關閉讀是怎麼做到的呢?難道是發出某種訊息讓對面進行主動關閉?顯然這麼做是不好的。TCP採用了下面的做法,當我們呼叫shutdown關閉讀時,會轉而呼叫一個名為sorflush的函式,該函式會在socket中的狀態欄位上加上SS_CANTRCVMORE位(so->so_state |= SS_CANTRCVMORE),該標誌位表示插口不能再從對方接收資料(即TCP協議層不會再將下層傳入的資料放入該socket的接收快取了)。隨後清空套接字接收快取中的所有現有資料。至此關閉讀成功。
此外還要注意的一點是shutdown可以不管引用計數就啟動TCP的正常終止序列。而close僅當該套接字對應的套接字描述符的引用計數變為0(UNP上寫的是“把描述符的引用計數減一”,我的理解是在檔案物件結構中記錄的引用計數,該引用計數記錄的是引用該檔案物件的描述符的數量,檔案物件與檔案描述符的關係可以參見博文《TCP/IP實現(一) 系統檔案結構及mbuf》)時才會關閉該套接字。
3.同時關閉(同時進行的兩次揮手)
同時關閉依然各端各自進行兩次揮手,不過不是先後進行,而是同時進行。其步驟如下圖所示,要注意其狀態變化不同於四次揮手。
4.異常終止
1)出現錯誤時的異常終止
TCP首部中的RST用於進行復位操作,在之後會解釋復位。無論何時一個報文段發往由四元組指明的連接出現錯誤,TCP都會發出一個RST,比如在半連線通達上傳送資料,埠不可達錯誤等。對於埠不可達,UDP會產生一個ICMP埠不可達訊息,TCP會使用復位。測試如下:一個TCP客戶端向本地伺服器請求連線,但伺服器位執行,tcpdump結果如下:
可以看到TCP回覆了一個RST分節。可是有這樣一個問題:假設該分節沒到主機,而是到某個中間路由時便找不到網路了,此時會回覆ICMP網路不可達網路不可達報文,那麼此時當主機收到該報文,並由ICMP協議處理後分用到相關協議(參《TCP/IP實現(七) ICMP協議》),此時TCP會如何處理收到的錯誤訊息呢?是返回錯誤碼碼?與收到RST分節的處理有何不同呢?這個問題暫時還不太清楚,在看過原始碼後或許可以解答。
當收到RST分組後不會回覆任何響應,收到RST的一方將終止連線,並通知應用層。
2)主動異常終止
除了前面提到的傳送FIN進行正常釋放連線外,還可以主動傳送RST進行異常釋放。異常釋放可以不必等待套接字傳送快取中的資料全部發送完畢,而是丟棄任何待發送的資料並立即傳送RST報文段(使用SO_LINGER選項)。RST的接收端可以區分除對方執行的是正常關閉還是異常關閉。
5.close系統呼叫(SO_LINGER延遲關閉)
當呼叫close函式時,如前所說會檢視套接字描述符所對應檔案物件中的引用計數,當該檔案描述符是引用該檔案物件的最後一個描述符時才會執行相應的關閉操作。預設情況下,close函式會將該套接字標記為關閉(核心中會斷開socket與協議的關聯,並將該socket標記為與任何描述符無關so->so_state |= SS_NOFDREF,隨後釋放sockt結構),並立即返回,之後程序便不能通過該套接字描述符便不可使用。
但是我們可以通過setsockopt函式設定SO_LINGER選項來改變這種預設行為。SO_LINGER選項的引數為一個結構體:
struct linger{
int l_onoff; // 該位表示是否關閉本選項,為0則關閉,並忽略l_linger的值
int l_linger; // 該選項為延時時間,只有在l_onoff為1時有效
};
該情況分為以下三種:
1.關閉該選項,即l_onoff為0,l_linger的值是被忽略的。此時close函式進行的是預設情況下的關閉,即close立即返回(此時關閉連線並不一定已經完成)、
2.開啟該選項(即l_onoff為1)
1)l_linger值為0
此時close函式將丟棄套接字傳送快取中的所有資料,併發送RST報文。
2)l_linger值為非0
當呼叫close時核心可能將會延時一段時間後再退出。即當套接字傳送快取中仍有資料,則 將程序投入睡眠,直到資料傳送完畢並被確認或者延時時間到才返回(但若套接字為非阻塞則不會投入睡眠,並立即返回EWOULDBLOCK(EAGAIN))。使用SO_LINGER選項時應該檢查close的返回值,因為當資料未傳送完畢而延時時間到才返回時會返回EWOULDBLOCK(EAGAIN),並且丟棄套接字傳送快取中的全部資料,此時應該再次。
【注】:對端回覆對ACK只能說明對端以將該資料放入了套接字接收快取,但不能保證應用層進行了讀取,若要確認需要應用層確認機制。
四.互動式資料與Nagle演算法
1.互動式資料與延時確認
互動式資料指的是那些資料長度較小的資料報(不能將一個TCP報文填滿)。而對這些每一份很小的資料報,都需要兩個報文來完成(一個數據報文及一個確認報文)。因此TCP採用延時確認的方式來進行ACK的回覆,即當資料收到時不立即回覆ACK,而是等待一定的時間,以便可以與需要延該方向傳送的資料一同傳送,或是有更新的資料到來,從而傳送更新確認序號的ACK。TCP採用最大200ms的時延等待。
2.Nagle演算法
當網路中出現大量小分組時會降低網路利用率,造成網路擁塞。對此可以開啟Nagle演算法,該演算法要求:在一個TCP連線上最多隻能有一個未被確認的未完成的小分組,在該分組的確認到達前不能傳送其它小分組。這樣在等待確認的這段時間便可以收集更多的小分組進行傳送。單對於那些要求低時延的系統我們應該關閉Nagle演算法(預設是開啟的)。
五.滑動視窗(通告視窗)
每端都會在傳送至對方的資料報中包含本端當前的視窗大小。在建立連線時,先會將己方的視窗大小告知對方,這個大小的預設值對不同系統而言會有所不同,如:4096,2048等。滑動視窗的大小在資料傳送過程中會不斷變化,但不會超過最大大小(即對端的預設大小或對端使用插口API設定的大小)。滑動視窗大小在資料的互動過程中變化過程如下所述。
當傳送端收到對端對一段資料的ACK確認報文中後,說明對端已成功接收了滑動視窗前一部分的資料,此時便可以將滑動視窗的左邊沿向右移動。並且從收到的ACK確認報文中可以知道對端此時的接收視窗大小,從而可以進一步確認出滑動視窗的右邊沿(左邊沿+被通告的接收視窗大小)。
【注】:綜上可知通告視窗是接收方進行的流量控制
六.超時與重傳機制
1.採用往返測量與指數退避的重傳時間(RTO)
當一個數據報(不是SYN,連線超時已在前面的小節進行了說明)在傳送之後的一段時間內若仍未收到對端的確認,則認為該資料報未被對端正確接收,此時會重新發送一個數據報(資料大小不一定相同),而該段等待確認的時間就被稱為超時時間。超時重傳時間RTO的初始值是由TCP跟蹤資料報的往返時間RTT,再帶入公式計算出來的,由於路由器和網路流量均會變化,因此該時間值也會不斷變化。
若進行重傳後還未收到則將重傳時間進行指數增加,即每次在前一次超時重傳時間的基礎上乘2,這個過程便稱為指數退避,噹噹超時時間增加到一定限度後將不再增加,而是一直保持這個值,比如64秒。當一定時間之後TCP將會放棄重傳,並向對端傳送一個RST報文,這個時間一般為9分鐘。
【注】:大多TCP實現在任意時刻對每個連線只啟動一個定時器計算RTT,即在傳送一個報文段時,如果該連線的定時器已經被使用,則不測量該報文段的RTT
七.擁塞視窗及是相關演算法
1.慢啟動
慢啟動演算法用於在連線成功後快速探測網路容量,初始時將擁塞視窗大小置位1,從後每收到一個ACK確認報文就將擁塞視窗加1,即以指數方式增長:第一次傳送1個報文,然後2個,4,8......。直到超過慢啟動門限值ssthresh後啟動後面所說的擁塞避免演算法。
2.擁塞避免演算法
當擁塞視窗cwnd大小超過慢啟動門限後就將其增長速度用擁塞演算法中的公式進行計算,其增長速度由指數增長將為線性增加。此外擁塞演算法還會在鏈路中出現擁塞狀態時調整cwnd視窗大小,和慢啟動門限。其分為以下兩種情況:
1)收到重複ACK報文(3個以上)
此時認為網路中可能發生了資料報的丟失,但網路擁塞並未十分嚴重。因此將慢啟動門限ssthresh設定為當前擁塞視窗的一半,擁塞視窗設定為原先大小的一般並加上3個報文大小。接著重傳丟失的報文段(因為連著收到3次以上的重複確認報文,確認序號之後的那些已發資料可能已經丟失,也有可能是由於報文段亂序到達導致的,但TCP進行了相應的處理,即延時回覆ACK,所有當收到3個以上確認報文時很可能是報文發生了丟失)。當收到重傳報文的確認時將設定cwnd為ssthresh,重新進行擁塞避免演算法的增長。以上步驟也可稱為快速重傳與快速回復機制,當發生3個以上的重複報文時便認為丟失,不必等超時重傳(快速重傳)。因為並未發生超時,因此網路狀況或許不太糟糕,便不進行慢啟動演算法,而是直接進行擁塞避免演算法(快速恢復)。
2)超時引發的擁塞
此時發生超時重傳,網路中擁塞狀況可能比較嚴重,因此,將sstreach置為當前視窗(擁塞視窗與通告視窗的較小者)的一半(最小為2個報文大小,以MSS為單位),將cwnd置為一個報文段。之後進行慢啟動,到達sstreach後開始擁塞避免。
【注】:擁塞視窗及相關演算法用於在傳送端進行流量控制。
八.堅持定時器
1.定期查詢
當對端通搞視窗變為0時將停止向其傳送資料,當對端的程序將資料從緩衝佇列讀走後,此時對端將傳送新的ack報文來通知本端通告視窗的大小。可是若該ack丟失了呢?那麼通訊雙方豈不是陷入了死鎖。對此,TCP中設定了一個堅定定時器,用於定期向對端傳送一個位元組的資料進行查詢,以便髮絲按視窗是否增大。
2.糊塗視窗綜合症
對端通告視窗大小時可能很小,導致傳送端傳送一個很小的資料,如此往復下去。兩端可以採取了一些措施來避免,比如:接收方對通告視窗的大小做一些限制條件;傳送方對傳送資料的大小也可以採用一些限制條件(不知道TCP中是否如此做了)。
九.TCP保活定時器
當兩端不進行任何資料交換時,若中間的網路出現了故障(如路由損壞,網線斷開等),則TCP的兩端不會知道,且兩端將一直處於連線狀態,對此可設定TCP保活定時器(預設不啟動)。當連線兩端在2個小時之內都沒有任何動作,則會向對端傳送一個探查報文,若這2個小時內兩端有進行通訊則保活定時器會重新復位。若在發出探查報文後的75秒內未收到任何響應,則會繼續傳送,總共傳送10條。若都未響應則終止連線。此外,保活定時器的間隔時間是可以改變的,可其是系統級的(windows在登錄檔中修改),因此會影響到所有的連線。保活探查報文是恢復一個不正確序號的報文(即是對端希望收到的下一個位元組序號減1),這樣會導致對端迴應一個正確的ack報文(確認序號為下一個希望收到的位元組序號)。