域名系统
域名系统(Domain Name System,DNS)主要用于主机名字与IP地址之间的映射。
主机名既可以是一个简单得名字,如solaris,也可以是一个全限定域名,如solaris.unpbook.com。
资源记录
DNS中的条目称为资源记录(resource record,RR)
A记录把一个主机名映射成一个32位的IPv4的地址。
AAAA记录把一个主机名映射成一个128位的IPv6地址。
PTR记录把IP地址映射成主机名。
MX记录把一个主机指定作为主机的“邮件交换器(mail exchanger)”。
CNAME代表“cononical name”(规范名字),它的常见用法为常用的服务指派CNAME记录,例如下面名为linux的主机有以下2个CNAME记录
解析器和名字服务器
每个组织机构往往运行一个或多个名字服务器,它们通常是所谓的BIND(Berkeley Internet Name Domain)程序。
本书我们编写的应用程序通过调用成为解析器的函数库中的函数接触DNS服务器,常见的解析器函数是gethostbyname和gethostbyaddr。
下图展示了应用程序、解析器和名字服务器的一个典型关系
gethostbyname函数
查找主机名最基本的函数是gethostbyname。如果调用成功,它就返回一个指向hostent结构的指针。
#inlcude <netdb.h> struct hostent *gethostbyname(const char *hostname); struct hostent { char *h_name; /* official (canonical) 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 */ };
gethostbyname执行的是对A记录的查询。它只能返回IPv4地址
下图展示了hostent结构和它包含的信息
下面给出调用gethostbyname的简单例子
1 #include "unp.h" 2 3 int 4 main(int argc, char **argv) 5 { 6 char *ptr, **pptr; 7 char str[INET_ADDRSTRLEN]; 8 struct hostent *hptr; 9 10 while (--argc > 0) { 11 ptr = *++argv; 12 if ( (hptr = gethostbyname(ptr)) == NULL) { 13 err_msg("gethostbyname error for host: %s: %s", 14 ptr, hstrerror(h_errno)); 15 continue; 16 } 17 printf("official hostname: %s ", hptr->h_name); 18 19 for (pptr = hptr->h_aliases; *pptr != NULL; pptr++) 20 printf(" alias: %s ", *pptr); 21 22 switch (hptr->h_addrtype) { 23 case AF_INET: 24 pptr = hptr->h_addr_list; 25 for ( ; *pptr != NULL; pptr++) 26 printf(" address: %s ", 27 Inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str))); 28 break; 29 30 default: 31 err_ret("unknown address type"); 32 break; 33 } 34 } 35 exit(0); 36 }
运行结果
gethostbyaddr函数
与gethostbyname的行为相反,gethostbyaddr函数试图由一个二进制的IP地址找到相应的主机名
#include <netdb.h> struct hostent *gethostbyaddr(const char *addr,socklen_t len,int family); //返回:若成功则为非空指针,若出错则为NULL且设置h_errno
addr参数实际上不是char *类型,而是一个指向存放IPv4地址的某个in_addr结构的指针
gethostbyaddr向一个名字服务器查询PTR记录。我们感兴趣的字段通常是规范主机名的h_name。
getservbyname和getservbyport函数
getservbyname函数用于根据给定名字查找相应服务
名字到端口号的映射关系保存在一个文件中(通常是/etc/services)
#include <netdb.h> struct servent *getservbyname(const char *servername,const char *protoname);
servent结构如下
struct servent { char *s_name; /* official service name */ char **s_aliases; /* alias list */ int s_port; /* port number */ char *s_proto; /* protocol to use */ };
servent结构中我们关心的主要字段是端口号,下面是本函数的典型调用
struct servent *sptr; sptr=getservbyname("domain","unp"); /* DNS using UNP */ sptr=getservbyname("ftp","tcp"); /* FTP using TCP*/ sptr=getservbyname("ftp",NULL); /* FTP using TCP*/ sptr=getservbyname("ftp","unp"); /* this call will fail */
getservbyport用于根据给定端口号和可选协议查找相应服务
#include <netdb.h> struct servent *getservbyport(int port,const char *protoname);
port参数的值必须网络字节序。本函数的典型调用如下
struct servent *sptr; sptr=getservbyport(htons(53),"unp"); /* DNS using UDP */ sptr=getservbyport(htons(21),"tcp"); /* FTP using TCP*/ sptr=getservbyport(htons(21),NULL); /* FTP using TCP*/ sptr=getservbyport(htons(21),"unp"); /* this call will fail */
有些端口号在TCP上用于一种服务,在UDP上却用于完全不同的另一种服务
下面是使用gethostbyname和getservbyname的时间获取客户程序
1 #include "unp.h" 2 3 int 4 main(int argc, char **argv) 5 { 6 int sockfd, n; 7 char recvline[MAXLINE + 1]; 8 struct sockaddr_in servaddr; 9 struct in_addr **pptr; 10 struct in_addr *inetaddrp[2]; 11 struct in_addr inetaddr; 12 struct hostent *hp; 13 struct servent *sp; 14 15 if (argc != 3) 16 err_quit("usage: daytimetcpcli1 <hostname> <service>"); 17 18 if ( (hp = gethostbyname(argv[1])) == NULL) { 19 if (inet_aton(argv[1], &inetaddr) == 0) { 20 err_quit("hostname error for %s: %s", argv[1], hstrerror(h_errno)); 21 } else { 22 inetaddrp[0] = &inetaddr; 23 inetaddrp[1] = NULL; 24 pptr = inetaddrp; 25 } 26 } else { 27 pptr = (struct in_addr **) hp->h_addr_list; 28 } 29 30 if ( (sp = getservbyname(argv[2], "tcp")) == NULL) 31 err_quit("getservbyname error for %s", argv[2]); 32 33 for ( ; *pptr != NULL; pptr++) { 34 sockfd = Socket(AF_INET, SOCK_STREAM, 0); 35 36 bzero(&servaddr, sizeof(servaddr)); 37 servaddr.sin_family = AF_INET; 38 servaddr.sin_port = sp->s_port; 39 memcpy(&servaddr.sin_addr, *pptr, sizeof(struct in_addr)); 40 printf("trying %s ", 41 Sock_ntop((SA *) &servaddr, sizeof(servaddr))); 42 43 if (connect(sockfd, (SA *) &servaddr, sizeof(servaddr)) == 0) 44 break; /* success */ 45 err_ret("connect error"); 46 close(sockfd); 47 } 48 if (*pptr == NULL) 49 err_quit("unable to connect"); 50 51 while ( (n = Read(sockfd, recvline, MAXLINE)) > 0) { 52 recvline[n] = 0; /* null terminate */ 53 Fputs(recvline, stdout); 54 } 55 exit(0); 56 }
getaddrinfo函数
getaddrinfo函数能够处理名字到地址以及服务到端口的这两种转换,而且支持IPv6。
#include <netdb.h> int getaddrinfo(const char *hostname, const char *service, const struct addrinfo *hint, struct addrinfo **result);
本函数通过result指针参数返回一个指向addrinfo结构链表的指针,该结构定义如下
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; };
hostname参数是一个主机名或地址串(IPv4点分十进制数串或IPv6十六进制数串)
service参数是一个服务名或十进制端口号数串
hints参数可以是一个空指针,也可以是一个指向某个addrinfo结构的指针,调用者在这个结构中填入关于期望返回的信息类型的暗示。
例如,如果指定的服务既支持TCP也支持UDP,那么调用者可以把hints结构中的ai_socktype成员设置成SOCK_DGRAM,使得返回的仅仅是适用于数据报套接字的信息。
hints结构中调用者可以设置的成员有:
1.ai_flags(0个或多个或在一起的AI_xxx值)
2.ai_family(某个AF_xxx值)
3.ai_socktype(某个SOCK_xxx值)
4.ai_protocol
ai_flags成员可用的标志值及含义如下:
AI_PASSIVE 套接字将用于被动打开
AI_CONONNAME 告诉getaddrinfo函数返回主机的规范名字
AI_NUMERICHOST 防止任何类型的名字到地址映射,hostname参数必须是一个地址串
AI_NUMERICSERV 防止任何类型的名字到服务器映射,service参数必须是一个十进制端口号数串
AI_V4MAPPED 如果同时制定ai_family成员为AF_INET6,那么如果没有可用的AAAA记录,就返回A记录对应的IPv4映射的IPv6地址
AI_ALL 如果同时指定AI_V4MAPPED标志,那么除了返回与AAAA记录对应的IPv6地址外,还返回与A记录对应的IPv4映射的IPv6地址
AI_ADDRCONFIG 按照所在主机的配置选择返回地址类型
如果本函数返回成功,那么result参数指向的变量已被填入一个指针,它指向由其中的ai_next成员串联起来的addrinfo结构链表。可导致返回多个addrinfo结构的情形有以下两个:
1.如果与hostname参数关联的地址有多个,则每个地址都返回一个对应的结构
2.如果service参数指定的服务支持多个套接字类型,那么每个套接字类型都可能返回一个对应的结构
在addrinfo结构中返回的信息可现成用于socket调用,随后现成用于客户的connect或sendto调用,或者社和服务器的bind调用。
如:socket函数的参数就是addrinfo结构中的ai_family、ai_socktype和ad_addr成员。
下图是getaddrinfo返回信息的实例
下面查看这个函数的一些常见的输入:
1.指定hostname和service。这是TCP或UDP客户进程调用getaddrinfo的常规输入。
该调用返回后,TCP客户在一个循环中针对每个返回的IP地址,逐一调用socket和connect,直到有一个连接成功,或者所有地址尝试完毕为止。
2.典型的服务器值指定service而不指定hostname,同时在hints结构中指定AI_PASSIVE标志。返回的套接字地址结构中应含有一个值为INADDR_ANY(对于IPv4)的IP地址
3.服务器可以使用select或poll函数让服务器进程处理多个套接字。
这种情况下,服务器将遍历getaddrinfo返回的整个addrinfo结构链表,并为每个结构创建一个套接字,再使用select或poll。
gai_strerror函数
下图个出可由getaddrinfo返回的非0错误值的名字和含义。gai_strerror以这些值为它的唯一参数,返回对应的出错字符串
#include <netdb.h> const char *gai_strerror(int error);
freeaddrinfo函数
由getaddrinfo返回的所有存储空间都是动态分配的。这些存储空间通过调用freeaddrinfo释放
#include <netdb.h> void freeaddrinfo(struct addrinfo *ai);
下面函数是使用getaddrinfo的接口函数
host_serv函数
host_serv函数不要求调用者分配并填写一个hints结构,该结构中我们感兴趣的两个字段(地址族和套接字类型)成为这个接口函数的参数。
#include "unp.h"
struct addrinfo *host_serv(const char *host, const char *serv, int family, int socktype);
//返回:若成功则为指向addrinfo结构的指针,若出错则为NULL
下面是该函数的源代码
1 struct addrinfo * 2 host_serv(const char *host, const char *serv, int family, int socktype) 3 { 4 int n; 5 struct addrinfo hints, *res; 6 7 bzero(&hints, sizeof(struct addrinfo)); 8 hints.ai_flags = AI_CANONNAME; /* always return canonical name */ 9 hints.ai_family = family; /* AF_UNSPEC, AF_INET, AF_INET6, etc. */ 10 hints.ai_socktype = socktype; /* 0, SOCK_STREAM, SOCK_DGRAM, etc. */ 11 12 if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0) 13 return(NULL); 14 15 return(res); /* return pointer to first on linked list */ 16 }
tcp_connect函数
我们使用getaddrinfo编写tcp_connect函数:创建一个TCP套接字并连接到一个服务器。
#include "unp.h" int tcp_connect(const char *host, const char *serv);
下面是该函数的源代码
1 #include "unp.h" 2 3 int 4 tcp_connect(const char *host, const char *serv) 5 { 6 int sockfd, n; 7 struct addrinfo hints, *res, *ressave; 8 9 bzero(&hints, sizeof(struct addrinfo)); 10 hints.ai_family = AF_UNSPEC; 11 hints.ai_socktype = SOCK_STREAM; 12 13 if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0) 14 err_quit("tcp_connect error for %s, %s: %s", 15 host, serv, gai_strerror(n)); 16 ressave = res; 17 18 do { 19 sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); 20 if (sockfd < 0) 21 continue; /* ignore this one */ 22 23 if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0) 24 break; /* success */ 25 26 Close(sockfd); /* ignore this one */ 27 } while ( (res = res->ai_next) != NULL); 28 29 if (res == NULL) /* errno set from final connect() */ 30 err_sys("tcp_connect error for %s, %s", host, serv); 31 32 freeaddrinfo(ressave); 33 34 return(sockfd); 35 }
使用tcp_connect重新编写时间获取客户程序
1 #include "unp.h" 2 3 int 4 main(int argc, char **argv) 5 { 6 int sockfd, n; 7 char recvline[MAXLINE + 1]; 8 socklen_t len; 9 struct sockaddr_storage ss; 10 11 if (argc != 3) 12 err_quit("usage: daytimetcpcli <hostname/IPaddress> <service/port#>"); 13 14 sockfd = Tcp_connect(argv[1], argv[2]); 15 16 len = sizeof(ss); 17 Getpeername(sockfd, (SA *)&ss, &len); 18 printf("connected to %s ", Sock_ntop_host((SA *)&ss, len)); 19 20 while ( (n = Read(sockfd, recvline, MAXLINE)) > 0) { 21 recvline[n] = 0; /* null terminate */ 22 Fputs(recvline, stdout); 23 } 24 exit(0); 25 }
tcp_listen函数
tcp_listen执行TCP服务器的通常步骤:创建一个TCP套接字,给它捆绑服务器的总所周知的端口,并允许接收外来的连接请求。
#include "unp.h" int tcp_listen(const char *host, const char *serv, socklen_t *addrlenp); //返回:若成功则为连接套接字描述符,若出错则不返回
下面是该函数的源代码
1 #include "unp.h" 2 3 int 4 tcp_listen(const char *host, const char *serv, socklen_t *addrlenp) 5 { 6 int listenfd, n; 7 const int on = 1; 8 struct addrinfo hints, *res, *ressave; 9 10 bzero(&hints, sizeof(struct addrinfo)); 11 hints.ai_flags = AI_PASSIVE; 12 hints.ai_family = AF_UNSPEC; 13 hints.ai_socktype = SOCK_STREAM; 14 15 if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0) 16 err_quit("tcp_listen error for %s, %s: %s", 17 host, serv, gai_strerror(n)); 18 ressave = res; 19 20 do { 21 listenfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); 22 if (listenfd < 0) 23 continue; /* error, try next one */ 24 25 Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); 26 if (bind(listenfd, res->ai_addr, res->ai_addrlen) == 0) 27 break; /* success */ 28 29 Close(listenfd); /* bind error, close and try next one */ 30 } while ( (res = res->ai_next) != NULL); 31 32 if (res == NULL) /* errno from final socket() or bind() */ 33 err_sys("tcp_listen error for %s, %s", host, serv); 34 35 Listen(listenfd, LISTENQ); 36 37 if (addrlenp) 38 *addrlenp = res->ai_addrlen; /* return size of protocol address */ 39 40 freeaddrinfo(ressave); 41 42 return(listenfd); 43 }
下面使用tcp_listen重新编写时间获取服务器程序
1 #include "unp.h" 2 #include <time.h> 3 4 int 5 main(int argc, char **argv) 6 { 7 int listenfd, connfd; 8 socklen_t len; 9 char buff[MAXLINE]; 10 time_t ticks; 11 struct sockaddr_storage cliaddr; 12 13 if (argc != 2) 14 err_quit("usage: daytimetcpsrv1 <service or port#>"); 15 16 listenfd = Tcp_listen(NULL, argv[1], NULL); 17 18 for ( ; ; ) { 19 len = sizeof(cliaddr); 20 connfd = Accept(listenfd, (SA *)&cliaddr, &len); 21 printf("connection from %s ", Sock_ntop((SA *)&cliaddr, len)); 22 23 ticks = time(NULL); 24 snprintf(buff, sizeof(buff), "%.24s ", ctime(&ticks)); 25 Write(connfd, buff, strlen(buff)); 26 27 Close(connfd); 28 } 29 }
下面是tcp_listen的协议无关时间获取服务器程序
1 #include "unp.h" 2 #include <time.h> 3 4 int 5 main(int argc, char **argv) 6 { 7 int listenfd, connfd; 8 socklen_t len, addrlen; 9 char buff[MAXLINE]; 10 time_t ticks; 11 struct sockaddr_storage cliaddr; 12 13 if (argc == 2) 14 listenfd = Tcp_listen(NULL, argv[1], &addrlen); 15 else if (argc == 3) 16 listenfd = Tcp_listen(argv[1], argv[2], &addrlen); 17 else 18 err_quit("usage: daytimetcpsrv2 [ <host> ] <service or port>"); 19 20 for ( ; ; ) { 21 len = sizeof(cliaddr); 22 connfd = Accept(listenfd, (SA *)&cliaddr, &len); 23 printf("connection from %s ", Sock_ntop((SA *)&cliaddr, len)); 24 25 ticks = time(NULL); 26 snprintf(buff, sizeof(buff), "%.24s ", ctime(&ticks)); 27 Write(connfd, buff, strlen(buff)); 28 29 Close(connfd); 30 } 31 }
udp_client函数
使用getaddrinfo来编写udp_client函数。
本函数创建一个为连接的UDP套接字,并返回3项数据。
1.返回值是该套接字的描述符
2.把目的IP地址和端口存放在saptr指向的套接字地址结构中,用于稍后调用sendto
3.这个套接字地址结构的大小在lenp指向的变量中返回
#include "unp.h" int udp_client(const char *host, const char *serv, SA **saptr, socklen_t *lenp); //返回:若成功则为未连接套接字描述符,若出错则不返回
下面是该函数的源代码
1 #include "unp.h" 2 3 int 4 udp_client(const char *host, const char *serv, SA **saptr, socklen_t *lenp) 5 { 6 int sockfd, n; 7 struct addrinfo hints, *res, *ressave; 8 9 bzero(&hints, sizeof(struct addrinfo)); 10 hints.ai_family = AF_UNSPEC; 11 hints.ai_socktype = SOCK_DGRAM; 12 13 if ( (n = getaddrinfo(host, serv, &hints, &res)) != 0) 14 err_quit("udp_client error for %s, %s: %s", 15 host, serv, gai_strerror(n)); 16 ressave = res; 17 18 do { 19 sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol); 20 if (sockfd >= 0) 21 break; /* success */ 22 } while ( (res = res->ai_next) != NULL); 23 24 if (res == NULL) /* errno set from final socket() */ 25 err_sys("udp_client error for %s, %s", host, serv); 26 27 *saptr = Malloc(res->ai_addrlen); 28 memcpy(*saptr, res->ai_addr, res->ai_addrlen); 29 *lenp = res->ai_addrlen; 30 31 freeaddrinfo(ressave); 32 33 return(sockfd); 34 }
协议无关时间获取客户程序
1 #include "unp.h" 2 3 int 4 main(int argc, char **argv) 5 { 6 int sockfd, n; 7 char recvline[MAXLINE + 1]; 8 socklen_t salen; 9 struct sockaddr *sa; 10 11 if (argc != 3) 12 err_quit("usage: daytimeudpcli1 <hostname/IPaddress> <service/port#>"); 13 14 sockfd = Udp_client(argv[1], argv[2], (void **) &sa, &salen); 15 16 printf("sending to %s ", Sock_ntop_host(sa, salen)); 17 18 Sendto(sockfd, "", 1, 0, sa, salen); /* send 1-byte datagram */ 19 20 n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL); 21 recvline[n] = '