TCP服務的特點
傳輸層協議主要有兩個: TCP協議和UDP協議。TCP協議相對於UDP協議的特點是:面向連接、字節流和可靠傳輸。
使用TCP協議通信的雙方必須先建立連接,然后才能開始數據的讀寫。雙方都必須為該連接分配必要的內核資源,以管理連接的狀態和連接上數據的傳輸。TCP連接是全雙工的,即雙方的數據讀寫可以通過一個連接進行。完成數據交換之后,通信雙方都必須斷開連接以釋放系統資源。
TCP協議的這種連接是一對一的,所以基於廣播和多播(目標是多個主機地址)的應用程序不能使用TCP服務。而無連接協議UDP則非常適合於廣播和多播。
字節流服務和數據報服務的這種區別對應到實際編程中,則體現為通信雙方是否必須執行相同次數的讀、寫操作(當然,這只是表現形式)。當發送端應用程序連續執行多次寫操作時,TCP模塊先將這些數據放入TCP發送緩沖區中。當TCP模塊真正開始發送數據時,發送緩沖區中這些等待發送的數據可能被封裝成一個或多個TCP報文段發出。因此,TCP模塊發送出的TCP報文段的個數和應用程序執行的寫操作次數之間沒有固定的數量關系。
當接收端收到一個或多個TCP報文段后,TCP模塊將它們攜帶的應用程序數據按照TCP報文段的序號(見后文)依次放人TCP接收緩沖區中,並通知應用程序讀取數據。接收端應用程序可以一次性將TCP接收緩沖區中的數據全部讀出,也可以分多次讀取,這取決於用戶指定的應用程序讀緩沖區的大小。因此,應用程序執行的讀操作次數和TCP模塊接收到的TCP報文段個數之間也沒有固定的數量關系。
綜上所述,發送端執行的寫操作次數和接收端執行的讀操作次數之間沒有任何數量關系,這就是字節流的概念:應用程序對數據的發送和接收是沒有邊界限制的。UDP則不然。發送端應用程序每執行一次寫操作,UDP模塊就將其封裝成-一個 UDP數據報並發送之。接收端必須及時針對每一個UDP數據報執行讀操作(通過recvfrom系統調用),否則就會丟包(這經常發生在較慢的服務器,上)。並且,如果用戶沒有指定足夠的應用程序緩沖區來讀取UDP數據,則UDP數據將被截斷。
TCP傳輸是可靠的。首先,TCP 協議采用發送應答機制,即發送端發送的每個TCP報文段都必須得到接收方的應答,才認為這個TCP報文段傳輸成功。其次,TCP協議采用超時重傳機制,發送端在發送出一個TCP報文段之后啟動定時器,如果在定時時間內未收到應答,它將重發該報文段。最后,因為TCP報文段最終是以IP數據報發送的,而IP數據報到達接收端可能亂序、重復,所以TCP協議還會對接收到的TCP報文段重排、整理,再交付給應用層。.UDP協議則和IP協議一樣,提供不可靠服務。它們都需要上層協議來處理數據確認和超時重傳。
TCP頭部結構
TCP固定頭部結構
-
16位端口號(port number):告知主機該報文段是來自哪里(源端口)以及傳給哪個上層協議或應用程序(目的端口)的。進行TCP通信時,客戶端通常使用系統自動選擇的臨時端口號,而服務器則使用知名服務端口號。1.3 節中提到過,所有知名服務使用的端口號都定義在/etc/services文件中。
-
32位序號( sequence number):一次TCP通信(從TCP連接建立到斷開)過程中某一個傳輸方向上的字節流的每個字節的編號。假設主機A和主機B進行TCP通信,A發送給B的第一個TCP報文段中,序號值被系統初始化為某個隨機值ISN ( Initial Sequence Number,初始序號值)。那么在該傳輸方向上(從A到B),后續的TCP報文段中序號值將被系統設置成ISN加上該報文段所攜帶數據的第一個字節在整個字節流中的偏移。例如,某個TCP報文段傳送的數據是字節流中的第1025~2048字節,那么該報文段的序號值就是ISN+1025.另外一個傳輸方向(從B到A)的TCP報文段的序號值也具有相同的含義。
-
32位確認號(acknowledgement number):用作對另一方發送來的TCP報文段的響應。其值是收到的TCP報文段的序號值加1.假設主機A和主機B進行TCP通信,那么A發送出的TCP報文段不僅攜帶自己的序號,而且包含對B發送來的TCP報文段的確認號。反之,B發送出的TCP報文段也同時攜帶自己的序號和對A發送來的報文段的確認號。
-
4位頭部長度(header length):標識該TCP頭部有多少個32bit字(4字節)。因為4位最大能表示15,所以TCP頭部最長是60字節。
-
6位標志位包含如下幾項:
- URG標志,表示緊急指針(urgent pointer)是否有效。
- ACK標志,表示確認號是否有效。我們稱攜帶ACK標志的TCP報文段為確認報文段。
- PSH標志,提示接收端應用程序應該立即從TCP接收緩沖區中讀走數據,為接收后
續數據騰出空間(如果應用程序不將接收到的數據讀走,它們就會--直停留在TCP接收緩沖區中)。 - RST標志,表示要求對方重新建立連接。我們稱攜帶RST標志的TCP報文段為復位報文段。
- SYN標志,表示請求建立-一個連接。我們稱攜帶SYN標志的TCP報文段為同步報文段。
- FIN標志,表示通知對方本端要關閉連接了。我們稱攜帶FIN標志的TCP報文段為結束報文段。
-
16位窗口大小( window size):是TCP流量控制的-一個手段。這里說的窗口,指的是接收通告窗口(Receiver Window, RWND)。它告訴對方本端的TCP接收緩沖區還能容納多少字節的數據,這樣對方就可以控制發送數據的速度。
-
16位校驗和(TCP checksum):由發送端填充,接收端對TCP報文段執行CRC算法以檢驗TCP報文段在傳輸過程中是否損壞。注意,這個校驗不僅包括TCP頭部,也包括數據部分。這也是TCP可靠傳輸的-一個重要保障。
-
16位緊急指針(urgent pointer):是-一個正的偏移量。它和序號字段的值相加表示最后一個緊急數據的下一-字節的序號。因此,確切地說,這個字段是緊急指針相對當前序號的偏移,不妨稱之為緊急偏移。TCP 的緊急指針是發送端向接收端發送緊急數據的方法。
TCP頭部選項
TCP頭部的最后-一個選項字段(options) 是可變長的可選信息。這部分最多包含40字節,因為TCP頭部最長是60字節(其中還包含前面討論的20字節的固定部分)。典型的TCP頭部選項結構如圖所示。
選項的第一個字段kind說明選項的類型。有的TCP選項沒有后面兩個字段,僅包含1字節的kind字段。第二個字段length ( 如果有的話)指定該選項的總長度,該長度包括kind字段和length字段占據的2字節。第三個字段info (如果有的話)是選項的具體信息。常見的TCP選項有7種,如圖所示。
- kind=0是選項表結束選項。
- kind=1是空操作(nop)選項,沒有特殊含義,一般用於將TCP選項的總長度填充為4字節的整數倍。
- kind =2是最大報文段長度選項。TCP連接初始化時,通信雙方使用該選項來協商最大報文段長度(Max Segment Size, MSS)。 TCP模塊通常將MSS設置為(MTU-40) 字節(減掉的這40字節包括20字節的TCP頭部和20字節的IP頭部)。這樣攜帶TCP報文段的IP數據報的長度就不會超過MTU (假設TCP頭部和IP頭部都不包含選項字段,並且這也是一般情況),從而避免本機發生IP分片。對以太網而言,MSS值是1460 (1500 -40)字節。
- kind=3是窗口擴大因子選項。TCP連接初始化時,通信雙方使用該選項來協商接收通告窗口的擴大因子。在TCP的頭部中,接收通告窗口大小是用16位表示的,故最大為65535字節,但實際上TCP模塊允許的接收通告窗口大小遠不止這個數(為了提高TCP通信的吞吐量)。窗口擴大因子解決了這個問題。假設TCP頭部中的接收通告窗口大小是N,窗口擴大因子(移位數)是M,那么TCP報文段的實際接收通告窗口大小是N乘2^M,或者說N左移M位。注意,M的取值范圍是0~14。我們可以通過修改/proc/sys/net/ipv4/tcp_window_scaling內核變量來啟用或關閉窗口擴大因子選項。
和MSS選項-樣,窗口擴大因子選項只能出現在同步報文段中,否則將被忽略。但同步報文段本身不執行窗口擴大操作,即同步報文段頭部的接收通告窗口大小就是該TCP報文段的實際接收通告窗口大小。當連接建立好之后,每個數據傳輸方向的窗口擴大因子就固定不變了。
- kind=4是選擇性確認( Selective Acknowledgment, SACK) 選項。TCP通信時,如果某個TCP報文段丟失,則TCP模塊會重傳最后被確認的TCP報文段后續的所有報文段,這樣原先已經正確傳輸的TCP報文段也可能重復發送,從而降低了TCP性能。SACK技術正是為改善這種情況而產生的,它使TCP模塊只重新發送丟失的TCP報文段,不用發送所有未被確認的TCP報文段。選擇性確認選項用在連接初始化時,表示是否支持SACK技術。我們可以通過修改/proc/sys/net/ipv4/tcp_ sack內核變量來啟用或關閉選擇性確認選項。
- kind=5是SACK實際工作的選項。該選項的參數告訴發送方本端已經收到並緩存的不連續的數據塊,從而讓發送端可以據此檢查並重發丟失的數據塊。每個塊邊沿(edge of block)參數包含一個4字節的序號。其中塊左邊沿表示不連續塊的第一個數據的序號, 而塊右邊沿則表示不連續塊的最后一個數據的序號的下一個序號。這樣一對參數(塊左邊沿和塊右邊沿)之間的數據是沒有收到的。因為一個塊信息占用8字節,所以TCP頭部選項中實際上最多可以包含4個這樣的不連續數據塊(考慮選項類型和長度占用的2字節)。kind=8是時間截選項。該選項提供了較為准確的計算通信雙方之間的回路時間( Round Trip Time, RTT)的方法,從而為TCP流量控制提供重要信息。我們可以通過修改/proc/sys/net/ipv4/tcp_ timestamps 內核變量來啟用或關閉時間戳選項。
TCP連接的建立和關閉
第1個TCP報文段包含SYN標志,因此它是一個同步報文段,即ernest-laptop (客戶端)向Kongming20 (服務器)發起連接請求。同時,該同步報文段包含一個ISN值535734930的字號。
第2個TCP報文段也是同步報文段,表示Kongming20同意與ernest-laptop建立連接。同時它發送自己的ISN值為2159701207的序號,並對第1個同步報文段進行確認。確認值是535734931,即第1個同步報文段的序號值加1,序號值是用來標識TCP數據流中的每一字節的。但同步報文段比較特殊,即使它並沒有攜帶任何應用程序數據,它也要占用一個序號值。
第3個TCP報文段是ernest- laptop對第2個同步報文段的確認。至此,TCP連接就建立起來了。建立TCP連接的這3個步驟被稱為TCP三次握手。
后面4個TCP報文段是關閉連接的過程。第4個TCP報文段包含FIN標志,因此它是-個結束報文段,即ermestlaptop要求關閉連接。結束報文段和同步報文段一樣, 也要占用一個序號值。Kongming20 用TCP報文段5來確認該結束報文段。緊接着Kongming20發送自己的結束報文段6, ernest-laptop 則用TCP報文段7給予確認。實際上,僅用於確認目的的確認報文段5是可以省略的,因為結束報文段6也攜帶了該確認信息。確認報文段5是否出現在連接斷開的過程中,取決於TCP的延遲確認特性。
在連接的關閉過程中,因為ermest-laptop先發送結束報文段,故稱ernest-laptop執行主動關閉,而稱Kongming20執行被動關閉。
一般而言,TCP連接是由客戶端發起,並通過三次握手建立(特殊情況是所謂同時打開的。TCP連接的關閉過程相對復雜一些。可能是客戶端執行主動關閉,比如前面的例子;也可能是服務器執行主動關閉,比如服務器程序被中斷而強制關閉連接;還可能是同時關閉(和同時打開一樣,非常少見)。
半關閉狀態
TCP連接是全雙工的,所以它允許兩個方向的數據傳輸被獨立關閉。換言之,通信的一端可以發送結束報文段給對方,告訴它本端已經完成了數據的發送,但允許繼續接收來自對方的數據,直到對方也發送結束報文段以關閉連接。TCP連接的這種狀態稱為半關閉( half close)狀態,如圖所示。
請注意,在圖中,服務器和客戶端應用程序判斷對方是否已經關閉連接的方法是:read系統調用返回0 (收到結束報文段)。當然,Linux 還提供其他檢測連接是否被對方關閉的方法,socket網絡編程接口通過shutdown函數提供了對半關閉的支持。
連接超時
如果客戶端訪問一個距離很遠的服務器,或者由於網絡繁忙,導致服務器對於客戶端發送的同步報文段沒有應答。TCP模塊會進行一共5次重連操作,這是由/proc/sys/net/ipv4/tcp. syn, retries 內核變量所定義的。每次重連的超時時間都增加一-倍。在5次重連均失敗的情況下,TCP模塊放棄連接並通知應用程序。
TCP狀態轉移
服務端的狀態
服務器通過listen系統調用進人LISTEN狀態,被動等待客戶蟎連接,因此執行的是所謂的被動打開。服務器- -旦監聽到某個連接請求(收到同步報文段),就將該連接放人內核等待隊列中,並向客戶端發送帶SYN標志的確認報文段。此時該連接處於SYN_RCVD狀態。如果服務器成功地接收到客戶端發送回的確認報文段,則該連接轉移到ESTABLISHED狀態。ESTABLISHED狀態是連接雙方能夠進行雙向數據傳輸的
狀態。
當客戶端主動關閉連接時(通過close或shutdown系統調用向服務器發送結束報文段),服務器通過返回確認報文段使連接進人CLOSE_ WAIT狀態。這個狀態的含義很明確:等待服務器應用程序關閉連接。通常,服務器檢測到客戶端關閉連接后,也會立即給客戶端發送一個結束報文段來關閉連接。這將使連接轉移到LAST_ _ACK狀態,以等待客戶端對結束報文段的最后一次確認。一旦確認完成,連接就徹底關閉了。
客戶端狀態
客戶端通過connct系統調用主動與服務器建立連接。connect 系統調用首先給服務器發送一個同步報文段,使連接轉移到SYN, _SENT狀態。此后,connect 系統調用可能因為如下兩個原因失敗返回:
- 如果connect連接的目標端口不存在(未被任何進程監聽),或者該端口仍被處於TIME WAIT狀態的連接所占用(見后文),則服務器將給客戶端發送-一個復 位報文段,connect 調用失敗。
- 如果目標端口存在,但connect在超時時間內未收到服務器的確認報文段,則connect調用失敗。
connect調用失敗將使連接立即返回到初始的CLOSED狀態。如果客戶端成功收到服務器的同步報文段和確認,則connect調用成功返回,連接轉移至ESTABLISHED狀態。當客戶端執行主動關閉時,它將服務器發送一個結束報文段,同時連接進入FIN_WAIT_1狀態。若此時客戶端收到服務器專門用於確認目的的確認報文段,則連接轉移至FIN_ WAIT_ 2狀態。當客戶端處於FIN_ WAIT_ 2狀態時,服務器處於CLOSEWAIT狀態,這一對狀態是可能發生半關閉的狀態。此時如果服務器也關閉連接(發送結束報文段),則客戶端將給予確認並進人TIME _WAIT狀態。
圖還給出了客戶端從FIN_ WAIT_ 1狀態直接進入TME WAIT狀態的一條線路(不經過FINWAIT2狀態),前提是處於FIN__WAIT_1狀態的服務器直接收到帶確認信息的結束報文段(而不是先收到確認報文段,再收到結束報文段)。處於FIN_WAIT2狀態的客戶端需要等待服務器發送結束報文段,才能轉移至TIME_ WAIT狀態,否則它將一直停 留在這個狀態。如果不是為了在半關閉狀態下繼續接收數據,連接長時間地停留在FIN_ WAIT 2狀態並無益處。連接停留在FIN_ WAIT_ 2狀態的情況可能發生在:客戶端執行半關閉后,未等服務器關閉連接就強行退出了。此時客戶端連接由內核來接管,可稱之為孤兒連接(和孤兒進程類似)。Linux 為了防止孤兒連接長時間存留在內核中,定義了兩個內核變量: /proc/sys/netipv4/tcp_ max_orphans 和/proc/sys/net/ipv4/tcp_ fin_ timeout。 前者指定內核能接管的孤兒連接數目,后者指定孤兒連接在內核中生存的時間。
TIME_WAIT狀態
從圖來看,客戶端連接在收到服務器的結束報文段(TCP報文段6)之后,並沒有直接進入CLOSED狀態,而是轉移到TIME_ WAIT狀態。在這個狀態,客戶端連接要等待一段 長為2MSL (Maximum Segment Life,報文段最大生存時間)的時間,才能完全關閉。MSL是TCP報文段在網絡中的最大生存時間,標准文檔RFC 1122的建議值是2 min.TIME_ WAIT狀態存在的原因有兩點:
- 可靠地終止TCP連接。
- 保證讓遲來的TCP報文段有足夠的時間被識別並丟棄。
第一個原因很好理解。假設圖中用於確認服務器結束報文段6的TCP報文段7丟失,那么服務器將重發結束報文段。因此客戶端需要停留在某個狀態以處理重復收到的結束報文段(即向服務器發送確認報文段)。否則,客戶端將以復位報文段來回應服務器,服務器則認為這是-一個錯誤,因為它期望的是一個像TCP報文段7那樣的確認報文段。在Linux系統上,一個TCP端口不能被同時打開多次(兩次及以上)。當一個TCP連接處於TIME_ WAIT狀態時,我們將無法立即使用該連接占用着的端口來建立一個新連接。反過來思考,如果不存在TIME_ WAIT狀態,則應用程序能夠立即建立-一個和剛關閉的連接相似的連接(這里說的相似,是指它們具有相同的IP地址和端口號)。這個新的、和原來相似的連接被稱為原來的連接的化身( incarnation)。新的化身可能接收到屬於原來的連接的、攜帶應用程序數據的TCP報文段(遲到的報文段),這顯然是不應該發生的。這就是TIME_WAIT狀態存在的第二個原因。
另外,因為TCP報文段的最大生存時間是MSL,所以堅持2MSL時間的TIME WAIT狀態能夠確保網絡上兩個傳輸方向上尚未被接收到的、遲到的TCP報文段都已經消失(被中轉路由器丟棄)。因此,一個連接的新的化身可以在2MSL時間之后安全地建立,而絕對不會接收到屬於原來連接的應用程序數據,這就是TIME_WAIT狀態要持續2MSL時間的原因。
有時候我們希望避免TIME__WAIT狀態,因為當程序退出后,我們希望能夠立即重啟它。但由於處在TIME_ WAIT狀態的連接還占用着端口,程序將無法啟動(直到2MSL超時時間結束)。
但如果是服務器主動關閉連接后異常終止,則因為它總是使用同- -個知名服務端口號,所以連接的TIME WAIT狀態將導致它不能立即重啟。不過,我們可以通過socket選項so_REUSEADDR來強制進程立即使用處於TIME _WAIT狀態的連接占用的端口。
復位報文段
在某些特殊條件下,TCP連接的一端會向另一端發送攜帶RST標志的報文段,即復位報文段,以通知對方關閉連接或重新建立連接。
訪問不存在的端口
當訪問一個不存在的端口時,將回應一個復位報文段。因為復位報文段的接收通告窗口大小為0,所以可以預見:收到復位報文段的一端應該關閉連接或者重新連接,而不能回應這個復位報文段。實際上,當客戶端程序向服務器的某個端口發起連接,而該端口仍被處於TIMEWAIT狀態的連接所占用時,客戶端程序也將收到復位報文段。
異常終止連接
TCP提供了異常終止一個連接的方法,即給對方發送- -個復位報文段。一旦發送了復位報文段,發送端所有排隊等待發送的數據都將被丟棄。應用程序可以使用socket選項sO_ LINGER來發送復位報文段,以異常終止-一個連接。我們將在第5章討論SO_ LINGER選項。
處理半打開連接
考慮下面的情況:服務器(或客戶端)關閉或者異常終止了連接,而對方沒有接收到結束報文段(比如發生了網絡故障),此時,客戶端(或服務器)還維持着原來的連接,而服務器(或客戶端)即使重啟,也已經沒有該連接的任何信息了。我們將這種狀態稱為半打開狀態,處於這種狀態的連接稱為半打開連接。如果客戶端(或服務器)往處於半打開狀態的連接寫人數據,則對方將回應-一個復位報文段。
TCP交互數據流
TCP報文段所攜帶的應用程序數據按照長度分為兩種:交互數據和成塊數據。交互數據僅包含很少的字節。使用交互數據的應用程序( 或協議)對實時性要求高,比如telnet、ssh .等。成塊數據的長度則通常為TCP報文段允許的最大數據長度。使用成塊數據的應用程序(或協議)對傳輸效率要求高,比如ftp.本節我們討論交互數據流。
在一個telnet連接中,客戶端針對服務器返回的數據所發送的確認報文段都不搒帶任何應用程序數據(長度為0),而服務器每次發送的確認報文段都包含它需要發送的應用程序數據。服務器的這種處理方式稱為延遲確認,即它不馬上確認上次收到的數據,而是在一段延遲時間后查看本端是否有數據需要發送,如果有,則和確認信息-一起發出。因為服務器對客戶請求處理得很快,所以它發送確認報文段的時候總是有數據一-起發送。 延遲確認可以減少發送TCP報文段的數量。而由於用戶的輸入速度明顯慢於客戶端程序的處理速度,所以客戶端的確認報文段總是不攜帶任何應用程序數據。前文曾提到,在TCP連接的建立和斷開過程中,也可能發生延遲確認。
上例是在本地回路運行的結果,在局域網中也能得到基本相同的結果,但在廣域網就未必如此了。廣域網上的交互數據流可能經受很大的延遲,並且,搒帶交互數據的微小TCP報文段數量一般很多(一個按鍵輸人就導致一個TCP報文段),這些因素都可能導致擁塞發生。解決該問題的-一個簡單有效的方法是使用Nagle算法。
Nagle算法要求一個TCP連接的通信雙方在任意時刻都最多只能發送一個未被確認的TCP報文段,在該TCP報文段的確認到達之前不能發送其他TCP報文段。另一方面,發送方在等待確認的同時收集本端需要發送的微量數據,並在確認到來時以一個TCP報文段將它們全部發出。這樣就極大地減少了網絡上的微小TCP報文段的數量。該算法的另-一個優點在於其自適應性:確認到達得越快,數據也就發送得越快。
TCP成塊數據流
當傳輸大量大塊數據的時候,發送方會連續發送多個TCP報文段,接收方可以一次確認所有這些報文段。那么發送方在收到上一次確認后,能連續發送多少個TCP報文段呢?這是由接收通告窗口(還需要考慮擁塞窗口)的大小決定的。
另外一個值得注意的地方是,服務器每發送4個TCP報文段就傳送一個PSH標志給客戶端,以通知客戶端的應用程序盡快讀取數據。不過這對服務器來說顯然不是必需的,因為它知道客戶端的TCP接收緩沖區中還有空閑空間(接收通告窗口大小不為0)。
帶外數據
有些傳輸層協議具有帶外(Out Of Band, 0OB)數據的概念,用於迅速通告對方本端發生的重要事件。因此,帶外數據比普通數據(也稱為帶內數據)有更高的優先級,它應該總是立即被發送,而不論發送緩沖區中是否有排隊等待發送的普通數據。帶外數據的傳輸可以使用一條獨立的傳輸層連接,也可以映射到傳輸普通數據的連接中。實際應用中,帶外數據的使用很少見,已知的僅有telnet、 ftp 等遠程非活躍程序。
UDP沒有實現帶外數據傳輸,TCP也沒有真正的帶外數據。不過TCP利用其頭部中的緊急指針標志和緊急指針兩個字段,給應用程序提供了一種緊急方式。TCP的緊急方式利用傳輸普通數據的連接來傳輸緊急數據。這種緊急數據的含義和帶外數據類似,因此后文也將TCP緊急數據稱為帶外數據。
我們先來介紹TCP發送帶外數據的過程。假設一個進程已 經往某個TCP連接的發送緩沖區中寫入了N字節的普通數據,並等待其發送。在數據被發送前,該進程又向這個連接寫人了3字節的帶外數據“abc"。此時,待發送的TCP報文段的頭部將被設置URG標志,並且緊急指針被設置為指向最后一個帶外數據的下一字節(進一步減去當前TCP報文段的序號值得到其頭部中的緊急偏移值),如圖所示。
由圖可見,發送端一次發送的多字節的帶外數據中只有最后--字節被當作帶外數據(字母c),而其他數據(字母a和b)被當成了普通數據。如果TCP模塊以多個TCP報文段來發送圖所示TCP發送緩沖區中的內容,則每個TCP報文段都將設置URG標志,並且它們的緊急指針指向同一個位置( 數據流中帶外數據的下一個位置),但只有-一個TCP報文段真正搒帶帶外數據。
現在考慮TCP接收帶外數據的過程。TCP接收端只有在接收到緊急指針標志時才檢查緊急指針,然后根據緊急指針所指的位置確定帶外數據的位置,並將它讀人一個特殊的緩存中。這個緩存只有1字節,稱為帶外緩存。如果上層應用程序沒有及時將帶外數據從帶外緩存中讀出,則后續的帶外數據( 如果有的話)將覆蓋它。
前面討論的帶外數據的接收過程是TCP模塊接收帶外數據的默認方式。如果我們給TCP連接設置了SO_OOBINLINE選項,則帶外數據將和普通數據一樣被TCP模塊存放在TCP接收緩沖區中。此時應用程序需要像讀取普通數據一樣來讀取帶外數據。那么這種情況下如何區分帶外數據和普通數據呢?顯然,緊急指針可以用來指出帶外數據的位置,socket編程接口也提供了系統調用來識別帶外數據。
TCP超時重傳
TCP服務必須能夠重傳超時時間內未收到確認的TCP報文段。為此,TCP模塊為每個TCP報文段都維護-一個重傳定時器,該定時器在TCP報文段第一次被發送時啟動。如果超時時間內未收到接收方的應答,TCP模塊將重傳TCP報文段並重置定時器。至於下次重傳的超時時間如何選擇,以及最多執行多少次重傳,就是TCP的重傳策略。
Linux有兩個重要的內核參數與TCP超時重傳相關: /proc/sys/net/ipv4/tcp_retries1 和/proc/sys/net/ipv4/tcp_retries2。 前者指定在底層IP接管之前TCP最少執行的重傳次數,默認值是3。后者指定連接放棄前TCP最多可以執行的重傳次數,默認值是15(一般對應13 ~ 30 min)。
擁塞控制
TCP模塊還有一一個重要的任務,就是提高網絡利用率,降低丟包率,並保證網絡資源對每條數據流的公平性。這就是所謂的擁塞控制。
TCP擁塞控制的四個部分:慢啟動(slow start)、擁塞避免(congestion avoidance).快速重傳(fast retransmit)和快速恢復(fast recovery).擁塞控制算法在Linux下有多種實現,比如reno算法、vegas 算法和cubic算法
等。它們或者部分或者全部實現了上述四個部分。/pos/s/s/tipv4/epo._congestion_control 文件指示機器當前所使用的擁塞控制算法。
擁塞控制的最終受控變量是發送端向網絡-次連續寫人 (收到其中第- -個數據的確認之前)的數據量,我們稱為SWND (Send Window,發送窗口四)。不過,發送端最終以TCP報文段來發送數據,所以SWND限定了發送端能連續發送的TCP報文段數量。這些TCP報文段的最大長度(僅指數據部分)稱為SMSS (Sender Maximum Segment Size,發送者最大段大小),其值一般等於MSS.
發送端需要合理地選擇SWND的大小。如果SWND太小,會引起明顯的網絡延遲;反之,如果SWND 太大,則容易導致網絡擁塞。前文提到,接收方可通過其接收通告窗口(RWND)來控制發送端的SWND.但這顯然不夠,所以發送端引入了一個稱為擁塞窗口(Congestion Window, CWND)的狀態變量。實際的SWND值是RWND和CWND中的較小者。圖中顯示了擁塞控制的輸人和輸出(可見,它是-一個閉環反饋控制)。
慢啟動和擁塞避免
TCP連接建立好之后,CWND將被設置成初始值IW (Initial Window),其大小為2~ 4個SMSS。但新的Linux內核提高了該初始值,以減小傳輸滯后。此時發送端最多能發送Iw字節的數據。此后發送端每收到接收端的一個確認,其CWND就按照下式增加:
CWND+= min (N,SMSS)
其中N是此次確認中包含的之前未被確認的字節數。這樣--來,CWND將按照指數形;式擴大,這就是所謂的慢啟動。慢啟動算法的理由是,TCP模塊剛開始發送數據時並不知道網絡的實際情況,需要用一種試探的方式平滑地增加CWND的大小。
但是如果不施加其他手段,慢啟動必然使得CWND很快膨脹(可見慢啟動其實不慢)並最終導致網絡擁塞。因此TCP擁塞控制中定義了另一個重要的狀態變量:慢啟動門限,(slow start threshold size, ssthresh)。 當CWND的大小超過該值時,TCP 擁塞控制將進人擁塞避免階段。
擁塞避免算法使得CWND按照線性方式增加,從而減緩其擴大。RFC 5681中提到了如下兩種實現方式:
- 每個RTT時間內按照上式 計算新的CWND,而不論該RTT時間內發送端收到多少個確認。
- 每收到一個對新數據的確認報文段,就按照下式來更新CWND.
CWND+= SMSS* SMSS/CWND
圖中粗略地描述了慢啟動和擁塞避免發生的時機和區別。該圖中,我們以SMSS為單位來顯示CWND (實際上它是以字節為單位的),以次數為單位來顯示RTT,這只是為了方便討論問題。此外,我們假設當前的ssthresh是16SMSS大小(當然,實際的ssthresh 顯然遠不止這么大)。
以上我們討論了發送端在未檢測到擁塞時所采用的積極避免擁塞的方法。接下來介紹擁塞發生時(可能發生在慢啟動階段或者擁塞避免階段)擁塞控制的行為。不過我們先要搞清楚發送端是如何判斷擁塞已經發生的。發送端判斷擁塞發生的依據有如下兩個:
- 傳輸超時,或者說TCP重傳定時器溢出。
- 接收到重復的確認報文段。
擁塞控制對這兩種情況有不同的處理方式。對第一種情況仍然 使用慢啟動和擁塞避免。對第二種情況則使用快速重傳和快速恢復(如果是真的發生擁塞的話)。注意,第二種情況如果發生在重傳定時器溢出之后,則也被擁塞控制當成第一種情況來對待。如果發送端檢測到擁塞發生是由於傳輸超時,即上述第一種情況,那么它將執行重傳並做如下調整:
ssthresh=max ( FlightSize/2,2*SMSS)
CWMD<=SMSS
其中FlightSize是已經發送但未收到確認的字節數。這樣調整之后,CWMD將小於SMSS,那么也必然小於新的慢啟動門限值ssthresh (因為根據上式, 它一定不小於SMSS的2倍),故而擁塞控制再次進人慢啟動階段。
快速重傳和快速恢復
在很多情況下,發送端都可能接收到重復的確認報文段,比如TCP報文段丟失,或者接收端收到亂序TCP報文段並重排之等。擁塞控制算法需要判斷當收到重復的確認報文段時,網絡是否真的發生了擁塞,或者說TCP報文段是否真的丟失了。具體做法是:發送端如果連續收到3個重復的確認報文段,就認為是擁塞發生了。然后它啟用快速重傳和快速恢復算法來處理擁塞,過程如下:
- 當收到第3個重復的確認報文段時,按照上式計算shresh,然后立即重傳丟失的報文段,並按照下式 設置CWND.
CWND=ssthresh+3*SMSS
- 每次收到1個重復的確認時,設置CWND=CWND+SMSS.此時發送端可以發送新的TCP報文段(如果新的CWND允許的話)。
- 當收到新數據的確認時,設置CWND=ssthresh (ssthresh 是新的慢啟動門限值,由第一步計算得到)。