基本UDP套接字編程


概述

使用TCP編寫的應用程序和使用UDP編寫的應用程序之間存在一些本質差異,其原因在於這兩個傳輸層之間的差別:UDP是無連接不可靠的數據報協議,非常不同於TCP提供的面向連接的可靠字節流。然而相比TCP,有些場合更適合UDP。使用UDP編寫的一些常見應用程序有:DNS(域名系統)、NFS(網絡文件系統)和SNMP(簡單網絡管理協議)。

下圖給出了典型的UDP客戶/服務器程序的函數調用。客戶不必與服務器建立連接,而是只管使用sendto函數給服務器發送數據報,其中必須指定目的地(即服務器)的地址作為參數。類似的,服務器不接受來自客戶的連接,而是只管調用recvfrom函數,等待來自某個客戶的數據到達。recvfrom將於所接受的數據報一道返回客戶的協議地址,因此服務器可以把響應發送給正確的客戶。

recvfrom和sendto函數

這個函數類似於標准的read和write函數,不過需要三個額外的參數:

#include<sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t nbytes, int flags, 
                struct sockaddr *from, socklen_t *addrlen);
                
ssize_t sendto(int sockfd, const void* buf, size_t nbytes, int flags,
                const struct sockaddr* to, sockelen_t addrlen);

前三個參數sockfd,buf和nbytes等同於read和write函數的三個參數:描述符、指向讀入或寫出緩沖區的指針和讀寫字節數。

sendto的to參數指向一個含有數據報接收者的協議地址(如IP地址和端口號)的套接字地址結構,其大小是由addrlen參數指定。recvfrom的from參數指向一個將由該函數在返回時填寫的數據報發送者的協議地址的套接字地址結構,而在該套接字結構中填寫的字節數則在addrlen參數所指的整數中返回給調用者。注意,sendto的最后一個參數是一個整數值,而recvfrom的最后一個參數是一個指向整數值的指針(即值-結果參數)

recvfrom的最后兩個參數類似於accept的最后兩個參數:返回時其中套接字的地址結構告訴我們是誰發送了數據報(UDP情況下)或是誰發起了連接(TCP情況下)。sendto的最后兩個參數類似於connect的最后兩個參數:調用時其中套接字的地址結構被我們填入數據報將發往(UDP情況下)或與之建立連接(TCP情況下)的協議地址。

這兩個函數都把所讀寫的數據的長度作為函數返回值。在recvfrom使用數據報協議的典型用途中,返回值就是所接收數據報中的用戶數據量。

寫一個長度為0的數據報是可行的。在UDP情況下,這會形成一個只包含一個IP首部(對於IPV4通常為20字節,對於IPV6通常是40字節)和一個8字節的UDP首部而沒有數據的IP數據報。這也意味着對於數據報協議,recvfrom返回0值是可以接受的:它不像TCP套接字上read返回0值那樣表示對端已關閉連接。既然UDP是無連接的,因此也就沒有諸如關閉一個UDP連接之類的事情。

如果recvfrom的from是一個空指針,那么相應的長度參數(addrlen)也必須是一個空指針,表示我們並不關心數據報發送者的協議地址。

UDP回射服務器

一般來說,大多數TCP服務器是並發的,而大多數UDP服務器是迭代的。

事實上每個UDP套接字都有一個接收緩沖區,到達該套接字的每個數據報都進入這個套接字的接收緩沖區。這樣,在進程能夠讀該套接字任何已排好隊的數據報之前,如果有多個數據報到達該套接字,那么相繼到達的數據報僅僅加到該套接字的接收緩沖區中。然而這個緩沖區的大小是由限制的

UDP的connect函數

可以給UDP套接字調用connect,然而這樣做的結果與TCP連接大相徑庭:沒有三路握手過程。內核只是檢查是否存在立即可知的錯誤(例如一個顯然不可達的目的地),記錄對端的IP地址和端口號(取自傳遞給connect的套接字地址結構),然后立即返回到調用進程。

對於已連接UDP套接字(調用connect后),與默認的未連接UDP套接字相比,發生了三個變化:

  1. 我們再也不能給輸出操作指定目的IP地址和端口號。也就是說,我們不使用sento,而改用write或send。寫到已連接UDP套接字上的任何內容都自動發送到由connect指定的協議地址(如IP地址和端口號)。

     其實我們可以給已連接UDP套接字調用sendto,但是不能指定目的地址。sendto的第五個參數(指向指明目的地址的套接字地址結構的指針)必須為空指針,第六個參數(該套接字地址結構的大小)應該為0。POSIX規范指出當第五個參數為空指針時,第六個參數的取值就不再考慮。
    
  2. 我們不必使用recvfrom以獲悉數據報的發送者,而改用read、recv或recvmsg。在一個已連接UDP套接字上,由內核為輸入操作返回的數據報只有那些來自connect所指定協議地址的數據報。目的地為這個已連接UDP套接字的本地協議地址(如IP地址和端口號),發源地卻不是該套接字早先connect到的協議地址的數據報,不會投遞到該套接字。這樣就限制一個已連接UDP套接字能且僅能與一個對端交換數據報。

     確切地說,一個已連接UDP套接字僅僅與一個IP地址交換數據報,因為connect到多播或廣播地址是可能的。
    
  3. 由已連接UDP套接字引發的異步錯誤會返回給它們所在的進程,而未連接UDP套接字不接收任何異步錯誤。

應用進程首先調用connect指定對端的IP地址和端口號,然后使用read和write與對端進程交換數據。

來自任何其他IP地址或端口的數據報(上圖中的"???"表示)不投遞給這個已連接套接字,因為它們要么源IP地址要么源UDP端口不與該套接字connect到的協議地址相匹配。這些數據報可能投遞給同一個主機上的其他某個UDP套接字。如果沒有相匹配的其他套接字,UDP將丟棄它們並生成相應的ICMP端口不可達錯誤。

給一個UDP套接字多次調用connect

擁有一個已連接UDP套接字的進程可出於下列兩個目的之一再次調用connect:

  • 指定新的IP地址和端口號
  • 斷開套接字

第一個目的(即給一個已連接UDP套接字指定新的對端)不同於TCP套接字中connect的使用:對於TCP套接字,connect只能調用一次。

為了斷開一個已連接UDP套接字,我們再次調用connect時把套接字地址結構的地址族成員(對於IPv4為sin_family,對於IPv6為sin6_family)設置為AF_UNSPEC。這么做可能會返回一個EAFNOSUPPORT錯誤,不過沒有關系。使套接字斷開連接的是在已連接UDP套接字上調用connect的進程。

性能

當應用進程在一個為連接的UDP套接字上調用sendto時,源自Berkeley的內核暫時連接該套接字,發送數據報,然后斷開該連接。在一個未連接UDP套接字上給兩個數據報調用sendto函數於是涉及內核執行下列6個步驟:

  1. 連接套接字

  2. 輸出第一個數據報

  3. 斷開套接字

  4. 連接套接字

  5. 輸出第二個數據報

  6. 斷開套接字連接

     另一個考慮是搜索路由表的次數。第一次臨時連接需為目的IP地址搜索路由表並高速緩存這條信息。第二次臨時連接注意到目的地址等於已高速緩存的路由表信息的目的地(我們假設這兩個sendto調用相同的目的地址),於是就不必再次查找路由表。
    

當應用進程知道自己要給同一目的地址發送多個數據報時,顯示連接套接字的效率更高。調用connect后調用兩次write涉及內核執行以下步驟:

  1. 連接套接字
  2. 輸出第一個數據報
  3. 輸出第二個數據報

在這種情況下,內核只復制一次含有目的IP地址和端口號的套接字地址結構,相反當調用sendto時,需要賦值兩次。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM