如今,但凡說精通網絡的,第二個意思就是“精通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和Open×××,對于OpenSSL,DTLS完全是為了給基于UDP的應用提供安全保護而存在的,既然叫傳輸層安全,那就要要包容整個傳輸層(實際上,SSL協定在分層意義上跟TCP/IP的傳輸層一點關系都沒有),對于Open×××,是曆史原因才自己實作确認和重傳的,那時還沒有DTLS可以借鑒,這是無奈的。是以大家不要輕易把這兩個事件作為可以借鑒的事實,在借鑒一件事的時候,一定要考慮它的曆史背景,讀史使人明智,事實在于如此。
5.7.結論
是以,隻有你确認以下事實的時候,使用udp才是明智的