天天看點

《UNIX網絡程式設計 卷1:套接字聯網API(第3版)》——8.11 UDP的connect函數

本節書摘來自異步社群《unix網絡程式設計 卷1:套接字聯網api(第3版)》一書中的第8章,第8.11節,作者:【美】w. richard stevens , bill fenner , andrew m. rudoff著,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視

在8.9節結尾我們提到,除非套接字已連接配接,否則異步錯誤是不會傳回到udp套接字的。我們确實可以給udp套接字調用connect(4.3節),然而這樣做的結果卻與tcp連接配接大相徑庭:沒有三路握手過程。核心隻是檢查是否存在立即可知的錯誤(例如一個顯然不可達的目的地),記錄對端的ip位址和端口号(取自傳遞給connect的套接字位址結構),然後立即傳回到調用程序。

給connect函數重載(overload)udp套接字的這種能力容易讓人混淆。如果使用約定,令sockname是本地協定位址,peername是外地協定位址,那麼更好的名字本該是setpeername。類似地,bind函數更好的名字本該是setsockname。

有了這個能力後,我們必須區分:

未連接配接udp套接字(unconnected udp socket),新建立udp套接字預設如此;

已連接配接udp套接字(connected udp socket),對udp套接字調用connect的結果。

對于已連接配接udp套接字,與預設的未連接配接udp套接字相比,發生了三個變化。

(1)我們再也不能給輸出操作指定目的ip位址和端口号。也就是說,我們不使用sendto,而改用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套接字不接收任何異步錯誤。

圖8-14就4.4bsd總結了上列第一點。

《UNIX網絡程式設計 卷1:套接字聯網API(第3版)》——8.11 UDP的connect函數

posix規範指出,在未連接配接udp套接字上不指定目的位址的輸出操作應該傳回enotconn,而不是edestaddrreq。

圖8-15總結了我們給已連接配接udp套接字歸納的三點。

《UNIX網絡程式設計 卷1:套接字聯網API(第3版)》——8.11 UDP的connect函數

應用程序首先調用connect指定對端的ip位址和端口号,然後使用read和write與對端程序交換資料。

來自任何其他ip位址或端口的資料報(圖8-15中我們用“???”表示)不投遞給這個已連接配接套接字,因為它們要麼源ip位址要麼源udp端口不與該套接字connect到的協定位址相比對。這些資料報可能投遞給同一個主機上的其他某個udp套接字。如果沒有相比對的其他套接字,udp将丢棄它們并生成相應的icmp端口不可達錯誤。

作為小結,我們可以說udp客戶程序或伺服器程序隻在使用自己的udp套接字與确定的唯一對端進行通信時,才可以調用connect。調用connect的通常是udp客戶,不過有些網絡應用中的udp伺服器會與單個客戶長時間通信(如tftp),這種情況下,客戶和伺服器都可能調用connect。

dns提供了另一個例子,如圖8-16所示。

《UNIX網絡程式設計 卷1:套接字聯網API(第3版)》——8.11 UDP的connect函數

通常通過在/etc/resolv.conf檔案中列出伺服器主機的ip位址,一個dns客戶主機就能被配置成使用一個或多個dns伺服器。如果列出的是單個伺服器主機(圖中最左邊的方框),客戶程序就可以調用connect,但是如果列出的是多個伺服器主機(圖中從右邊數第二個方框),客戶程序就不能調用connect。另外dns伺服器程序通常是處理客戶請求的,是以伺服器程序不能調用connect。

8.11.1 給一個udp套接字多次調用connect

擁有一個已連接配接udp套接字的程序可出于下列兩個目的之一再次調用connect:

指定新的ip位址和端口号;

斷開套接字。

第一個目的(即給一個已連接配接udp套接字指定新的對端)不同于tcp套接字中connect的使用:對于tcp套接字,connect隻能調用一次。

為了斷開一個已udp套接字連接配接,我們再次調用connect時把套接字位址結構的位址族成員(對于ipv4為sin_family,對于ipv6為sin6_family)設定為af_unspec。這麼做可能會傳回一個eafnosupport錯誤(tcpv2第736頁),不過沒有關系。使套接字斷開連接配接的是在已連接配接udp套接字上調用connect的程序(tcpv2第787~788頁)。

各種unix變體斷開套接字上連接配接的方式存在差異,同樣的方法可能适合某些系統而不适合其他系統。舉例來說,以空的套接字位址結構指針調用connect的方法僅僅适合某些系統(而在另一些系統上,要求第三個參數即套接字位址結構長度為非0)。posix規範和bsd手冊頁面在此幫助不大,隻是提到必須使用一個空位址(null address),而根本沒有提到出錯傳回值(甚至成功傳回值也沒有提到)。最便于移植的解決辦法就是清零一個位址結構後把它的位址族成員設定為af_unspec,再把它傳遞給connect。

另一個存在差異的地方是斷開連接配接前後套接字本地綁定位址的取值。aix保留被選中的本地ip位址和端口号,即使它們起源于隐式捆綁。freebsd和linux把本地ip位址設定回全0,即使早先調用過bind,端口号也保持不變。solaris在隐式捆綁時把本地ip位址設定回全0,在顯式調用過bind時保持ip位址不變。

8.11.2 性能

當應用程序在一個未連接配接的udp套接字上調用sendto時,源自berkeley的核心暫時連接配接該套接字,發送資料報,然後斷開該連接配接(tcpv2第762~763頁)。在一個未連接配接的udp套接字上給兩個資料報調用sendto函數于是涉及核心執行下列6個步驟:

連接配接套接字;

輸出第一個資料報;

斷開套接字連接配接;

輸出第二個資料報;

斷開套接字連接配接。

另一個考慮是搜尋路由表的次數。第一次臨時連接配接需為目的ip位址搜尋路由表并高速緩存這條資訊。第二次臨時連接配接注意到目的位址等于已高速緩存的路由表資訊的目的地(我們假設這兩個sendto調用有相同的目的位址),于是就不必再次查找路由表(tcpv2第737~738頁)。

當應用程序知道自己要給同一目的位址發送多個資料報時,顯式連接配接套接字效率更高。調用connect後調用兩次write涉及核心執行如下步驟:

輸出第二個資料報。

在這種情況下,核心隻複制一次含有目的ip位址和端口号的套接字位址結構,相反當調用兩次sendto時,需複制兩次。[partridge和pink 1993]指出,臨時連接配接未連接配接的udp套接字大約會耗費每個udp傳輸三分之一的開銷。

繼續閱讀