免責聲明:和往常同樣,此文章的觀點都屬於‘No Bugs’Hare(譯註:一個網站) ,也許不必定和翻譯者或者Overload編輯的意見一致。同時,翻譯者從Lapine翻譯到英語也具備必定的難度。除此以外,翻譯者與Overload對於從閱讀此文章所帶來的後果或不做爲明確不負任何責任。php
原文地址:Once Again on TCP vs UDPhtml
討論TCP與UDP的好與壞幾乎與Linux和windows的爭辯有着同樣長的歷史。我一直支持一個觀點,也就是:UDP與TCP都有各自的適用場景(好比:【NoBugs15】),在此討論下我在這兩方面的理解。git
對於那些已經明白了TCP與UDP基本知識的,能夠跳過「縮小差距:提高TCP的交互性」段。固然,你仍然能夠在此段發現一些有趣的東西。程序員
TCP與UDP都運行在IP基礎上,讓咱們看看網絡協議(IP)具體是什麼。對於咱們來講,咱們能夠把它理解爲一下幾點:github
實際中的操做要比上述複雜的多,(不少東西都涉及到IP的操做,包括ICMP、ARP、OSPF和BGP)那時如今咱們能夠或多或少能夠忽略一些具體的實現細節。咱們如今須要知道的是IP的構成,也就是下面的格式:算法
IP 報頭(IPv4:20到24字節) |
IP負載 |
IP的一個很是重要的特性就是徹底不保證數據包的投遞。任意一個包均可能丟失,這就意味着可能會丟失任意數量的包。windows
IP只能做爲統計,這種行爲也是由它的設計決定的。這也就是爲何主幹英特網路由器可以路由數量巨大的網絡流量。若是在路由期間發生錯誤(包括從鏈路過載到忽然重啓等緣由),路由器則被容許將錯誤包丟棄。api
在IP數據棧中,提供可靠的數據傳輸是有主機保證的,路由器不作任何保證。瀏覽器
接下來,討論下兩個協議中簡單的一個: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數據報文在傳輸過程當中也一樣會丟失。
和UDP相反,TCP是個很是複雜的協議,它保證數據的可靠傳輸。惟一相對簡單點的就是TCP的數據包格式:
IP報頭(IPv4:20到24字節) |
TCP報頭(20到60字節) |
TCP負載 |
一般狀況下,TCP的報頭在20字節,在不多的狀況下,會達到60字節。
一旦咱們經過TCP傳輸數據的時候,事情就變得複雜起來。下面是對TCP工做的簡單描述和歸納【1】:
【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那麼複雜同時也那麼重要,而且提供了可靠的數據傳輸,爲何不在全部的網絡數據傳輸中都使用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’)。
在上述的方案U中,UDP的實現方式工做的很好,這個和消息交換的細節有着密切關係。特別是,咱們假設每個包都有全部必須的信息。那麼丟失一個包,會被下一個包給恢復。可是若是不是這樣狀況,使用UDP就變得不那麼簡單了(non-trivial)。
一樣,這整個模式是假設每10ms發送一個數據包。這很輕易地就形成大量的網絡傳輸。另外一方面,方案U在增長髮送時間間隔後,會失去必定的交互性。
基本上,會有一些經驗能夠借鑑:
當實現一個可靠的UDP協議時,實現的TCP協議特性越多,最終獲得一個次品的TCP協議實現版本的機率就越大
當你處於須要使用UDP,但卻又同時須要讓它變得可靠的狀況下時,你可使用可靠的UDP庫【Enet】【UDT】【RakNet】。然而,這些庫也沒有施任何魔法,它們本質上也是侷限在必定的超時後進行重發。所以,在使用任何庫以前,你須要準確明白究竟要達到什麼程度的可靠性,同時在這個過程中,能夠犧牲掉多少交互性。
在實現可靠UDP傳輸的過程中應該注意的是,現的TCP協議特性越多,最終獲得一個次品的TCP協議實現版本的機率就越大。TCP是一個很是複雜的協議(對於大部分的複雜都是有正當理由的),全部試圖去實現一個」更好的TCP「是異常艱難的。另外一方面,以丟棄TCP大部分功能而去實現一個」可靠的UDP「倒是有可能的。
有幾件事情,能夠用來提高TCP的交互性。
在使用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交互性的一個很是流行的方式是在TCP的socket上開啓TCP_NODELAY(設置setsockopt()的一個參數)。若是開啓了TCP_NODELAY,那麼Nagle算法將會被關閉(一般狀況下,開啓TCP_NODELAY還會有一些其餘影響。好比會增長PSH標誌,這會致使在數據發送端的TCP棧收到數據後直接發送,而不須要等待緩衝區滿的時候才發送,一般這也是想要的結果。保持不變的是,它不會強制數據包被安全投遞到目的地址,直到TCP流中的先前的數據包已經被接收,這是由於須要維持流的一致性)。
然而,TCP_NODLAY不是沒有注意事項的。最重要的是,開啓了TCP_NODELAY,那麼程序員本身須要在調用send()以前,收集全部須要發送的數據。不然的話,每調用一次send()都會致使TCP棧發送一個數據包(與之相關聯的是40-84字節的開銷)。
TCP的OOB機制的目的是打破原有的數據流,發送一些優先級更高的數據。隨着OOB增長的優先級(在某種意義上,這種行爲是繞過了TCP的發送緩衝區和TCP接收緩衝區),它能夠處理一些交互性相關的事情。可是,TCP的OOB數據只能狗發送1字節(你可使用send(…, MSG_OOB)發送多個字節,可是隻有最後一個字節的數據纔會被解釋稱OOB數據),這種使用方式一般具備很大的限制。
一個能夠體現MSG_OOB優勢的場景是,在發送大文件過程當中,發送一個OOB的」終止」命令,當接收到OOB的「終止」命令後,接收方能夠簡單地從流中讀取全部數據,而後丟棄。這樣TCP緩衝區就被高效地清除了。同時恢復鏈路而不用丟棄老的鏈路去新建新的鏈路。
即便使用了以上所有的技巧,TCP依舊仍是缺少普遍的交互性。尤爲是,OOB傳輸1字節的數據也不是一個可選項,即便是丟失了一些可有可無的數據,也會致使重發。處理鏈路「阻塞」也只是減輕了這個問題帶來的影響,並無從根本上解決。另外一方面,若是應用程序可以容忍必定程度上的延遲,那麼在「other considerations」描述的一些方法能夠你選擇協議的關鍵因素。
若是你足夠幸運,你的應用程序的需求同時能夠被TCP或UDP知足,如下的注意事項可能會影響到你的選擇。這些注意事項包括:(但不侷限於)
選擇TCP而是UDP(或者相反)一般不是那麼顯而易見。在某種意義上,使用UDP替代TCP實際上是以交互性代替可靠性。在選擇過程當中,有一個相當重要的因素是可接受的延遲大小。TCP一般是在幾秒左右,而UDP則在0.1秒如下。另外一方面,在「灰色地帶」,上述的注意事項也是須要考慮的。同時,提高TCP的交互性和UDP的可靠性仍是有一些方法的(見上述)。大多數時候,能夠縮小這二者之間的差距。
[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/