1,報文格式
報文如下,10.30.13.1往10.30.16.10的80端口發送了一個UDP報文,80端口其實監聽的是TCP。
服務器回覆了一個類型爲端口不可達的ICMP,ICMP數據部分就是請求UDP ip層及其以上的數據。
2,產生的原因
首先原因就是接收udp報文的服務器對應的端口沒有開啓UDP服務器。注意這裏的描述,並不是端口沒有開啓服務,而是沒有開啓UDP服務,如果開啓了TCP服務,照樣也會回port unreachable。
有時候,寫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和對端關聯的任何信息,因此也就無法找到一個特定的五元組將錯誤碼傳給它。
3,應用程序怎麼獲知端口不可達。
udp一般應用程序不會獲知icmp 的端口不可達信息。
爲了獲取udp端口不可達的情況,有2種方法:
1):int val = 1;
setsockopt(fd, IPPROTO_IP, IP_RECVERR , &val,sizeof(int));
2):
對udp進行connect操作,並且將sendto改成send
注:如果發送的目的ip,在當前網絡中不存在,會怎麼樣?
客戶端會先發送arp, 尋找目的主機的mac,因爲目的ip不存在,自然沒有迴應,但是sendto返回成功(sendto返回成功僅僅表示將該報文發到ip的輸出隊列中),該報文不會被髮送出去。
4,源程序
注意,阻塞情況下,recvfrom會阻塞,即使收到端口不可達消息,也會阻塞。但是經過 方法1 和 方法2後,recvfrom會返回,返回值是-1,然後 判斷errno是否是ECONNREFUSED來判斷是否收到端口不可達消息。
#include <stdio.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <string.h>
#include <errno.h>
unsigned char revc_buf[1024];
int main()
{
int fd,ret,recv_len,size=1024;
struct sockaddr_in server_addr,addr;
int val = 1;
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = inet_addr("192.168.2.254");
server_addr.sin_port = htons(77);
fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
if(fd < 0)
{
perror("socket fail ");
return -1;
}
printf("socket sucess\n");
//方法1
#if 1
setsockopt(fd, IPPROTO_IP, IP_RECVERR , &val,sizeof(int));
if(sendto(fd, "nihao", strlen("nihao"), 0, (const struct sockaddr *)&(server_addr), sizeof(struct sockaddr_in))<0)
{
perror("sendto fail ");
return -1;
}
printf("sendto sucess\n");
recv_len = recvfrom(fd, revc_buf, sizeof(revc_buf), 0, (struct sockaddr *)&addr, (int *)&size);
if (ret == -1)
{
if (errno == ECONNREFUSED)
{
printf("Recv port unreachable\n");
}
}
//方法2
#elif 0
ret = connect(fd, (const struct sockaddr *) &(server_addr), sizeof (struct sockaddr_in));
if(ret < 0)
{
printf("connect fail\n");
return -1;
}
ret = send(fd, "ni hao", strlen("nihao"),0);
if(ret < 0)
{
printf("write fail\n");
return -1;
}
ret = recvfrom(fd, revc_buf, sizeof(revc_buf), 0, (struct sockaddr *)&addr, (int *)&size);
if (ret == -1) {
if (errno == ECONNREFUSED)
{
printf("Recv port unreachable\n");
}
}
#endif
close(fd);
return 0;
}