Table of Contents

 

概述

域名系统

gethostbyname 函数

gethostbyaddr 函数

getservbyname 和 getservbyport 函数

getaddrinfo 函数

gai_strerror 函数

freeaddrinfo 函数

可重入函数

gethostbyname_r 和 gethostbyaddr_r 函数

其他网络相关信息


概述

我们应该使用名字而不是数值来表示主机服务器,主要原因是名字比较容易记住。


域名系统

域名系统 DNS(Domain Name System)主要用于主机名字与 IP 地址之间的映射。既可以是一个简单的名字,如 solaris,也可以是一个全限定域名(FQDN),如 solaris.unpbook.com 。

  • 资源记录

域名系统中的条目称为资源记录(resource record)RR。我们感兴趣的有如下几个:

(1)A                 A记录把一个主机名映射成一个32位的 IPv4 地址。

(2)AAAA         AAAA记录把一个主机名映射成一个128位的 IPv6 地址。

(3)PTR           称为“指针记录”的 PTR 记录把 IP 地址映射成主机名。

(4)MX              MX 记录把一个主机指定作为给定主机的“邮件交换器”。

(5)CNAME     “canonical name”(规范名字),为常用的服务指派 CNAME 记录。

如下是 unpbook.com 域中关于主机 freebsd 的4个DNS记录:

nameserver应该配置多少_主机名

  • 解析器和名字服务器

每个组织机构往往运行一个或多个名字服务器(name server),它们通常就是所谓的 BIND(Berkeley Internet Name Domain)程序。诸如我们编写的客户和服务器等应用程序,这些程序通过调用称为解析器的函数库接触 DNS 服务器。

通常的应用程序调用函数来执行解析器中的代码来获取 DNS 信息,解析器有解析器配置文件,如 Linux 中的 /etc/resolv.conf 文件通常包含本地名字服务器主机的 IP 地址。

nameserver应该配置多少_nameserver应该配置多少_02

解析器使用 UDP 向本地名字服务器发出查询。如果本地名字服务器不知道答案,它通常就会使用 UDP 在整个因特网上查询其他名字服务器。如果答案太长,超出了 UDP 消息的承载能力,本地名字服务器和解析器会自动切换到 TCP。


gethostbyname 函数

因为我们编程大多数情况下应该使用名字而非地址,所以这个函数就有了用途。这是查找主机名最基本的函数,这个函数只能返回 IPv4 地址。如果调用成功,返回一个指向 hostent 结构的指针,该结构中含有所查找主机的所有 IPv4 地址。而 getaddrinfo 函数能够同时处理 IPv4 和 IPv6 地址。

#include <netdb.h>
extern int h_errno;

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 */
           }

struct hostent *gethostbyname(const char *name);

我们假设所查询的主机有2个别名和3个IPv4地址:

nameserver应该配置多少_nameserver应该配置多少_03

返回 NULL 表示有错误发生,该函数不设置 errno 变量,而是将全局整数变量 h_errno 设置为在头文件 <netdb.h> 中定义的下列常值之一:

HOST_NOT_FOUND
              The specified host is unknown.

NO_DATA
              The  requested  name  is  valid  but  does not have an IP address. 
              Another type of request to the name server for this domain may  return  an  answer.
              The  constant NO_ADDRESS is a synonym for NO_DATA.

NO_RECOVERY
              A nonrecoverable name server error occurred.

TRY_AGAIN
              A temporary error occurred on an authoritative name server.  
              Try again later.

下面是一个实例:

#include <stdio.h>
#include <netdb.h>
#include <sys/socket.h>
#include <arpa/inet.h>

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

    while (--argc > 0) {
        ptr = *++argv;
        if((hptr = gethostbyname(ptr)) == NULL){
            printf("gethostbyname error : %s", 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("address : %s\n",
                       inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));
            break;
        default:
            printf("unknown address type");
            break;
        }
    }

    return 0;
}

执行:

$ main baidu.com

输出如下:

official hostname : baidu.com
address : 220.181.57.216

gethostbyaddr 函数

该函数试图由一个二进制的 IP 地址找到相应的主机名。

#include <netdb.h>
extern int h_errno;

#include <sys/socket.h>       /* for AF_INET */
struct hostent *gethostbyaddr(const void *addr, socklen_t len, int type);

返回:成功返回非空指针,出错返回NULL且设置 h_errno


getservbyname 和 getservbyport 函数

服务也通常靠名字来认知。我们可以在程序中通过名字而不是其端口号来指代一个服务,而且名字到端口号的映射关系保存在一个文件中(通常是 /etc/services),那么即使端口号发生变动,我们需要修改的仅仅是 /etc/services 文件中的某一行,而不必重新编译程序。getservbyname 用于根据给定名字和协议查找相应服务,getservbyport 用于根据给定端口和协议查找相应服务。

#include <netdb.h>

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

struct servent *getservbyport(int port, 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 */
           }

下面是使用用例:

#include <stdio.h>
#include <netdb.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#ifdef  HAVE_SOCKADDR_DL_STRUCT
#include    <net/if_dl.h>
#endif

#define MAXLINE 1024
#define	SA	struct sockaddr

char *sock_ntop(const struct sockaddr *sa, socklen_t salen)
{
    char        portstr[8];
    static char str[128];     /* Unix domain is largest */

    switch (sa->sa_family) {
    case AF_INET: {
        struct sockaddr_in *sin = (struct sockaddr_in *) sa;

        if (inet_ntop(AF_INET, &sin->sin_addr, str, sizeof(str)) == NULL)
            return(NULL);
        if (ntohs(sin->sin_port) != 0) {
            snprintf(portstr, sizeof(portstr), ":%d", ntohs(sin->sin_port));
            strcat(str, portstr);
        }
        return(str);
    }
/* end sock_ntop */

#ifdef  IPV6
    case AF_INET6: {
        struct sockaddr_in6    *sin6 = (struct sockaddr_in6 *) sa;

        str[0] = '[';
        if (inet_ntop(AF_INET6, &sin6->sin6_addr, str + 1, sizeof(str) - 1) == NULL)
            return(NULL);
        if (ntohs(sin6->sin6_port) != 0) {
            snprintf(portstr, sizeof(portstr), "]:%d", ntohs(sin6->sin6_port));
            strcat(str, portstr);
            return(str);
        }
        return (str + 1);
    }
#endif

#ifdef  AF_UNIX
    case AF_UNIX: {
        struct sockaddr_un *unp = (struct sockaddr_un *) sa;

            /* OK to have no pathname bound to the socket: happens on
               every connect() unless client calls bind() first. */
        if (unp->sun_path[0] == 0)
            strcpy(str, "(no pathname bound)");
        else
            snprintf(str, sizeof(str), "%s", unp->sun_path);
        return(str);
    }
#endif

#ifdef  HAVE_SOCKADDR_DL_STRUCT
    case AF_LINK: {
        struct sockaddr_dl *sdl = (struct sockaddr_dl *) sa;

        if (sdl->sdl_nlen > 0)
            snprintf(str, sizeof(str), "%*s (index %d)",
                     sdl->sdl_nlen, &sdl->sdl_data[0], sdl->sdl_index);
        else
            snprintf(str, sizeof(str), "AF_LINK, index=%d", sdl->sdl_index);
        return(str);
    }
#endif
    default:
        snprintf(str, sizeof(str), "sock_ntop: unknown AF_xxx: %d, len %d",
                 sa->sa_family, salen);
        return(str);
    }
    return (NULL);
}

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)
        printf("usage : daytimetcp <hostname> <service>");

    if((hp = gethostbyname(argv[1])) == NULL){
        if(inet_aton(argv[1], &inetaddr) == 0){
            printf("hostname error : %s", 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)
        printf("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*/
        printf("connect error");
        close(sockfd);
    }
    if(*pptr == NULL)
        printf("unable to connect");

    while ((n = read(sockfd, recvline, MAXLINE)) > 0) {
        recvline[n] = 0;
        printf("%s", recvline);
    }

    return 0;
}

比喻这时,你自己有一个主机名叫 myPrivate,然后在这个主机上运行有一个 daytime 的服务器程序,我们可以这样使用该程序:

$ daytimetcp myPrivate daytime


getaddrinfo 函数

该函数能够处理名字到地址以及服务到端口这两种转换,返回的是一个 sockaddr 结构而不是一个地址列表。使用该函数可以解析 IPv6 地址。

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

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

struct addrinfo {
               int              ai_flags;
               int              ai_family;
               int              ai_socktype;
               int              ai_protocol;
               socklen_t        ai_addrlen;
               struct sockaddr *ai_addr;
               char            *ai_canonname;
               struct addrinfo *ai_next;
           };

返回:成功返回0,出错则为非0。

node 参数是一个主机名或地址串(IPv4 的点分十进制数串或 IPv6 的十六进制数串)。service 参数是一个服务器名或十进制端口号数串。hints 参数可以为空,也可以是一个指向某个 addrinfo 结构的指针,调用者在这个结构中填入关于期望返回的信息类型暗示,如调用者把 hints->ai_socktype = SOCK_DGRAM 可使得返回的仅仅是适用于数据报套接字的信息。如果该函数返回成功,那么由 res 参数指向的变量已被填入一个指针,它指向的是由其中的 ai_next 成员串接起来的 addrinfo 结构链表。

#include <sys/types.h>
       #include <stdio.h>
       #include <stdlib.h>
       #include <unistd.h>
       #include <string.h>
       #include <sys/socket.h>
       #include <netdb.h>

       #define BUF_SIZE 500

       int
       main(int argc, char *argv[])
       {
           struct addrinfo hints;
           struct addrinfo *result, *rp;
           int sfd, s;
           struct sockaddr_storage peer_addr;
           socklen_t peer_addr_len;
           ssize_t nread;
           char buf[BUF_SIZE];

           if (argc != 2) {
               fprintf(stderr, "Usage: %s port\n", argv[0]);
               exit(EXIT_FAILURE);
           }

           memset(&hints, 0, sizeof(struct addrinfo));
           hints.ai_family = AF_UNSPEC;    /* Allow IPv4 or IPv6 */
           hints.ai_socktype = SOCK_DGRAM; /* Datagram socket */
           hints.ai_flags = AI_PASSIVE;    /* For wildcard IP address */
           hints.ai_protocol = 0;          /* Any protocol */
           hints.ai_canonname = NULL;
           hints.ai_addr = NULL;
           hints.ai_next = NULL;

           s = getaddrinfo(NULL, argv[1], &hints, &result);
           if (s != 0) {
               fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
               exit(EXIT_FAILURE);
           }

           /* getaddrinfo() returns a list of address structures.
              Try each address until we successfully bind(2).
              If socket(2) (or bind(2)) fails, we (close the socket
              and) try the next address. */

           for (rp = result; rp != NULL; rp = rp->ai_next) {
               sfd = socket(rp->ai_family, rp->ai_socktype,
                       rp->ai_protocol);
               if (sfd == -1)
                   continue;

               if (bind(sfd, rp->ai_addr, rp->ai_addrlen) == 0)
                   break;                  /* Success */

               close(sfd);
           }

           if (rp == NULL) {               /* No address succeeded */
               fprintf(stderr, "Could not bind\n");
               exit(EXIT_FAILURE);
           }

           freeaddrinfo(result);           /* No longer needed */

           /* Read datagrams and echo them back to sender */
           for (;;) {
               peer_addr_len = sizeof(struct sockaddr_storage);
               nread = recvfrom(sfd, buf, BUF_SIZE, 0,
                       (struct sockaddr *) &peer_addr, &peer_addr_len);
               if (nread == -1)
                   continue;               /* Ignore failed request */

               char host[NI_MAXHOST], service[NI_MAXSERV];

               s = getnameinfo((struct sockaddr *) &peer_addr,
                               peer_addr_len, host, NI_MAXHOST,
                               service, NI_MAXSERV, NI_NUMERICSERV);
              if (s == 0)
                   printf("Received %zd bytes from %s:%s\n",
                           nread, host, service);
               else
                   fprintf(stderr, "getnameinfo: %s\n", gai_strerror(s));

               if (sendto(sfd, buf, nread, 0,
                           (struct sockaddr *) &peer_addr,
                           peer_addr_len) != nread)
                   fprintf(stderr, "Error sending response\n");
           }
       }

gai_strerror 函数

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

const char *gai_strerror(int errcode);

下图给出了可由该函数返回的非 0 错误值的名字和含义。

nameserver应该配置多少_服务器_04

返回:指向错误描述消息字符串的指针。


freeaddrinfo 函数

由 getaddrinfo 返回的所有存储空间都是动态获取的,包括 addrinfo 结构等等。该函数用来释放这些空间。

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

void freeaddrinfo(struct addrinfo *res);

getnameinfo 函数

#include <sys/socket.h>
#include <netdb.h>

int getnameinfo(const struct sockaddr *sa, socklen_t salen,
                       char *host, socklen_t hostlen,
                       char *serv, socklen_t servlen, int flags);

以一个套接字地址 sa 为参数,返回描述其中的主机的一个字符串和描述其中的服务的另一个字符串。

flag 可以改变该函数的操作:

nameserver应该配置多少_#include_05


可重入函数

我们可以查看 gethostbyname 和 gethostbyaddr 函数源码,如下

static struct hostent host;     /* result stored here */

struct hostent *gethostbyname(const char *__name)
{
    return (gethostbyname2(__name, family));
}

struct hostent *gethostbyname2(const char *__name, int __af)
{
    /* call DNS functions for A or AAAA query */
    
    /* fill in host structure */
    
    return (&host);
}

struct hostent *gethostbyaddr(const void *__addr, __socklen_t __len, int __type)
{
    /* call DNS functions for PTR query in in-addr.arpa domain */
    
    /* fill in host structure */
    
    return (&host);
}

会发现,这个 static 的host 变量会同时在这两个函数中返回。也就是说在多线程编程中,这可能会引发问题。这两个函数都不是可重入函数。

  • gethostbyname、gethostbyaddr、getservbyname、getservbyport这些函数都不是可重入的,它们的名字后加 _r 版本的实现是可重入的。
  • inet_pton 和 inet_ntop 总是可重入的。
  • inet_ntoa 不可重入,不过支持线程的一些实现提供了使用线程特定数据的可重入版本。
  • getaddrinfo 可重入的前提是由它调用的函数都可重入,这就是说,它应该调用可重入版本的 gethostbyname 和 getservbyname。
  • getnameinfo 同 getaddrinfo。

gethostbyname_r 和 gethostbyaddr_r 函数

有两种方法把不可重入的 gethostbyname 之类的函数改为可重入函数。

(1)把不可重入函数填写并返回静态结构的做法改为由调用者分配再由可重入函数填写结构。

(2)由可重入函数调用 malloc 以动态分配内存空间。当然这种方法必须要有与之对应的释放函数,如 freeaddrinfo 函数等。

#include <netdb.h>
       extern int h_errno;

       #include <sys/socket.h>       /* for AF_INET */

       int gethostbyaddr_r(const void *addr, socklen_t len, int type,
               struct hostent *ret, char *buf, size_t buflen,
               struct hostent **result, int *h_errnop);

       int gethostbyname_r(const char *name,
               struct hostent *ret, char *buf, size_t buflen,
               struct hostent **result, int *h_errnop);

       int gethostbyname2_r(const char *name, int af,
               struct hostent *ret, char *buf, size_t buflen,
               struct hostent **result, int *h_errnop);

其他网络相关信息

主机、网络、协议、服务:这四类信息都可以存放在一个文件中,每类都有三个访问函数:getXXXent、setXXXent、endXXXent。这些函数通过包含头文件 <netdb.h> 提供:如下是 host 的相关函数

#include <netdb.h>
       extern int h_errno;

       void sethostent(int stayopen);

       void endhostent(void);

       /* System V/POSIX extension */
       struct hostent *gethostent(void);

除了用于顺序处理文件的 get、set、end 这三个函数外,每类信息还提供一些键值查找函数。这些函数顺序遍历整个文件,但不是把每一行都返回给调用者,而是寻找与某个参数匹配的一个表项。这些键值查找函数具有形如 getXXXbyYYY 的名字,如下

nameserver应该配置多少_nameserver应该配置多少_06

只有主机和网络信息可以通过 DNS 获取,协议和服务信息总是从文件获取。

如果使用 DNS 查找主机和网络信息,那么只有键值查找函数才有意义。因为你不能使用 gethostent 并期待顺序遍历 DNS 中的所有表项。而且 gethostent 仅仅读取 /etc/hosts 文件却避免访问 DNS 。