UNP第11章——名字与地址转换
阅读原文时间:2023年07月14日阅读:2

1.域名系统

  程序中只使用主机名和服务名的好处是,如果IP或端口变化,只需要改变映射关系,不需要重新编译程序。

1.1 资源记录

  DNS的条目为资源记录,有用的项如下:

A IPv4地址
AAAA IPv6地址
CNAME 规范名字
如:
ftp.unpbook.com 的 CNAME 为 linux.unpbook.com
www.unpbook.com 的 CNAME 为 linux.unpbook.com

1.2 解析器和名字服务器

  程序通过调用解析器库函数,调用DNS服务。常用的函数为 gethostbyname, gethostbyaddr。

  与解析器相关的配置文件和库函数关系如下

2.1 gethostbyname

   struct hostent \*gethostbyname(const char \*name);

       struct hostent {  
           char  \*h\_name;            /\* official name of host \*/  
           char \*\*h\_aliases;         /\* alias list \*/  
           int    h\_addrtype;        /\* host address type \*/  
           int    h\_length;          /\* length of address \*/  
           char \*\*h\_addr\_list;       /\* list of addresses \*/  
       }

  该函数只能返回资源记录的 A ,也就是IPv4。

  若要考虑IPv6,应使用 getaddrinfo.

  h_name 就是 CNAME(规范名字),如 ftp.unpbook.com 的规范名字为 linux.unpbook.com

  h_addrtype 只有为 IPv4,才有用

  h_addr_list 指向一个数组,数组元素为 IP地址(网络字节序,不是点分十进制)。

  可以传入点分十进制,或域名调用 gethostbyname

  gethostbyname 调用错误,不会设置errno,而是设置 h_errno,使用 hstrerror(h_errno) 得到错误描述的字符串。

int
main(int argc, char **argv)
{
char *ptr, **pptr;
char str[INET_ADDRSTRLEN];
struct hostent *hptr;

    while (--argc > 0) {  
            ptr = \*++argv;  
            if ( (hptr = gethostbyname(ptr)) == NULL) {  
                    err\_msg("gethostbyname error for host: %s: %s",  
                                    ptr, hstrerror(h\_errno));  
                    continue;  
            }  
            printf("official hostname: %s\\n", hptr->h\_name);

            for (pptr = hptr->h\_aliases; \*pptr != NULL; pptr++)  
                    printf("\\talias: %s\\n", \*pptr);

            switch (hptr->h\_addrtype) {  
            case AF\_INET:  
                    pptr = hptr->h\_addr\_list;  
                    for ( ; \*pptr != NULL; pptr++)  
                            printf("\\taddress: %s\\n",  
                                    Inet\_ntop(hptr->h\_addrtype, \*pptr, str, sizeof(str)));  
                    break;

            default:  
                    err\_ret("unknown address type");  
                    break;  
            }  
    }  
    exit(0);  

}

2.2 gethostbyaddr

   struct hostent \*gethostbyaddr(const void \*addr,  
                                 socklen\_t len, int type);

  输入网络字节序IP地址,查找主机CNAME。

  type 为 AF_INET

  返回 hostent ,通常我们感兴趣的只有 h_name。

2.3 可重入函数

  gethostbyname 和 getbostbyaddr 是不可重入的,原因是使用 static hostent host 。

  不可重入带来问题通常在 多线程和信号。

  如

main()
{
signal(SIGALRM, sig_alrm);
hptr = gethostbyname(…);
}

void
sig_alrm(int sig)
{
hptr= gethostbyname(…);
}

  可重入可不可重入总结:

gethostbyname, gethostbyaddr, getservbyname, getservbyport 都是不可重入的,原因是使用静态变量。不过有可重入版本
inet_pton, inet_ntop 不可重入
inet_ntoa 不可重入,不过有可重入版本。
getaddrinfo, getnameinfo 可重入

  对于errno的处理,errno可能会被多线程,信号意外改变,可以如下处理

void
sig_alrm(int sig)
{
int errno_save;

errno\_save  = errno;  
if (write(...) != nbytes)  
     fprintf(stderr, "write error = %d\\n", errno);  
errno = errno\_save;  

}

3.1 getserbyname

   struct servent \*getservbyname(const char \*name, const char \*proto);

       struct servent {  
           char  \*s\_name;       /\* official service name \*/  
           char \*\*s\_aliases;    /\* alias list \*/  
           int    s\_port;       /\* port number \*/  
           char  \*s\_proto;      /\* protocol to use \*/  
       }

  输入服务名和协议,查询端口号。

  如果指定proto,必须保证服务器使用了对应协议,如果没有指定,因为通常服务器使用相同端口号来使用不同协议,所以也没关系。

  s_port是网络字节序

  相关配置文件 /etc/services

  典型调用:

sptr = getservbyname("domain", "udp"); // DNS using UDP
sptr = getservbyname("ftp", "tcp"); // FTP using TCP

3.2 getservbyport

   struct servent \*getservbyport(int port, const char \*proto);

  输入端口号和协议,查询服务名

  port必须为网络字节序

  典型调用:

sptr = getservbyport(hton(53), "udp"); // DNS using UDP

4. 使用 gethostbyname 和 getserbyname

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);  

}

5.1 getaddrinfo

   int getaddrinfo(const char \*hostname, const char \*service,  
                   const struct addrinfo \*hints,  
                   struct addrinfo \*\*res);

       struct addrinfo {  
           int              ai\_flags;  
           int              ai\_family;  
           int              ai\_socktype;  
           int              ai\_protocol;  
           size\_t           ai\_addrlen;  
           struct sockaddr \*ai\_addr;  
           char            \*ai\_canonname;  
           struct addrinfo \*ai\_next;  
       };

  getaddrinfo支持IPv4,IPv6,并且提供名字到地址,服务到端口两种转换,返回的是一个sockaddr结构而不是地址列表,因此可以直接用于套接字库函数。

  hostname 是一个主机名或地址串(点分十进制或十六进制串)

  service 是一个服务名或十进制端口号串

  hints 可以为空,也可以是指向 struct addrinfo的指针,表示对期望返回信息的暗示。

    hints可以设置成员有

           int              ai\_flags;  
           int              ai\_family;  
           int              ai\_socktype;  
           int              ai\_protocol;

    ai_flags 可用的标志值和含义:

AI_PASSIVE 套接字将用于被动打开
AI_CANONAME 给之getaddrinfo返回主机的规范名字
AF_NUMERICHOST 防止任何类型的名字到地址的映射,hostname参数必须是一个地址串
AF_NUMERICSERV 防止任何类型的名字到端口的映射,service参数必须是一个端口号
AI_V4MAPPED 如果同时指定ai_famliy 为AF_INET6,那么如果没有可用的AAAA记录,就返回A记录对应的IPv4映射的IPv6地址
AI_ALL 如果同时指定AI_V4MAPPED,那么除了返回AAAA记录对应的IPv6地址外,还返回A记录对应的IPv4映射的IPv6地址。
AI_ADDRCONFIG 按照所在主机的配置选择返回地址类型,也就是只查找与所在主机回馈接口以外的网络接口配置的IP地址版本一致的地址。

  如果hints参数是空指针,那么本函数就假设ai_flag,ai_socktype,ai_protocol的值均为空,ai_family为AF_UNSPEC

  如果函数返回成功(0),result参数返回addrinfo结构的链表。

  举例,在没有任何暗示信息前提下,请求查找有2个IP地址的某个主机上的domain服务,那么就将返回4个addrinfo结构,

    第一个IP地址组合SOCK_STREAM套接字类型

    第一个IP地址组合SOCK_DGRAM套接字类型

    第二个IP地址组合SOCK_STREAM套接字类型

    第二个IP地址组合SOCK_DGRAM套接字类型

  且这些结构的返回顺序不确定。

  返回的addrinfo结构可用于其他套接字函数,

    socket调用使用 ai_family, ai_socktype,

    connect,bind调用使用 ai_addr, 和 ai_addrlen

  返回的res变量指向的addrinfo结构空间是动态分配的,需要使用freeaddrinfo释放。

  getaddrinfo的常见使用如下:

  (1)TCP或UDP客户进程,指定hostname和 service,返回后,TCP客户进程逐个使用返回的地址,以调用 socket , connect,直到一个地址调用成功。UDP客户进程调用sendto或connect,如果地址不工作(如收到错误信息,或者超时),则测试其他地址,直到成功。

  (2)服务进程一般只指定 service 而不指定 hostname,同时在AI_PASSIVE标志。TCP服务进程随后调用socket,bind,listen。如果想获得accept的客户地址,那么用 ai_addrlen 来malloc 地址结构。UDP服务调用socket,bind,recvfrom,如果服务器清楚自己只处理一种类型的套接字,那么应该把hints的ai_socktype设置成 SOCK_STREAM或SOCK_DGRAM。

  (3)到目前为止,我们的服务器(UDP,TCP)都只创建一个监听套接字或数据报套接字。而另一种设计是用 select或poll让服务器处理多个套接字,这种情况下服务器遍历整个addrinfo结构链表,并为每个结构创建一个套接字,再使用select或poll。

      这个技术的问题是,getaddrinfo返回多个结构的原因之一是该服务可以同时由IPv4,IPv6处理,但是这两个协议并非完全独立,如果我们为某个给定端口创建一个IPv6监听套接字,那么就没必要为同一个端口创建一个IPv4套接字,因为来自IPv4的连接将由协议栈和IPv6监听套接字自动处理。

5.2 gai_strerror

   const char \*gai\_strerror(int errcode);

  getaddrinfo的错误由gai_strerror解释。

5.3 freeaddrinfo

  void freeaddrinfo(struct addrinfo \*res);

  getaddrinfo返回的res指向的链表是动态分配的,由freeaddrinfo释放。

  同时注意res的浅拷贝问题。

5.4 getnameinfo

   int getnameinfo(const struct sockaddr \*sa, socklen\_t salen,  
                   char \*host, size\_t hostlen,  
                   char \*serv, size\_t servlen, int flags);

  输入套接字地址,返回主机名和服务名。

  sock_ntop和getnameinfo的区别是,前者不涉及DNS,只返回一个IP地址和端口号的一个可显示版本,后者通常尝试获得主机和服务名字。

5.5 对getaddrinfo的封装

  getaddrinfo是推荐使用的,但是调用过于麻烦,所以封装常用操作

struct addrinfo *
host_serv(const char *host, const char *serv, int family, int socktype)
{
int n;
struct addrinfo hints, *res;

    bzero(&hints, sizeof(struct addrinfo));  
    hints.ai\_flags = AI\_CANONNAME;  /\* always return canonical name \*/  
    hints.ai\_family = family;               /\* AF\_UNSPEC, AF\_INET, AF\_INET6, etc. \*/  
    hints.ai\_socktype = socktype;   /\* 0, SOCK\_STREAM, SOCK\_DGRAM, etc. \*/

    if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0)  
            return(NULL);

    return(res);    /\* return pointer to first on linked list \*/  

int
tcp_connect(const char *host, const char *serv)
{
int sockfd, n;
struct addrinfo hints, *res, *ressave;

    bzero(&hints, sizeof(struct addrinfo));  
    hints.ai\_family = AF\_UNSPEC;  
    hints.ai\_socktype = SOCK\_STREAM;

    if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0)  
            err\_quit("tcp\_connect error for %s, %s: %s",  
                             host, serv, gai\_strerror(n));  
    ressave = res;

    do {  
            sockfd = socket(res->ai\_family, res->ai\_socktype, res->ai\_protocol);  
            if (sockfd < 0)  
                    continue;       /\* ignore this one \*/

            if (connect(sockfd, res->ai\_addr, res->ai\_addrlen) == 0)  
                    break;          /\* success \*/

            Close(sockfd);  /\* ignore this one \*/  
    } while ( (res = res->ai\_next) != NULL);

    if (res == NULL)        /\* errno set from final connect() \*/  
            err\_sys("tcp\_connect error for %s, %s", host, serv);

    freeaddrinfo(ressave);

    return(sockfd);  

}

int
tcp_listen(const char *host, const char *serv, socklen_t *addrlenp)
{
int listenfd, n;
const int on = 1;
struct addrinfo hints, *res, *ressave;

    bzero(&hints, sizeof(struct addrinfo));  
    hints.ai\_flags = AI\_PASSIVE;  
    hints.ai\_family = AF\_UNSPEC;  
    hints.ai\_socktype = SOCK\_STREAM;

    if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0)  
            err\_quit("tcp\_listen error for %s, %s: %s",  
                             host, serv, gai\_strerror(n));  
    ressave = res;

    do {  
            listenfd = socket(res->ai\_family, res->ai\_socktype, res->ai\_protocol);  
            if (listenfd < 0)  
                    continue;               /\* error, try next one \*/

            Setsockopt(listenfd, SOL\_SOCKET, SO\_REUSEADDR, &on, sizeof(on));  
            if (bind(listenfd, res->ai\_addr, res->ai\_addrlen) == 0)  
                    break;                  /\* success \*/

            Close(listenfd);        /\* bind error, close and try next one \*/  
    } while ( (res = res->ai\_next) != NULL);

    if (res == NULL)        /\* errno from final socket() or bind() \*/  
            err\_sys("tcp\_listen error for %s, %s", host, serv);

    Listen(listenfd, LISTENQ);

    if (addrlenp)  
            \*addrlenp = res->ai\_addrlen;    /\* return size of protocol address \*/

    freeaddrinfo(ressave);

    return(listenfd);  

}
/* end tcp_listen */

  上面函数有一个问题,即指定的地址族为 AF_UNSPEC,即可能返回非期待的地址族套接字,如期待IPv4,返回IPv4和IPv6。

  解决方法:

    我们可以强制指定地址协议,如inet_pton

inet_pton(AF_INET,"0.0.0.0", &foo); //succeeds
inet_pton(AF_INET, "0::0", &foo); // fails
inet_pton(AF_INET6, "0.0.0.0", &foo); // fails
inet_pton(AF_INET6, "0::0", &foo); //succeeds

无连接UDP

int
udp_client(const char *host, const char *serv, SA **saptr, socklen_t *lenp)
{
int sockfd, n;
struct addrinfo hints, *res, *ressave;

    bzero(&hints, sizeof(struct addrinfo));  
    hints.ai\_family = AF\_UNSPEC;  
    hints.ai\_socktype = SOCK\_DGRAM;

    if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0)  
            err\_quit("udp\_client error for %s, %s: %s",  
                             host, serv, gai\_strerror(n));  
    ressave = res;

    do {  
            sockfd = socket(res->ai\_family, res->ai\_socktype, res->ai\_protocol);  
            if (sockfd >= 0)  
                    break;          /\* success \*/  
    } while ( (res = res->ai\_next) != NULL);

    if (res == NULL)        /\* errno set from final socket() \*/  
            err\_sys("udp\_client error for %s, %s", host, serv);

    \*saptr = Malloc(res->ai\_addrlen);  
    memcpy(\*saptr, res->ai\_addr, res->ai\_addrlen);  
    \*lenp = res->ai\_addrlen;

    freeaddrinfo(ressave);

    return(sockfd);  

}
/* end udp_client */

有连接UDP

int
udp_connect(const char *host, const char *serv)
{
int sockfd, n;
struct addrinfo hints, *res, *ressave;

    bzero(&hints, sizeof(struct addrinfo));  
    hints.ai\_family = AF\_UNSPEC;  
    hints.ai\_socktype = SOCK\_DGRAM;

    if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0)  
            err\_quit("udp\_connect error for %s, %s: %s",  
                             host, serv, gai\_strerror(n));  
    ressave = res;

    do {  
            sockfd = socket(res->ai\_family, res->ai\_socktype, res->ai\_protocol);  
            if (sockfd < 0)  
                    continue;       /\* ignore this one \*/

            if (connect(sockfd, res->ai\_addr, res->ai\_addrlen) == 0)  
                    break;          /\* success \*/

            Close(sockfd);  /\* ignore this one \*/  
    } while ( (res = res->ai\_next) != NULL);

    if (res == NULL)        /\* errno set from final connect() \*/  
            err\_sys("udp\_connect error for %s, %s", host, serv);

    freeaddrinfo(ressave);

    return(sockfd);  

}
/* end udp_connect */

int
udp_server(const char *host, const char *serv, socklen_t *addrlenp)
{
int sockfd, n;
struct addrinfo hints, *res, *ressave;

    bzero(&hints, sizeof(struct addrinfo));  
    hints.ai\_flags = AI\_PASSIVE;  
    hints.ai\_family = AF\_UNSPEC;  
    hints.ai\_socktype = SOCK\_DGRAM;

    if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0)  
            err\_quit("udp\_server error for %s, %s: %s",  
                             host, serv, gai\_strerror(n));  
    ressave = res;

    do {  
            sockfd = socket(res->ai\_family, res->ai\_socktype, res->ai\_protocol);  
            if (sockfd < 0)  
                    continue;               /\* error - try next one \*/

            if (bind(sockfd, res->ai\_addr, res->ai\_addrlen) == 0)  
                    break;                  /\* success \*/

            Close(sockfd);          /\* bind error - close and try next one \*/  
    } while ( (res = res->ai\_next) != NULL);

    if (res == NULL)        /\* errno from final socket() or bind() \*/  
            err\_sys("udp\_server error for %s, %s", host, serv);

    if (addrlenp)  
            \*addrlenp = res->ai\_addrlen;    /\* return size of protocol address \*/

    freeaddrinfo(ressave);

    return(sockfd);  

}
/* end udp_server */

  UDP套接字是不需要设置SO_REUSEADDR,因为UDP是没有WAIT_TIME类似的状态物。

手机扫一扫

移动阅读更方便

阿里云服务器
腾讯云服务器
七牛云服务器