UDP協(xié)議與TCP協(xié)議的比較
UDP協(xié)議是無面向連接的、不可靠的、無序的、無流量控制的傳輸層協(xié)議,UDP發(fā)送的每個數(shù)據(jù)報是記錄型的數(shù)據(jù)報,所謂的記錄型數(shù)據(jù)報就是接收進程可以識別接收到的數(shù)據(jù)報的記錄邊界。TCP協(xié)議是面向連接的、可靠的、有序的、擁有流量控制的傳輸層協(xié)議,它是字節(jié)流的協(xié)議,無記錄邊界。
1.記錄與字節(jié)流
UDP協(xié)議:發(fā)送進程在發(fā)送每個數(shù)據(jù)報的時候并不等待多個數(shù)據(jù)報集中在一起以一個較大數(shù)據(jù)報發(fā)送出去,而是立即發(fā)送出去,它是記錄型的協(xié)議。并且接收進程每次通過read或recv……獲得的數(shù)據(jù)報必定是發(fā)送進程所發(fā)送的那個數(shù)據(jù)報不可能是多個數(shù)據(jù)報,接收進程可以識別到發(fā)送進程所發(fā)送的每個數(shù)據(jù)報的記錄邊界。
TCP協(xié)議:發(fā)送進程在發(fā)送每個數(shù)據(jù)報的時候在內(nèi)核處理過程中有可能并不立即發(fā)送出去,而是會將多個數(shù)據(jù)報集中在一起以一個較大的數(shù)據(jù)報來發(fā)送,它是字節(jié)流的協(xié)議。而接收進程每次通過read來讀取發(fā)送進程發(fā)送過來的數(shù)據(jù)報并不一定是發(fā)送進程原先發(fā)送數(shù)據(jù)報,接收進程無法識別每個數(shù)據(jù)報的記錄邊界,所以TCP協(xié)議就是字節(jié)流的、無記錄邊界的協(xié)議。
例如:QQ聊天所用到的協(xié)議就應該是有記錄邊界的,聊天過程中是以“消息”為單位,消息可以看成一個記錄,所以QQ聊天協(xié)議采取UDP協(xié)議而不是TCP協(xié)議。
2.有序與無序
UDP協(xié)議:發(fā)送進程所發(fā)送的每個數(shù)據(jù)報并不按照原先發(fā)送的順序到達接收進程,有可能早發(fā)送的數(shù)據(jù)報較后到達接收進程。因為數(shù)據(jù)報在經(jīng)過中間路徑的傳送時會因為各個數(shù)據(jù)報傳送的路徑不同或者其它原因而造成這些數(shù)據(jù)報到達的順序不同,UDP協(xié)議是無序的傳輸協(xié)議。所以為了使基于UDP協(xié)議的應用程序有序,必須在應用程序中設置序號、確認機制來使其有序。
TCP協(xié)議:有序協(xié)議,有超時、序號、重傳、確認機制。
例如:FTP協(xié)議是用于傳送文件的協(xié)議,為了確保在傳送文件內(nèi)容的時候,傳送的每個數(shù)據(jù)報協(xié)議有序接收,所以FTP協(xié)議是基于TCP協(xié)議。
那為什么TFTP協(xié)議是基于UDP協(xié)議?因為為了保證有序,TFTP協(xié)議中引入了確認、序號字段。
這里還有一個問題,F(xiàn)TP協(xié)議中的控制連接傳送的內(nèi)容好像都是基于消息形式,客戶端在控制連接上發(fā)出一個請求消息,服務器端返回一個請求結果消息,感覺應該FTP控制連接采取UDP協(xié)議,為什么采取TCP協(xié)議?因為控制連接上是交互式的消息傳送,客戶端在發(fā)送一個請求之后,在服務器端的響應消息未到達之前,客戶端是不會發(fā)送第二個請求消息,所以不用擔心這兩個請求消息會疊加在一起。也就是對于交互式的消息傳遞也可以采用TCP協(xié)議。
3.流量控制
UDP協(xié)議:沒有流量控制機制,如果發(fā)送進程發(fā)送數(shù)據(jù)報塞滿了接收進程的接收緩沖區(qū),就會丟棄數(shù)據(jù)報。出現(xiàn)這種情況,UDP協(xié)議不會通知發(fā)送進程減緩數(shù)據(jù)的發(fā)送速率。
TCP協(xié)議:擁有流量控制。
4.客戶端通信過程比較
4.1 客戶端的連接過程比較
UDP協(xié)議在創(chuàng)建插口之后,可以同多個服務器端建立通信,而TCP協(xié)議只能與一個服務器端建立通信,TCP不允許目的地址是廣播或多播地址,UDP允許。UDP協(xié)議客戶端同服務器端的通信關系可以是一對多的關系,而TCP協(xié)議只能是一對一的關系。
當然UDP協(xié)議也可以像TCP協(xié)議一樣,通過connect來指定對方的ip地址、端口(對應下圖1中的③操作),connect是插口連接操作,connect操作之后代表對應的插口已連接,與TCP協(xié)議不同,UDP的connect實現(xiàn)不包含三向握手。不管是UDP協(xié)議還是TCP協(xié)議,connect實現(xiàn)的共同部分都包括:若所指定插口的本地地址、端口未指定,那么connect的時候由內(nèi)核為其指定本地地址、本地端口,內(nèi)核根據(jù)插口中的目的地址來判斷外出接口,然后指定該外出接口的IP地址為插口的本地地址。UDP協(xié)議通過connect操作之后同服務器端的通信關系成為一對一關系,不再是一對多的關系,而且這時也不能指定目的地址為廣播或多播地址,因為connect函數(shù)不允許目的地址為廣播或多播地址。UDP協(xié)議經(jīng)過 connect之后,在通過sendto來發(fā)送數(shù)據(jù)報時不需要指定目的地址、端口,如果指定了目的地址、端口,那么會返回錯誤。通過UDP協(xié)議可以給同一個插口指定多次connect操作,而TCP協(xié)議不可以,TCP只能指定一次connect操作。UDP協(xié)議指定第二次connect操作之后會先斷口第一次的連接,然后建立第二次的連接。
客戶端在建立同服務器端的連接過程中,第一步都會通過socket建立連接套接字,然后通過bind來綁定本地地址、本地端口,當然綁定操作可以不用指定。
UDP協(xié)議:若未指定綁定操作,那么可以通過下面connect操作來由內(nèi)核負責插口的綁定操作,若connect又未指定,那么綁定操作只好通過插口的寫操作(sendto、sendmsg)來指定目的地址、端口,這時插口本地地址不會指定,為通配地址,而本地端口由內(nèi)核指定,第一次sendto 操作之后,插口的本地端口經(jīng)過內(nèi)核指定之后就不會更改。
TCP協(xié)議:若未指定綁定操作,可以通過下面connect操作來由內(nèi)核負責插口的綁定操作。內(nèi)核會根據(jù)插口中的目的地址來判斷外出接口,然后指定該外出接口的IP地址為插口的本地地址。Connect操作對于TCP協(xié)議的客戶端是必不可少的,必須指定。
(不管是UDP協(xié)議還是TCP協(xié)議,所對應插口經(jīng)過connect操作之后就是已連接的插口,未經(jīng)過connect就代表未連接的插口。)
通過bind來綁定本地地址、本地端口的時候,不管是已連接的還是未連接的插口,如果存在某一個插口的本地端口同用戶所要綁定的本地端口相同,都會返回EADDRINUSE(Address already in use)錯誤。如果要綁定同已存在插口的本地端口相同的端口,必須先設置插口選項SO_REUSEADDR,然后再綁定。在linux系統(tǒng)中如果綁定的本地地址不同而本地端口相同可以不用設置插口選項SO_REUSEADDR,而對于其它的類UNIX系統(tǒng)根據(jù)《unix網(wǎng)絡編程》中所描述的都要預先設置 SO_REUSEADDR插口選項。
對于TCP協(xié)議絕不允許綁定的本地地址、端口同已存在的插口(不管是已連接的還是未連接的插口)相同。對于UDP協(xié)議通過設置插口選項 SO_REUSEPORT,允許綁定相同的本地地址、本地端口。在linux系統(tǒng)中,沒有SO_REUSEPORT這個選項,所以在linux系統(tǒng)中 UDP協(xié)議同TCP協(xié)議一樣都不允許存在兩個插口有相同的本地地址、本地端口。
TCP協(xié)議同UDP協(xié)議還有一個很大的不同點:例如有一臺多宿主機,它所擁有的IP地址有A、B、C,現(xiàn)在創(chuàng)建4個相同的TCP監(jiān)聽端口port,對應的四個插口地址結構(*,port)、(A,port)、(B,port)、(C,port),現(xiàn)在有客戶端要同(A,port)建立連接,那么只會同(A,port)插口建立連接,而不會同擁有通配地址*的插口建立連接。而如果是創(chuàng)建4個相同的UDP監(jiān)聽端口port,對應的四個插口地址結構(*,port)、(A,port)、(B,port)、(C,port),那么有客戶端要同(A,port)建立通信,那么發(fā)送到(A,port)的數(shù)據(jù)報也會拷貝一份到(*,port)插口。原因:TCP協(xié)議之間的通信是一對一的關系,而UDP可以是一對多的關系。
步驟 |
UDP協(xié)議 |
TCP協(xié)議 |
① |
socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP) |
socket(AF_INET,SOCK_STREAM,IPPROTO_TCP) |
② |
bind捆綁本地地址、本地端口(可忽略,在下面connect中或第一次sendto由內(nèi)核來指定) |
bind捆綁本地地址、本地端口(可忽略,在下面的connect操作中由內(nèi)核來指定本地地址、端口) |
③ |
connect指定對方地址、端口,建立連接(可忽略由sendto指定對方地址、端口) |
connect指定對方地址、端口,建立連接 |
④ |
讀或寫操作 |
讀或寫操作 |
圖1 客戶端連接過程
4.2 服務器端的連接過程比較
對于UDP協(xié)議客戶端與服務器端沒有什么本質(zhì)的區(qū)別,每個UDP協(xié)議的客戶端也是服務器端。而TCP協(xié)議就不同了,TCP協(xié)議必須通過listen 來申請監(jiān)聽,然后通過accept來接收一個客戶端的連接,當接收客戶端的連接會再創(chuàng)建一個單獨的插口用來同客戶端之間進行數(shù)據(jù)通信,也就是說服務器端由一個單獨的監(jiān)聽插口負責監(jiān)聽客戶端的連接請求,當接收到一個來自客戶端的連接請求之后,服務器會另外創(chuàng)建一個插口負責同客戶端之間進行連接通信。
服務端在通過bind來綁定本地地址、本地端口的時候應注意的情況,同客戶端是相同的。
步驟 |
UDP協(xié)議 |
TCP協(xié)議 |
① |
socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP) |
socket(AF_INET,SOCK_STREAM,IPPROTO_TCP) |
② |
bind捆綁本地地址、本地端口(可忽略,在下面connect中或第一次sendto由內(nèi)核來指定) |
bind捆綁本地地址、本地端口(可忽略,在下面listen操作中由內(nèi)核來指定本地地址、本地端口) |
③ |
connect指定對方地址、端口,建立連接(可忽略由sendto指定對方地址、端口) |
listen監(jiān)聽客戶端的連接 |
④ |
讀或寫操作 |
accept接受客戶端的連接 |
⑤ |
|
讀或寫操作 |
圖2 服務器端連接過程