[譯]再次對比TCP與UDP

免責聲明:和往常同樣,此文章的觀點都屬於‘No Bugs’Hare(譯註:一個網站) ,也許不必定和翻譯者或者Overload編輯的意見一致。同時,翻譯者從Lapine翻譯到英語也具備必定的難度。除此以外,翻譯者與Overload對於從閱讀此文章所帶來的後果或不做爲明確不負任何責任。php

原文地址:Once Again on TCP vs UDPhtml

BB_part64_v1-640x427

      討論TCP與UDP的好與壞幾乎與Linux和windows的爭辯有着同樣長的歷史。我一直支持一個觀點,也就是:UDP與TCP都有各自的適用場景(好比:【NoBugs15】),在此討論下我在這兩方面的理解。git

     對於那些已經明白了TCP與UDP基本知識的,能夠跳過「縮小差距:提高TCP的交互性」段。固然,你仍然能夠在此段發現一些有趣的東西。程序員

 

IP:除了數據包,別無其餘(just packets, nothing more)

     TCP與UDP都運行在IP基礎上,讓咱們看看網絡協議(IP)具體是什麼。對於咱們來講,咱們能夠把它理解爲一下幾點:github

  • 咱們有兩臺須要互相通訊的主機
  • 每臺主機都有一個屬於它們的IP地址
  • 網絡協議(IP棧)使用IP做爲一個標識符,提供了一個從主機A傳遞數據包到主機B的方法

     實際中的操做要比上述複雜的多,(不少東西都涉及到IP的操做,包括ICMP、ARP、OSPF和BGP)那時如今咱們能夠或多或少能夠忽略一些具體的實現細節。咱們如今須要知道的是IP的構成,也就是下面的格式:算法

IP 報頭(IPv4:20到24字節)
IP負載

     IP的一個很是重要的特性就是徹底不保證數據包的投遞。任意一個包均可能丟失,這就意味着可能會丟失任意數量的包。windows

IP只能做爲統計,這種行爲也是由它的設計決定的。這也就是爲何主幹英特網路由器可以路由數量巨大的網絡流量。若是在路由期間發生錯誤(包括從鏈路過載到忽然重啓等緣由),路由器則被容許將錯誤包丟棄。api

在IP數據棧中,提供可靠的數據傳輸是有主機保證的,路由器不作任何保證。瀏覽器

 

UDP:數據報文 ~= 數據包(datagrams ~= packets)

      接下來,討論下兩個協議中簡單的一個:UDP。UDP是運行在IP協議上的一個很是基礎的協議。這是由於它比較基礎,當UDP的數據報文運行在IP包之上時,老是會有一個一對一的對應關係。全部的UDP數據報文都增長了一個簡單的報頭(除了IP報頭)。報頭由4個段組成:源端口、目標端口、數據長度、校驗和。總共8個字節。一個典型的UDP數據報文如如下格式:緩存

IP報頭(IPv4:20到24字節)
UDP報頭(8字節)
UDP負載

      UDP「數據報文」和IP「數據包」很是相像。二者惟一的區別是增長了8字節的UDP報頭。在本文的剩下部分,咱們會交叉地使用這兩個術語。

UDP數據報文只是簡單地運行在IP數據包之上,IP數據包在傳輸過程當中會丟失,UDP數據報文在傳輸過程當中也一樣會丟失。

 

TCP:流!=數據包(stream != packets)

      和UDP相反,TCP是個很是複雜的協議,它保證數據的可靠傳輸。惟一相對簡單點的就是TCP的數據包格式:

IP報頭(IPv4:20到24字節)
TCP報頭(20到60字節)
TCP負載

      一般狀況下,TCP的報頭在20字節,在不多的狀況下,會達到60字節。

      一旦咱們經過TCP傳輸數據的時候,事情就變得複雜起來。下面是對TCP工做的簡單描述和歸納【1】:

  • TCP將在兩個主機之間通信的全部數據都翻譯成數據流(一個數據流從主機A流向主機B,另外一個則相反)
  • 每當主機調用send()時,數據都會被推送至數據流
  • TCP數據棧在發送端維護着一個緩衝區(一般爲2K-16K大小),全部推送到數據流的數據都到這個緩衝區中。若是緩存滿了的話,此刻send()不會馬上返回,直到緩衝區有足夠的空間【2】
  • 緩衝區中的數據會經過IP傳輸,就像TCP數據包同樣。每一個TCP數據包由一個IP數據包,一個TCP報頭,和TCP負載數據組成。從TCP緩衝區中發送的數據就是TCP數據包中的TCP負載數據。在TCP數據包發送的時候,負載數據並不會從發送端的TCP緩衝區中刪除。
  • 在接收端收到數據包後,它會向發送端發送一個附帶ACK的TCP報頭的TCP數據包,代表數據的特定的一部分已經收到。在發送端接收到ACK包後,發送端纔有可能會將相應的數據從TCP發送緩衝區中刪除【3】
  • 在接收端收到的數據會存放到另外一個緩衝區中,這個緩衝區的大小一般也是2K-16K。這個緩衝區保存的數據也就是recv()接收的數據源
  • 若是在預約的時間範圍內,發送端沒有收到ACK包,那麼將會從新發送上一個數據包。這也是TCP防止數據丟失,保證數據完整傳輸的主要機制【4】

【1】爲了簡單起見,這裏忽略了流控(flow control)與TCP窗口(TCP windows),同時例如SACK這樣的優化和快速重傳也不討論。

【2】或者,若是socket是non-blocking的話,這種狀況下,send()會直接返回EWOULDBLOCK。

【3】在實際中,ACK不必做爲一個單獨的包發送。任何一個包都有可能在須要的方向上攜帶ACK標誌。

【4】重發還有另外一種機制,當收到ACK時包含了重發,可是是亂序的。這部份內容超出了本文的討論範圍。

      到目前爲止,一切都還不算太複雜。可是還有一些注意事項。首先,當重發是由於沒有收到ACK而超時,那麼在下一次重發時,超時時間會加倍。發送第一個重發是T1時間後,也就是RTT(round-trip-time)的兩倍。發送第二個重發是T2=2*T1時間後,發送第三個則是T3=2*T2,以此類推。有這個特性(exponential back-off)是防止由於連續發送重發數據包而致使的網絡擁塞,儘管這當今這個避免網絡擁塞的方法受到挑戰。無論出於什麼緣由,「指數時間重發」仍是出如今TCP棧中。因此咱們仍是得面對它(咱們會在下面瞭解爲何以及在何時它那麼重要)。另外一個注意事項是和交互相關,涉及到一個名爲Nagle的算法。最初是設計用來避免telnet對每一個壓縮字符傳輸41字節的數據包(4000%的開銷)。從「TCP即流」的視角來看,它同時也隱藏了不少與數據包相關聯的細節(最終成爲程序員能夠將隨意大小的數據投遞到緩衝區中而不用關心細節的手段,由於「聰明的TCP棧」會爲咱們組裝全部的數據包 )。Nagle算法會避免發送一個新的數據包,只要知足a)是一個未被確認的outstanding數據包,b)緩衝區中沒有足夠的數據填充一個完整的數據包。在接下來咱們能夠看到,這對交互性有着重大的影響(可是,幸運的是,Nagle算法是能夠關閉的)。

 

TCP:是咱們所須要的?不是理想中的那麼快。(Just the ticket? No so fast :-()

      有人可能要問:若是TCP那麼複雜同時也那麼重要,而且提供了可靠的數據傳輸,爲何不在全部的網絡數據傳輸中都使用TCP呢?

      不幸的是,事情沒有那麼簡單。TCP的可靠數據傳輸是要付出代價的。而這個代價就是失去必定的交互性。

      咱們假想一個第一人稱射擊遊戲,遊戲中爲了獲取玩家的位置信息,玩家須要及時跟新本身的位置信息。咱們假設有兩種實現方式:方案U,玩家經過UDP發送位置信息(假設遊戲的實時性很好,且位置信息是隨着時間而變的,每10ms發送一個UDP包);方案T,玩家經過TCP發送位置信息。

      首先,方案T,在程序中每10ms調用一次send(),可是RTT假設是50ms,那麼更新的數據就是延遲(基於上述討論的Nagle算法)。幸運的是,Nagel算法是能夠經過使用TCP_NODELAY選項手動關閉的(詳細參照:‘Closing the gap: improving TCP interactivity’)。

      若是Nagle算法被關閉,同時也沒有數據包丟失(假設兩端處理數據的速度都夠快),那麼上述的兩個方案不會有任何的區別。可是假設其中的一些數據丟失了,那麼接下來會發生什麼?

      對於方案U,即便數據包丟失了,接下來的數據包也會及時更新,玩家的正確位置信息也會被快速恢復(最多10ms)。可是對於方案T來講,咱們對超時時間是沒法控制的,因此數據包會在超過2*RTT時間後才重發,即便是第一人稱射擊遊戲,RTT也很輕易地就超過了50ms(跨越大西洋至少須要100-150ms)。超過100ms以後,纔會重發上次丟失的數據包。這相對於方案U來講,是一個很大的劣勢。除此以外,對於方案T來講,若是第一包丟失了,而第二包被及時傳輸到目的方,第二包是不會傳遞給應用程序,直到第一包的重發數據包被正確接收後。將全部的數據視爲流就勢必會出現這樣的結果(只能在流前面的數據傳輸完後,才傳輸流後面的數據)。

      若是在傳輸過程當中,有多個數據包丟失,那麼對於方案T來講,事情還會變得更糟。第二個重發會在200ms以後(假設RTT爲50ms),以此類推。這種狀況反過來,當新的TCP連接創建後正常工做時,會致使已有的TCP連接出現「阻塞」。這種狀況是能夠解決的,可是須要一些努力(詳細參照:‘Closing the gap: improving TCP interactivity’)。

 

那麼,咱們老是該使用UDP?(So, should we always go with UDP?)

      在上述的方案U中,UDP的實現方式工做的很好,這個和消息交換的細節有着密切關係。特別是,咱們假設每個包都有全部必須的信息。那麼丟失一個包,會被下一個包給恢復。可是若是不是這樣狀況,使用UDP就變得不那麼簡單了(non-trivial)。

      一樣,這整個模式是假設每10ms發送一個數據包。這很輕易地就形成大量的網絡傳輸。另外一方面,方案U在增長髮送時間間隔後,會失去必定的交互性。

 

那麼咱們應該怎麼作?(What should we do then?)

      基本上,會有一些經驗能夠借鑑:

  • 若是應用程序的特徵時間大約是幾個小時(好比,傳輸冗長的文件),使用TCP應該沒什麼問題,開啓TCP內建的Keep-alive是推薦的作法
  • 若是應用程序的特徵時間在幾個小時之內,可是超過5秒。使用TCP多多少少也是能夠,可是,爲了確保交互性,最好實現一個本身的Keep-alive方案
  • 若是應用程序的特徵時間大體是在100ms和5秒之間。這是一個比較灰色的地帶。使用哪一個協議要根據實際狀況而定。好比「你在應用程序曾如何處理丟包的狀況?」,「你須要安全傳輸嗎?」具體參考」Closing the gap: reliable UDP「和」Closing the gap: improving TCP Interactivity「
  • 若是應用程序的特徵時間在100ms如下,頗有多是須要使用UDP,具體參考」Closing the gap: reliable UDP「,向UDP增長可靠性

當實現一個可靠的UDP協議時,實現的TCP協議特性越多,最終獲得一個次品的TCP協議實現版本的機率就越大

 

縮小差距:可靠的UDP(Closing the gap: reliable UDP)

      當你處於須要使用UDP,但卻又同時須要讓它變得可靠的狀況下時,你可使用可靠的UDP庫【Enet】【UDT】【RakNet】。然而,這些庫也沒有施任何魔法,它們本質上也是侷限在必定的超時後進行重發。所以,在使用任何庫以前,你須要準確明白究竟要達到什麼程度的可靠性,同時在這個過程中,能夠犧牲掉多少交互性。

      在實現可靠UDP傳輸的過程中應該注意的是,現的TCP協議特性越多,最終獲得一個次品的TCP協議實現版本的機率就越大。TCP是一個很是複雜的協議(對於大部分的複雜都是有正當理由的),全部試圖去實現一個」更好的TCP「是異常艱難的。另外一方面,以丟棄TCP大部分功能而去實現一個」可靠的UDP「倒是有可能的。

 

縮小差距:提高TCP的交互性(Closing the gap: improving TCP interactivity)

      有幾件事情,能夠用來提高TCP的交互性。

Keep-alives和連接」阻塞「(Keep-alives and ‘stuck’ connections)

      在使用TCP進行交互式通信過程當中,其中一個最惱人的問題就是TCP連接」阻塞「。當你使用瀏覽器瀏覽網頁卻卡在加載期的時候,能夠按下刷新鍵。這就是在瀏覽器中遇到的鏈路」阻塞「例子。一個解決鏈路」阻塞「的方法是在數據交互過程中,每N秒發送相似」keep alive「的數據包。若是在某一端沒有收到消息,也就是2*N時間超時。那麼就能夠假設鏈路已經」阻塞「了,此時能夠嘗試去從新創建連接。

      TCP協議自己包括了Keep-Alive機制(setsockopt()中的SO_KEEPALIVE)。可是這個間隔一般是2個小時(更糟的是,在windows平臺下,沒有一種可配置的方式,除了經過一個註冊表的全局設置項)。所以,若是想在2個小時內檢測鏈路是否」阻塞「,同時在TCP兩端的連接的操做系統都不支持socket級的keep-alive超時時間,你就須要本身經過TCP建立一個keep-alive,按照你想要的超時時間。這不是系統性的工程,可是也仍是須要花費一點功夫。

      實現自定義的keep-alive機制一般有如下幾個方式:

  • 將TCP流拆分紅一個個消息,每一個消息包含着類型,大小,和負載數據
  • 消息類型爲MY_DATA,標誌爲真正的負載數據。當接收到此類型數據時,會傳到上一層。做爲附加選項,能夠選擇重置」鏈路死亡「時間
  • 另一個消息類型爲MY_KEEPALIVE,不附加任何負載數據。當接收到此類型數據時,不會傳到上一層,可是會重置」鏈路死亡「時間
  • 在TCP鏈路上沒有其餘數據傳輸時,MY_KEEPALIVE類型數據會每間隔N秒發送
  • 當」鏈路死亡「時間超時時,此鏈路就將被聲明爲已斷開,以後會進行重連。

      做爲一個優化方式,可能在創建新鏈路的時候,老的連接須要保持鏈接。當你在創建新鏈路的時候,老的鏈路接收到了一些數據,那麼能夠選擇恢復老鏈路的通信,丟棄新鏈路。

TCP_NODELAY

      提高TCP交互性的一個很是流行的方式是在TCP的socket上開啓TCP_NODELAY(設置setsockopt()的一個參數)。若是開啓了TCP_NODELAY,那麼Nagle算法將會被關閉(一般狀況下,開啓TCP_NODELAY還會有一些其餘影響。好比會增長PSH標誌,這會致使在數據發送端的TCP棧收到數據後直接發送,而不須要等待緩衝區滿的時候才發送,一般這也是想要的結果。保持不變的是,它不會強制數據包被安全投遞到目的地址,直到TCP流中的先前的數據包已經被接收,這是由於須要維持流的一致性)。

    然而,TCP_NODLAY不是沒有注意事項的。最重要的是,開啓了TCP_NODELAY,那麼程序員本身須要在調用send()以前,收集全部須要發送的數據。不然的話,每調用一次send()都會致使TCP棧發送一個數據包(與之相關聯的是40-84字節的開銷)。

Out-of-Band Data

      TCP的OOB機制的目的是打破原有的數據流,發送一些優先級更高的數據。隨着OOB增長的優先級(在某種意義上,這種行爲是繞過了TCP的發送緩衝區和TCP接收緩衝區),它能夠處理一些交互性相關的事情。可是,TCP的OOB數據只能狗發送1字節(你可使用send(…, MSG_OOB)發送多個字節,可是隻有最後一個字節的數據纔會被解釋稱OOB數據),這種使用方式一般具備很大的限制。

      一個能夠體現MSG_OOB優勢的場景是,在發送大文件過程當中,發送一個OOB的」終止」命令,當接收到OOB的「終止」命令後,接收方能夠簡單地從流中讀取全部數據,而後丟棄。這樣TCP緩衝區就被高效地清除了。同時恢復鏈路而不用丟棄老的鏈路去新建新的鏈路。

剩餘的一些問題(Residual issues)

      即便使用了以上所有的技巧,TCP依舊仍是缺少普遍的交互性。尤爲是,OOB傳輸1字節的數據也不是一個可選項,即便是丟失了一些可有可無的數據,也會致使重發。處理鏈路「阻塞」也只是減輕了這個問題帶來的影響,並無從根本上解決。另外一方面,若是應用程序可以容忍必定程度上的延遲,那麼在「other considerations」描述的一些方法能夠你選擇協議的關鍵因素。

其餘的注意事項(Other considerations)

      若是你足夠幸運,你的應用程序的需求同時能夠被TCP或UDP知足,如下的注意事項可能會影響到你的選擇。這些注意事項包括:(但不侷限於)

  • TCP傳輸保證數據包的有序性,而UDP卻不保證(也許「可靠UDP」能夠)
  • TCP有流控,UDP沒有
  • TCP一般在防火牆和NAT網絡更友好,能夠簡單解釋爲「若是你想要你的用戶經過賓館或者工做場所去鏈接,TCP一般都能工做的很好,尤爲是在80端口或是443端口」
  • TCP程序一般很容易編寫。可是TCP也不是沒有注意事項,在此不進行討論。而使用UDP一般工做起來不會有太大的問題,可是要花比較長的時間
  • TCP一般有更大的開銷,尤爲在鏈路創建和關閉鏈路的時候。在總的網絡流量中,這點開銷可能算不了什麼,但其終究仍是一個須要考慮的問題

總結(Conclusions)

      選擇TCP而是UDP(或者相反)一般不是那麼顯而易見。在某種意義上,使用UDP替代TCP實際上是以交互性代替可靠性。在選擇過程當中,有一個相當重要的因素是可接受的延遲大小。TCP一般是在幾秒左右,而UDP則在0.1秒如下。另外一方面,在「灰色地帶」,上述的注意事項也是須要考慮的。同時,提高TCP的交互性和UDP的可靠性仍是有一些方法的(見上述)。大多數時候,能夠縮小這二者之間的差距。

 

引用(References)

[Enet] http://enet.bespin.org/
[Loganberry04] David ‘Loganberry’ Buttery, ‘Frithaes! – an Introduction
to Colloquial Lapine’, http://bitsnbobstones.watershipdown.org/
lapine/overview.html
[Masterraghu] http://www.masterraghu.com/subjects/np/introduction/
unix_network_programming_v1.3/ch24lev1sec2.html
[Mondal] Amit Mondal, Aleksandar Kuzmanovic, ‘Removing
Exponential Backoff from TCP’, ACM SIGCOMM Computer
Communication Review
[NoBugs15] ‘64 Network DO’s and DON’Ts for Game Engines. Part IV.
Great TCP-vs-UDP Debate’ http://ithare.com/64-network-dos-anddonts-
for-game-engines-part-iv-great-tcp-vs-udp-debate/
[NoBugs15a] ‘64 Network DO’s and DON’Ts for Game Engines. Part
VI. TCP’ http://ithare.com/64-network-dos-and-donts-for-multiplayer-
game-developers-part-vi-tcp/
[RakNet] https://github.com/OculusVR/RakNet
[Stevens] W. Richard Stevens, Bill Fenner, Andrew M. Rudoff, UNIX®
Network Programming Volume 1, Third Edition: The Sockets
Networking API
[UDT] http://udt.sourceforge.net/

相關文章
相關標籤/搜索