有時候,寫UDP socket程式的時候,在調用sendto或者recvfrom的時候,會發現有Connection refused錯誤返回,錯誤碼是ECONNREFUSED。對於懂得socket介面但是不很很懂網路的人,可能這根本就不是個問題,他會根據錯誤碼知道遠端沒有這個服務連接埠,正如socket api的man手冊中描述的那樣:
ECONNREFUSED
A remote host refused to allow the network connection (typically because it is not running the requested service).
有時候無知真的是一種幸福!但是如果你十分精通TCP/IP棧,那麼就想不通了,UDP既然無串連,怎麼知道遠端的情況呢?UDP不正如協議標準描述的那樣,發出去就不管了嗎?對於接收,沒有資料就一直等,如果設定了NOWAIT,則直接返回EAGAIN,表示稍後再試。不管怎麼說,也不會有ECONNREFUSED這麼詳細的資訊返回才對啊。
既然UDP不會從對端返回任何錯誤資訊,那麼一定有別的什麼返回了,總不能憑空猜測啊。這就涉及到了網路通訊協定設計中的資料平面和控制平面了,對於控制平面的訊息,可以是帶內傳輸,也可以是帶外傳輸。對於TCP而言,無疑是帶內傳輸的,因為它本身就是有串連的協議,協議本身會處理任何的錯誤和異常,然而對於UDP而言,因為其設計目的就是保持簡單性,故不再附帶有任何帶內的控制訊息邏輯,互連網上為了彌補這一類協議的控制邏輯的缺失,ICMP協議才顯得尤為重要!實際上,ICMP,根據名稱就可以看出它是一種專門的控制協議,控制和指示IP層發生的事件。
ECONNREFUSED正是ICMP返回的!然而並不是所有的UDP socket都可以享用ICMP帶來的錯誤提示,畢竟帶外控制訊息和協議本身的關聯太鬆散了。UDP socket必須顯式的connect對端才可以。現在問題又來了,既然UDP根本就是一個不需連線的協議,connect的意義何在呢?這其實是socket介面設計的範疇,和協議本身沒有任何關係,當一個UDP socket去connect一個遠端時,並沒有發送任何的資料包,其效果僅僅是在本地建立了一個五元組映射,對應到一個對端,該映射的作用正是為了和UDP帶外的ICMP控制通道捆綁在一起,使得UDP socket的介面含義更加豐滿。
我們知道,ICMP錯誤資訊返回時,ICMP的包內容就是出錯的那個未經處理資料包,根據這個未經處理資料包可以找出一個五元組,根據該五元組就可以對應到一個本地的connect過的UDP socket,進而把錯誤訊息傳輸給該socket,應用程式在調用socket介面函數的時候,就可以得到該錯誤訊息。如果一個UDP socket沒有調用過connect,那麼即使有ICMP資料包返回,由於socket保持了UDP的完整語義,協議棧也就不儲存關於該socket和對端關聯的任何資訊,因此也就無法找到一個特定的五元組將錯誤碼傳給它。
以下是一個測試程式:
#include <sys/types.h>#include <sys/socket.h>#include <string.h>#include <netinet/in.h>#include <stdio.h>#include <arpa/inet.h>#include <unistd.h>void test( int sd, struct sockaddr *addr, socklen_t len){ char buf[4]; connect(sd, (struct sockaddr *)addr, len); sendto(sd, buf, 4, 0, (struct sockaddr *)addr, len); perror("write"); sendto(sd, buf, 4, 0, (struct sockaddr *)addr, len); perror("write"); recvfrom(sd, buf, 4, 0, (struct sockaddr *)addr, len); perror("read");}int main(int argc, char **argv){ int sd; struct sockaddr_in addr; if(argc != 2) { exit(1); } bzero(&addr, sizeof(addr)); addr.sin_family = AF_INET; addr.sin_port = htons(12345); inet_pton(AF_INET, argv[1], &addr.sin_addr); sd = socket(AF_INET, SOCK_DGRAM, 0); test(sd, (struct sockaddr *)&addr, sizeof(addr)); return 0;}
編譯為UDPclient,執行./UDPclient 192.168.1.20,注意,這個地址一定要是個IP可達的地址,才好測試。按照上面的理論,結果應該是:第一個sendto成功,然後192.168.1.20返回了:
ICMP 192.168.1.20 udp port 12345 unreachable, length 40
接下來第二個sendto返回:
write: Connection refused
由於第二次沒有發送任何資料包到達192.168.1.20,所以也不能企望它返回ICMP錯誤資訊,因此接下來的recvfrom調用會阻塞。
最後的一個問題時,你不能太指望這個Connection refused以及一切帶外返回的錯誤資訊,因為你不能保證一定能收到遠端發送的ICMP包,如果中間的某個節點或者本機禁掉了ICMP,socket api調用就無法捕獲這些錯誤。