如今,但凡說精通網絡的,第二個意思就是“精通TCP”,事實上,很多自稱精通TCP的家伙們只是精通socket接口而已,對TCP行為精通的並不多,筆者也不算精通,但絕對是中等以上水平。如果你真的精通TCP行為,那么本文不讀也罷,直接發郵件給我,我們切磋一下,如果只是了解socket接口,那么建議讀本文,然后一定再看一下《TCP協議疑難雜症全景解析》
0.UDP協議和TCP協議
UDP是用戶數據報協議的簡稱,對於分組交換網絡,它實際上扮演了傳統郵局的角色,而TCP則是扮演了電話運營商以及物流公司的角色,對於分組交換網絡而言,UDP要比TCP更加基本一些,可以說,TCP則是實現一種基於流的通信過程,在IP這個數據報協議之上,TCP和UDP分別實現了更高層的“電路交換”和“分組交換”。
1.帶連接的udp(connect udp)
很多人都以為UDP連接不連接無所謂,實則不是這么簡單,但凡技術上的事,沒有無所謂,除非你能給出比讓你無所謂多一倍的理由。
1.1.效率
在操作TCP/IP協議的時候,說白了就是編寫基於網絡通信代碼的時候,你必須知道你所使用的協議是在哪里實現的,對於大多數的操作系統,協議棧都是內核的一部分,因此它們是在內核空間運行的,這就涉及到一個會影響效率的問題,那就是內核空間和用戶空間的切換過程你要了解,對於x86處理器以及大多數其它處理器,這個切換是比較耗時的,涉及到上下文的save/restore,因此如果你的應用是效率優先的,那么就要想辦法最小化切換次數,這還不夠,還有就是如果切換避免不了,那么就盡量減少拷貝次數,由於UDP是基於數據報的,數據只要准備好就會調用一次send或者sendto,這個是由應用邏輯決定的,然而我們可以決定的是到底是用send還是sendto,看看參數便知,sendto的參數要比send更多,因此這就意味着,如果使用sendto,需要拷貝的參數就會多一些,參數到了內核空間之后,內核還要准備數據結構暫時容納這些參數,當數據發出之后,內核需要釋放這些暫時存儲的參數(當然可以使用棧來管理這些參數以實現自動釋放)。
如果一個UDP是connect的,那就可以在connect之后直接調用send了,內核在應用connect之后就永久維護了這次UDP的連接,以后每次收發數據,內核不再需要分配/刪除這些數據,而只是查找就可以了,同時也減少了數據的拷貝量,既然connect的UDP在內核中已經存在了一個“連接”,那么無論何時,只要通信沒有結束,內核總是能隨時追蹤到這個“連接”,因此就引出了1.2。
1.2.錯誤提示
如果是一個沒有“連接”的UDP,數據在調用sendto發出后發送端就釋放了關於目的地的任何信息,然而數據如果最終由IP封裝(或者被任何有錯誤提示的下層協議封裝),數據在半路上或者終點遇到某種問題不能到達目的地時,會有ICMP(對於非IP協議,可以是其它機制)錯誤信息返回,然則發送端已經不再可能知道該錯誤信息要發給哪個應用(源/目的地信息已經釋放)。
對於有“連接”的UDP通信,由於內核協議棧已經維護了從源到目的地的單向連接,因此當錯誤信息發來的時候,內核協議棧會准確定位到該轉發給哪個應用。
最后說明一點,UDP的連接是單向的,在調用connect的時候並不會產生任何通信流量,它只是在內核協議棧中綁定了一對五元組而已,該五元組是:UDP協議/源IP/源端口/目的IP/目的端口。
2.udp高效率的神話
如果你在網上詢問,TCP和UDP的區別,得到的結果無非以下幾種:UDP不需要處理確認,UDP比較高效,基於連接與無連接,TCP耗資源,...
然而以上這些都是神話,是神話就是要破除它。UDP並不是一定比TCP高效,要知道,TCP發展到今天,其算法已經非常豐富,合理配置的話足以應付各種復雜的環境,大多數情況下都會比UDP更加高效。
3.udp的設計-多路復用的IP
TCP/IP只是一個協議族,在這個協議族上,完成了所有應該完成的工作,UDP作為一種數據報協議,其更多的作用在於使用端口的概念進行應用復用,在實現上,它基本上是復制了IP協議,在復制的基礎上,增加了一個可選的校驗和,一定程度上保護了數據的完整性,當然你也可以不要它。
IP協議完美的實現了數據報業務,發后不管結果,盡力而為-雖然ICMP一定程度提供了有限的反饋信息。有人說IP協議很不負責,然則分層模型的要旨就是每層僅提供單一的服務,這也是unix的哲學。IP協議僅僅提供了最底層的數據報分組交換通信,這是和電路交換完全並行的一個通信模型。有的時候,真的需要這樣的“發后不管,盡力而為”的服務,TCP/IP的絕妙解決方式不是直接讓IP來提供這個服務,而是在IP之上提供了多路復用的UDP,結果是,針對於主機,同一個IP可以承載多個“發后不管,盡力而為”的服務,IP最終僅僅提供傳輸服務。
4.udp的使用場合前奏
讀懂了第3節,那么我們接着往后說,到底哪些業務需要“發后不管,盡力而為”的服務呢?在現實中,我們知道,平信是這樣的業務,說起平信,值得還念,90后的幾乎再也不需要寫信了,當時我上大學時,一周要給遠方的女朋友寄送最少一封信,可是有時候信件一周內就到了,可是有時,信件丟了,因此我要花費大量的電話時間和金錢來解釋以證明自己真的寫了信,只是沒有送到而已,也有的時候,當我解釋了之后,信件莫名其妙的到了,延遲了好久。這就是發后不管,盡力而為的服務,也有那么一次,為了澄清一個事實-這封信絕對不能丟,我使用了特快專遞,雖然並沒有表現出什么特快,然而我收到了回執,這就不是發后不管的服務,而是“帶確認的服務”,因此是否發后不管和通信效率並沒有什么必然關聯,這是需要澄清的。
很多人都認為udp應用在實時要求比較高的領域,然后還說什么自己完成順序和重傳,既然需要保序和重傳,那為何不直接使用TCP呢,這些人實際上知道TCP之所以效率低,就是因為它需要處理按序交付以及處理重傳。如果為UDP加上了這些功能,那豈不是UDP的優勢也不再了?
我想,上述這些人一定是教科書讀太多了,加上各種教科書又都是從那么幾本經典書籍以及不多的幾篇RFC中摘錄的,因此“天下教科書一大抄”本身成就了眾口鑠金的效果。實際情況遠非這么簡單。TCP真的沒有UDP效率高嗎?未必!
要知道,我們網民生活的互聯網的每一根通信管道並不是固定容量的,而是可以伸縮的,然而標准的UDP在發送時卻是每包長度固定的,為了簡單起見,使用UDP協議的應用程序之間都使用固定長度的包通信,這就有問題了...
4.1.問題一:無法利用空閑帶寬導致資源浪費
很簡單的一個場景,UDP雙方每次以512字節定長包通信,這就意味着發送端發出512字節,接收端就接收512字節,每512字節封裝一個IP包,除了端口復用這個優勢之外,白白增加了一個UDP頭的封裝成本。即使能一次發送更多的數據,也不得不每512字節發一次,對於使用TCP的應用,數據到達內核協議棧以后,某些情況下,可以暫時積攢起來,這樣就節省了封裝的費用,但是並不會浪費時間導致交互問題,因為TCP會使用一種很聰明的算法,當發現數據必須積攢的時候,就說明此時不積攢也不行,TCP的復雜算法會在延遲和吞吐量之間達到一個很好的平衡,詳見《TCP協議疑難雜症全景解析》
最簡單的一個事實就是鏈路的MTU幾乎不會影響到UDP,而會影響到TCP,而這直接影響IP的分片,這又是一個效率問題,極端點說,UDP可能每次發送的包是MTU的幾百分之一,也可能是MTU的幾百倍,前者太低效,后者將消耗轉嫁給了IP。
4.2.問題二:網絡擁塞或兩端性能不匹配時無法被反饋導致大量丟包
對於TCP而言,它可以使用擁塞控制和流量控制算法智能控制該發送速率,而對於UDP,由於沒有確認機制,即使網絡擁堵了或者對端機器吃不消了,發送端還是不停的發送,這樣就會加重病態的惡化程度,對於這種惡化,UDP的收發兩端不必負任何責任,詳見5.1。
4.3.改進UDP發包為動態長度代價太高
由於上述的兩個問題,有必要對UDP在用戶態做一些調整,然而因為有TCP的存在,到底是做調整還是直接用TCP,這是一個問題,該問題的最終解決涉及到時間成本和金錢成本。
5.UDP真正的使用場合
5.1.網絡情況於公平性
雖然udp沒有流量控制和擁塞控制,也不需要確認,但是這並不一定會提高效率,俗話說,磨刀不誤砍柴工,有時候一些必要的維護工作是要做的,雖然udp比tcp簡潔很多,但在這簡潔的背后,其高效率的假象難免會有一些掩耳盜鈴的意味,一個udp數據報發出去就不管了,你收不到確認,於是你默認它已經安全到達對端了,這就好比你不希望代碼出錯,於是你將代碼中的錯誤提示刪除是一樣的道理。
實際上,在網絡極度擁堵的情況下,udp的丟包率極其高,這正是因為它沒有擁塞控制導致的,由於同樣沒有流量控制,在兩端速率不匹配的情況下,也會出現持續不減的高丟包率,而TCP就沒有這個問題,因為它會自我調節。
再談公平性的影響。tcp天生就是公平的,而udp不是,它是毫無秩序的,就和真正的分組交換網的定義一樣。由於沒有秩序,網絡情況絲毫不會反饋給端點,不但自身會造成高丟包率,還會擠壓tcp流量的帶寬。
用一個實例結束本節,那就是道路交通。北京交通可以看做udp,而上海交通則是tcp,雖然都面臨擁堵,但是你會發現,北京的車一旦遭遇擁堵,幾乎就卡死了,而上海雖然有的路段比北京還堵,然而不會卡死,車流即使再慢也仍會緩慢前行。
5.2.通訊持續性和交互性
對於分組交換網絡通信,協議棧的成本主要表現在以下幾方面:a.封裝導致的空間復雜度;b.緩存導致的時間復雜度。對於封裝,無疑和緩存是直接對立的,如果你想將數據馬上發出去,那么就需要直接封裝並發給下層,這樣無疑消耗了更多的協議頭空間,如果你不想如此消耗,那么就把載荷緩存,待緩存達到一定量時再一次性發出,這樣“協議頭/載荷”值將最小化,無疑節省了空間,但是浪費了時間。
以上原理理解之后,緊接着你可能會想到兩種通信類型,一類是短連接通信,一類是長連接通信。考慮通信效率的時候一定要考慮這種通信持續性所帶來的影響,如果你只需要發一個包且該包可以發后不管且自己有重發/輪詢機制,那么UDP比較好,如果此時用TCP的話,光握手就需要2個包(第三次握手可以攜帶數據),平均下來不划算,這樣的例子就是DNS查詢。反之若是長連接,那么TCP握手和揮手的額外時間會平攤到持久的通信中,在持久的通信中,應用程序可以從TCP流中得到額外的好處,比如積累發送,Nagel算法帶來的好處等等,另外,大多數情況,確認並不是一種開銷,因為很多TCP算法都是用捎帶確認或者延遲確認,因此大多數情形下確認包就不會影響發送端的速率又不會占據帶寬。
在用戶的交互體驗上,UDP協議的通信完全取決於應用程序本身的發送和接收,但是TCP協議的通信則要受到協議棧的影響,應用程序發出了的數據並不一定等於發到了網絡,比如Nagel算法就會影響實際發送。
5.3.通訊行為
你真的希望數據發出后就不管了嗎?如果不是,那就使用TCP,不要自己實現確認和連接,當然在短連接情況下,你要仔細權衡TCP握手的開銷和你自己實現的確認的開銷。
以DNS為例,這明顯不是一個發后不管的應用,但是DNS客戶端可以在一定時間沒有收到回復后,再發出另一次查詢,這並不影響最終的結果,這只是一個基礎設施類型的單點查詢任務,並不是電腦前用戶最終的任務,因此使用UDP完全可以,但是對於HTTP就不一樣了,HTTP本質上類似一次內容傳輸,然后瀏覽器解析傳來的內容並給予展示,這對格式有嚴格的要求,並且事先並不知道結果的大小,結果的格式更是不固定,因此稍有差錯就會影響到瀏覽器的解析。一次HTTP通信並不是一對一應用通信,而是牽扯到很多其它應用,比如服務器端的cgi以及客戶端的script,因此絕對需要精確傳輸,此時就不能用UDP,即使是短連接也是要用TCP的。
5.4.多點通訊
我們知道,TCP是一個有連接的通信協議,在實際傳輸前,你必須和通信目的地建立一個雙向的連接,並且只能和唯一的目的地建立連接,那么如果我們想將數據傳給多個目的地,那么我們就需要建立多個這樣的連接,在TCP之上,實現多點通信並不容易,這是TCP的握手協議以及揮手協議決定的。
對於UDP,由於沒有連接,就很好實現多點通信,對於使用了有連接的UDP,完全可以使用DNAT或者負載均衡之類的技術來實現多點通信。由於UDP協議不需要建立連接,那么完全可以向一個組播地址發送數據或者輪轉地向多個目的地持續發送相同的數據。
5.5.數據邊界
UDP是基於數據報的,這也就是說,每一個UDP包都是有邊界的,這是和流式通信最大的不同,對於TCP而言,完全按照數據本身來界定邊界,而對於UDP而言,則完全按照收發雙方每次通信的實際內容界定邊界。
5.6.即已存在的事實
我們在兩個著名的開源代碼中會遇到用UDP實現TCP的功能,它們是OpenSSL和OpenVPN,對於OpenSSL,DTLS完全是為了給基於UDP的應用提供安全保護而存在的,既然叫傳輸層安全,那就要要包容整個傳輸層(實際上,SSL協議在分層意義上跟TCP/IP的傳輸層一點關系都沒有),對於OpenVPN,是歷史原因才自己實現確認和重傳的,那時還沒有DTLS可以借鑒,這是無奈的。因此大家不要輕易把這兩個事件作為可以借鑒的事實,在借鑒一件事的時候,一定要考慮它的歷史背景,讀史使人明智,事實在於如此。
5.7.結論
因此,只有你確認以下事實的時候,使用udp才是明智的
5.7.1.你的應用完全是udp完成,或者你不在乎tcp會受到的影響
5.7.2.你對網絡狀態很熟悉,確保udp網絡中沒有氓流行為,瘋狂搶帶寬
5.7.3.通信雙方配置,負載匹配或者自己手工解決了這些問題
5.7.4.數據事實證明TCP真的比UDP更低效率或者你宗教般痴迷於UDP的高效率
5.7.5.你不在乎上述4點或者你根本不懂網絡
5.7.6.TCP實在不方便實現多點傳輸的情況