UNP-UNIX網絡編程 第十一章:名字與地址轉換

(一) 域名系統(DNS)

DNS主要用於主機名和IP地址之間的映射。主機名可以是簡單的名字ljm,也可以是全限定域名ljm.localdomainbaidu.com等。

1.資源記錄

DNS中的條目稱爲資源記錄(RR)。我們感興趣的RR類型只有幾個:
A A記錄把一個主機名映射爲一個32位的IPv4地址。
AAAA 4A記錄把一個主機名映射爲一個128位的IPv6地址。
ljm IN A 127.0.0.1
IN AAAA 3ffe:1f8d:9bc3:1234:ef93:ac89
PTR 稱爲指針記錄。把IP地址映射爲主機名。對IPv4地址,32位的4個字節先反轉順序,再添加in-addr.arpa。
對於IPv6地址,128位地址每四位組先反轉,再添加ip6.arpa。
例如上面主機ljm的兩個PTR記錄爲:1.0.0.127.in-addr.arpa和8.9.c.a.3.9.f.e….ip6.arpa

2. 解析器和名字服務器

1> 名字服務器。
每個組織機構都會有一個名字服務器(有時也叫DNS服務器)。它們保存着主機名和IP地址之間的映射的資源記錄。
2> 解析器
客戶端和服務器端等應用程序通過解析器的函數來對主機名或IP地址進行解析。
典型的解析器函數爲gethostbyname和gethostbyaddr。前者把主機名映射爲IPv4地址,後者相反。
3> 具體過程
當我們需要解析某個主機名或IP地址時,我們在代碼中調用解析器函數,則解析器函數就會根據解析器配置文件(/etc/resolv.conf)
中的本地名字服務器的IP地址,去發出UDP查詢,如果找不到,則會在整個因特網上查詢其他名字服務器。
如果答案太長,則本地解析器會自動切換到TCP。

(二)gethostbyname和gethostbyaddr函數

1. gethostbyname函數

從主機名到IPv4地址的映射。
該函數執行的只是查詢A記錄,所以即使主機有IPv6地址,也不會返回。

#include     <netdb.h>  
struct hostent* gethostbyname(const char* hostname);  
//返回:若成功返回非NULL指針,若出錯返回NULL並設置h_errno 

1> 結構體hostent:

struct hostent
{  
 char* h_name;      //規範名字,也可以理解爲全限定域名。  
 char** h_aliases;  //主機別名,一個主機可能有多個主機別名,所以這裏是二維char數組。  
 int h_addrtype;    //地址族:AF_INET  
 int h_length;      //地址長度:4  
 char** h_addr_list;//IPv4地址  
}; 

1>地址族和地址長度,由於這裏只能是IPv4的地址映射,所以這兩個值不會改變,既然不會改變,爲何需要這兩個參數?感覺有點多餘。
h_addr_list,主機可能會有多個IP地址,所以這裏是二維數組。這裏的IP地址顯然是二進制型的,如要輸出也需轉換表達式型。

2>當gethostbyname返回錯誤時:
這裏當函數發送錯誤時,不是設置errno,而是設置h_errno。一般我們使用函數hstrerror來解析這個h_errno錯誤值。
const char* hstrerror(int h_errno);

3> 我們來寫一個例子程序,輸入任意多個主機名,輸出每個主機名對應的規範名字,別名,IP地址。

    #include "unp.h"  
    int main(int argc, char** argv)  
    {  
        char* ptr, ** pptr;  
        char buff[4];  
        struct hostent * hptr;  
        while(--argc>0)  
        {  
            ptr=*++argv;  
            if((hptr=gethostbyname(ptr))==NULL)  
            {//給每個命令行參數調用gethostbyname()  
                printf("error for host : %s : %s\n", ptr,hstrerror(h_errno));  
                continue;  
            }  
            printf("host name : %s\n",ptr); //輸出 host name
            for(pptr=hptr->h_aliases;*pptr!=NULL;pptr++)  
                 printf("aliases: %s\n",*pptr); //輸出 aliases
            switch(hptr->h_addrtype)  
            {//pptr指向一個指針數組。每個指針指向一個地址,對每個地址調用inet_ntop並輸出返回的字符串  
            case AF_INET:  
                for(pptr=hptr->h_addr_list;*pptr!=NULL;pptr++)  
                printf("aliases: %s\n",Inet_ntop(hptr->h_addrtype, *pptr, buff, sizeof(buff)));  
                break;  
            default:  
                printf("unknown address type\n");  
                break;  
            }  
        }  
    }  

2. gethostbyaddr函數。

與上面的函數作用相反,從一個二進制的IPv4地址轉換到相應的主機名。

#include     <netdb.h>  
struct hostent* gethostbyaddr(const char* addr, socklen_t len, int family);// len爲4,family爲AF_INET  
//返回:若成功返回非NULL指針,若出錯返回NULL並設置h_errno

函數查詢的是in_addr.arpa消息記錄。
函數返回的是和gethostbyname返回一樣的結構體,只是這裏我們只關心結構體中的h_name。

(三)getservbyname和getservbyport函數

上面提到我們可以用主機名來代替IP地址,下面我們可以使用服務名來代替端口號。

0. 像主機一樣:可以使用主機名和IP地址來標識一臺主機。

服務:可以使用服務名和端口號來標識一個服務。
一個服務可以支持多個協議(TCP,UDP,SCTP),就像一個進程可以同時監聽TCP套接字和UDP套接字一樣。一般一個服務只有一個端口號如http的80服務名和端口號的映射保存在一個文件(/etc/services)中。這樣即使端口號發生變動,我們只需要修改文件(/etc/services)即可。而不需要重新編譯程序。看一下/etc/services的內容:vim /etc/services

1. getservbyname函數:從服務名映射到端口號。

struct servent* getservbyname(const char* servname, const char* protoname);  
//返回:如果成功返回非空指針,如果失敗返回NULL  

此函數就是查找本地的/etc/services裏面的內容,上面可知,/etc/services每一行有三個東西,
service-name,port,protocol。所以本函數提供了兩個參數來唯一標識端口號。函數返回的結構體servent:

struct servent
{  
    char*  s_name;  //服務名
    char** s_aliases; //別名 
    int    port;  //端口名
    char*  s_proto; //protocol 
};  

這裏我們只關心port,注意返回的是網絡字節序的,所以給sockaddr_in賦值時,無需再去轉換字節序。
這裏函數的第二個參數可以是NULL,此時返回哪個端口號取決於實現,但一般無所謂,因爲一個服務不同的協議,通常對應一個端口號。
如果指定protoname,則該協議必須支持。

ser=getservbyname("tcpmux","tcp");//ok  
ser=getservbyname("tcpmux","sctp");//error  

2. getservbyport函數:從端口號到服務名的映射

    struct servent* getservbyport(int port, const char* protoname);  
    //返回:如果成功返回非空指針,如果失敗返回NULL  

注意這裏的port參數必須是網絡字節序的。

    struct servent* ser;  
    ser=getservbyport(htons(1),"tcp");//ok  
    ser=getservbyport(htons(1),NULL);//ok  
    ser=getservbyport(htons(1),"sctp");//error  

之前的程序我們是用IP地址和端口號來標識一個目標主機上的進程的。現在,我們就可以使用主機名和服務名來標識一個目標主機上的進程。

(四)我們把我們之前的獲取時間客戶端改爲使用主機名和服務名來標識。

names/daynametcpcli1.c

#include    "unp.h"

int
main(int argc, char **argv)
{
    int                 sockfd, n;
    char                recvline[MAXLINE + 1];
    struct sockaddr_in  servaddr;
    struct in_addr      **pptr;
    struct in_addr      *inetaddrp[2];
    struct in_addr      inetaddr;
    struct hostent      *hp;
    struct servent      *sp;

    if (argc != 3)
        err_quit("usage: daytimetcpcli1 <hostname> <service>");

    if ( (hp = gethostbyname(argv[1])) == NULL) {
        if (inet_aton(argv[1], &inetaddr) == 0) {
            err_quit("hostname error for %s: %s", argv[1], hstrerror(h_errno));
        } else {
            inetaddrp[0] = &inetaddr;
            inetaddrp[1] = NULL;
            pptr = inetaddrp;
        }
    } else {
        pptr = (struct in_addr **) hp->h_addr_list;
    }
    if ( (sp = getservbyname(argv[2], "tcp")) == NULL)
        err_quit("getservbyname error for %s", argv[2]);
/*---------------------------------------------------------------*/
    for ( ; *pptr != NULL; pptr++) 
    {
        sockfd = Socket(AF_INET, SOCK_STREAM, 0);

        bzero(&servaddr, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = sp->s_port;
        memcpy(&servaddr.sin_addr, *pptr, sizeof(struct in_addr));
        printf("trying %s\n",
               Sock_ntop((SA *) &servaddr, sizeof(servaddr)));
/*---------------------------------------------------------------*/
        if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) == 0)
            break;      /* success */
        err_ret("connect error");
        close(sockfd);
    }
/*---------------------------------------------------------------*/
    if (*pptr == NULL)
        err_quit("unable to connect");
/*---------------------------------------------------------------*/
    while ( (n = Read(sockfd, recvline, MAXLINE)) > 0) {
        recvline[n] = 0;    /* null terminate */
        Fputs(recvline, stdout);
    }
    exit(0);
}

這裏我們從命令行輸入參數:服務器主機名和服務名.
我們調用gethostbyname來獲得服務器IP地址列表,一個一個嘗試連接。
如果連接失敗,要關閉套接字,然後重新socket,connect。不能直接重connect。
然後我們調用getservbyname來獲得端口號,這裏的端口號是衆所周知的端口號,因爲該函數是查看本機的/etc/services,來獲知端口號的。而服務器主機也是利用這個衆所周知的端口號服務名進行bind的。

(五)getaddrinfo函數:名->號

因爲gethostbyname和gethostbyaddr這兩個函數只適用於IPv4地址。所以有了既支持IPv4和IPv6的函數getaddrinfo。
getaddrinfo函數能夠處理IP地址和主機名的轉換,而且還能同時處理端口號和服務名的映射。

int getaddrinfo(const char* hostname, const char* service, const struct addrinfo* hints, struct addrinfo **result);  
//返回:成功返回0,失敗返回非0.  

1. hostname爲輸入的主機名,service爲輸入服務名,hints爲需要返回結果的提示信息,

該信息會影響返回結果,可以是NULL。Result爲函數返回的結果。

2. 說一下爲何返回結果result前有兩個**,該函數返回一個鏈表,*result指向鏈表的頭結構,注意此時我們是讓函數內部

開闢內存空間,調用函數者只需提供一個指針,在函數內部修改這個指針本身,而不是要修改這個指針指向的東西。

struct addrinfo
{  
 int ai_flags;//AI_PASSIVE, AI_CANONNAME  
 int ai_family;//AF_XXX  
 int ai_socktype;//SOCK_XXX  
 int ai_protocol;//0 or IPPROTO_XXX  
 socklen_tai_addrlen;//length of ai_addr  
 char* ai_canonname;//ptr to canonical name for host  
 struct sockaddr* ai_addr;//ptr to socket address  
 struct addrinfo* ai_next;//ptr to next struct in linked list  
};  

這裏前4個成員是給hints參數設置的。後4個成員是result返回的結果。因爲前4個成員的不同值會影響後4個成員的值。
1> ai_flags:一般的標識值爲AI_PASSIVE和AI_CANONNAME
AI_PASSIVE:套接字將用於被動打開。
AI_CANONNAME:告知函數返回主機的規範名字。即ai_canonname
2> ai_family:即返回的是主機名對應的IPv4地址還是IPv6地址。
3> ai_socktype:即因爲服務名可能對應多個協議,所以這裏指定返回TCP還是UDP
4> ai_protocol:指定具體的協議,當ai_socktype不能唯一標識特定的協議時,就要用到此參數。
即因爲SCTP也屬於流協議,其socktype也是SOCK_STREAM,所以如果某個服務支持TCP和SCTP,則我們就必須指明協議名。即:
IPPROTO_TCP或IPPROTO_UDP。

3 .如果hints爲NULL,則ai_flags,ai_socktype,ai_protocol的值均爲0,ai_family的值爲AF_UNSPEC。

4. 該函數返回是一個鏈表,爲何?

當提供的主機名有多個IP地址時,每個IP都會返回一個對應的結構。
當未指明ai_socktype時,提供的服務名支持多個協議,則每個協議返回一個結構。
所以當主機名有2個IP地址,服務名支持tcp和udp,且未指明ai_socktype,則就會返回2*2=4個結構。
返回的鏈表的結構體的順序是不固定。

5. 返回的結構體中的ai_addr可直接用於socket,connect函數調用。因爲其IP地址都是二進制型的,且IP地址的類型爲sockaddr,和協議無關的。且IP地址和端口號都是網絡字節序的,所以無需任何轉換函數。

6. 函數getaddrinfo的一些常見的輸入

函數有六個可輸入的參數值:hostname,,service,以及hints的前4個成員。
(1) 客戶端
對於TCP和UDP客戶端而言,我們使用該函數,用來創建連接,連接服務器的。所以我們需要指定hostname和service的值,而至於hints的4個成員,如果確認知道自己處理的是哪一類型套接字,應該指定ai_socktype或和ai_protocol。一般ai_flags爲AI_PASSIVE。而一般不指定ai_family,因爲主機有可能用於IPv4和IPv6地址,我們需要一個一個嘗試socket->connect。

(2) 服務器端
對於服務器,我們使用該函數,只是用來指定端口號的。所以我們一般不指定主機名,只指定service。而主機名爲空,則返回的套接字地址中的IP地址爲通配地址,也正是我們想要的。

注意如果不指定ai_family或指定爲AF_UNSPEC,這時至少返回兩個結構,一個包含Ipv4的通配地址INADDR_ANY,另一個包含IPv6的通配地址IN6ADDR_ANY_INIT。這時我們可以使用select來監聽這兩個套接字。
對於hints的前4個成員,我們指定ai_flags爲AI_PASSIVE。且應該指定套接字的類型,以防止返回多個結構。

注意:因爲我們後續需要調用accept函數來獲得套接字,我們就需要先new一個套接字結構,這個套接字結構的大小如何確定?
函數返回的鏈表中,addrinfo的每個結構體中都會有其套接字結構的大小ai_addrlen。所以我們根據這個值可以確認套接字的大小。

7. 可以說,getaddrinfo函數功能很強大,但使用起來也很複雜。. 如果hints爲NULL,則ai_flags,ai_socktype,ai_protocol的值均爲0,ai_family的值爲AF_UNSPEC。

(七)freeaddrinfo函數:

函數getaddrinfo函數返回一個結構體鏈表,其所有的存儲空間都是動態獲取的,如new或malloc。
所以我們用完之後要釋放這些內存。就是調用freeaddrinfo來釋放的。

    void freeaddrinfo(struct addrinfo* ai);  

典型的用法:

    struct addrinfo* result;  
    intret=getaddrinfo(...,&result);  
    freeaddrinfo(result);  

這樣就可以釋放返回的整個鏈表。
但是這有個問題,比如我們通過遍歷找到了我們所需的結構,然後把這個結構體複製出來,然後調用該函數釋放所有內存。

但是:如果只複製這個結構體本身的話,是有問題的,因爲這個結構體內有指針(套接字結構指針和規範名字指針),複製的時候切記把這些指針指向的空間也要複製。不然只複製指針的話,則freeaddrinfo會釋放掉所有內存,這樣我們複製出來的這個結構體的指針指向的是已釋放的內存,這樣很危險。所以複製結構體時,切記需要深度複製。

(八) 一些實際的例子

1. 對於TCP客戶端:

提供確認的主機名和服務名,然後對於服務器的每個IP地址,進行:

    while(){  
    socket->connect  
    }  

2. 對於TCP服務器端:

一般不提供主機名,而是提供服務名,然後對於本地的每個類型的通配地址以及服務名,進行:

    while(){  
    socket->bind  
    }  
    accept()  
    …  

如果bind成功,則跳出循環。有點問題,這樣只能綁定一個IP地址:或爲IPv4的通配地址,或爲IPv6的通配地址。

(九)getnameinfo函數:該函數爲getaddrinfo的互補函數,即提供套接字地址,返回主機名和服務名。

int getnameinfo(const structsockaddr* addr, socklen_t addrlen, 
                            char*hostname, socklen_t hostlen, 
                            char* serv,socklen_t servlen, int flags);  
//返回:成功返回0,失敗返回非0  

各個參數都很明顯,只有最後一個flag,其用於指示一些東西。比如:
NI_DGRAM:指示返回的服務是基於UDP的,因爲有可能端口號相同的不同協議對應的是不同的服務。
其他的一些標誌值:p267
注意1:這裏可以把標記值進行或,然後這樣就可以同時設置兩個標誌值。
注意2:getaddrinfo和getnameinfo都是設計DNS的,而一般服務器是不使用getnameinfo函數的,
直接用IP地址來標識就可以了,因爲getnameinfo設計DNS,其是很耗時的。

(十) 可重入函數

1. 我們先來看看gethostbyname和gethostbyaddr的代碼:

    static hostent host;  
    struct hostent* gethostbyname(const char* hostname)  
    {  
        /*.....*/  
        return (&host);  
    }  
    struct hostent* gethostbyaddr(const char* addr,socklen_t len, int family)  
    {  
        /*.....*/  
        return (&host);  
    }  

可以看到函數返回的都是一個static的對象。問題就在這上面。
假如我們在一個主程序中調用gethostbyname函數,且在信號處理函數中也調用gethostbyname,看看會發生什麼:

main()  
{  
struct hostent* hp;  
   ...  
signal(SIGALRAM,sig_alrm);  
   ...  
    hp=gethostbyname(...);  
}  
void sig_alrm(intsigno)  
{  
struct hostent* hp1;  
hp1=gethostbyname(...);  
}  

假如此時主程序中執行到函數gethostbyname期間,而且函數已經處理好static的host對象了,準備返回了,此時來了個信號,則主程序中斷,去處理這個信號,而在信號處理函數中重新調用了gethostbyname,那麼該host對象將被重用,因爲此時只有一個進程,只保留一個副本,這麼一來,原先由主程序計算出的值被重寫成了信號處理函數調用計算出的值。則這樣就會產生錯誤。這樣就是不可重入函數。

2. 看看以前的函數可重入性:

1> gethostbyname、gethostbyaddr、getservbyname、getservbyport都是不可重入函數(返回的是static的結構指針)
2> inet_pton、inet_ntop都是可重入的。
3> getaddrinfo可重入的前提是調用的函數都是可重入的,比如函數內調用的是gethostbyname可重入版本,getservbyname的可重入版本。
4> getnameinfo可重入的前提是調用的函數都是可重入的,比如函數內調用的是gethostbyaddr可重入版本,getservbyport的可重入版本。

3. errno變量也有類似的問題。

首先,每個進程有一個errno的副本。而在同一個進程中,例如如下代碼:

    if(close(fd)<0)  
    {  
        fprintf(stderr,"close error, errno= %s\n", errno);  
    }  

如果此時close函數產生錯誤,則內核設置errno,當程序調用結束close時,還未來得及執行輸出時,此時信號來了,信號處理函數中也產生了錯誤,則errno被重置,則返回到主程序時,就有問題了。

4. 解決可重入性問題的一個方法:

就是在信號處理函數中永遠不要調用不可重入函數,且可把errno先進行保存,然後在函數最後還原回來。
如:

void sig_handler(int signo)  
{  
    int errno_save=errno;  
    /*...other code*/  
    errno=errno_save;  
}

還有在信號處理函數中,不要調用標準I/O函數,如fprintf等。因爲很多版本實現的標準I/O函數都是不可重入的。

(十一)gethostbyname_r和gethostbyaddr_r函數(可重入版本)

首先,把不可重入函數改爲可重入函數有兩種方法:
1> 不可重入函數的問題在於返回一個全局static對象。而我們可以由調用者動態開闢一個對象空間,然後交由函數進行修改。如gethostbyname,我們可以讓調用者先動態開闢一個hostent對象,然後讓該函數來進行處理。
gethostbyname_r和gethostbyaddr_r函數就是這麼做的。
但引入的問題:
調用者不僅需要new一個hostent對象,還要提供hostent對象中的指針指向的空間。

struct hostent* gethostbyname_r(const char* hostname,struct hosten* result, char*buf, int buflen, int*h_errnop);  

其中result就是交由函數修改的對象。而buf就是這個對象中的指針指向的一塊內存空間。錯誤碼爲h_errnop,而不是全局變量h_errno
這裏很難確認這個buf的大小。不好用。
2> 我們可以讓不可重入函數本身new一個對象,最後返回。而不是返回一個全局static對象。getaddrinfo函數就是這麼做的。
引入的問題:該函數內部分配的空間,必須需要調用者顯示釋放掉,即調用函數freeaddrinfo。否則造成內存泄漏。

(十二) 其他網絡相關信息

我們上面提到了gethostby…、getservby…。
網絡信息包含:主機,服務,網絡,協議。因爲我們有:p273
其中主機和網絡是通過DNS來獲取的。
協議和服務是通過查詢本地主機的文件來獲取的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章