不过在介绍这些函数之前,有必要先对主机名作一些说明。
主机名既可以是一个简单名字,如 solaris,也可以是一个全限定域名(Fully Qualified Domain Name, FODN,也称绝对名字),如 solaris.unpbook.com(严格说来,FQDN 必须要以一个点号结尾,以告知 DNS 解析器该名字是全限定的,从而不必搜索解析器自己维护的可能域名列表,不过很多用户往往都省略了结尾的点号)。为实现主机名与 IP 地址之间的映射,就需要用到域名系统(Domain Name System,DNS)。
DNS 中的条目称为资源记录(resource record,RR)。常用的 RR 类型主要有以下几种:
1、A:A 记录会把一个主机名映射成一个 IPv4 地址。比如下面是 unpbook.com 域中关于主机 freebsd 的 4 个 DNS 记录,其中第一个就是一个 A 记录:
freebsd IN A 12.106.32.254
IN AAAA 3ffe:b80:1f8d:1:a00:20ff:fea7:686b
IN MX 5 freebsd.unpbook,com.
IN MX 10 mailhost.unpbook.com.
2、AAAA:“四 A”记录会把一个主机名映射成一个 128 位的 IPv6 地址,如上所示。
3、MX:MX 记录表示把一个主机指定作为给定主机的“邮件交换器”(mail exchanger)。上例中的主机 freebsd 有 2 个 MX 记录:第一个的优先级值为 5,第二个的优先级为 10。当存在多个 MX 记录时,按照优先级顺序使用,值越小优先级越高。
4、PTR:“指针记录”(pointer record)PTR 会把 IP 地址映射成主机名。在做 PTR 查询时,会先把 IPv4/IPv6 地址反转顺序,每个字节都转换成各自的十进制/十六进制,然后在末尾添加上 in-addr.arpa/ip6.arpa。比如上例中 freebsd 的两个 PTR 记录分别是 254.32.106.12.in-addr.arpa 和 b.6.8.6.7.a.e.f.f.f.0.2.0.0.a.0.1.0.0.0.d.8.f.1.0.8.b.0.e.f.f.3.ip6.arpa。
5、CNAME:CNAME 代表“规范名字”(canonical name)。它经常为常用的服务(如 ftp 和 www)指派 CNAME 记录,这样用户就只需要使用这些服务名而无需知道真实的主机名。比如下面是一个名为 linux 的主机的 2 个 CNAME 记录:
ftp IN CNAME linux.unpbook.com.
www IN CNAME linux.unpbook.com.
很多组织机构往往运行有一个或多个名字服务器(name server),即通常所谓的 BIND(Berkeley Internet Name Domain)程序。而大部分的网络应用程序都是通过调用称为解析器(resolver)的函数库中的函数(如下文的 gethostbyname 等)来接触 DNS 解析器。下图展示了应用进程、解析器和名字服务器之间的一个典型关系。
应用程序使用通常的函数调用来执行解析器中的代码,解析器代码通过读取其系统相关配置文件来确定本组织机构的各个名字服务器的位置(通常使用文件 /etc/resolv.conf 包含本地名字服务器主机的 IP 地址)。解析器先向本地名字服务器发出 UDP 请求进行查询,如果它也不知道,通常也会使用 UDP 向其他名字服务器查询。如果答案太长,超出了 UDP 消息的承载能力,本地名字服务器和解析器就会自动切换到 TCP。
除了使用 DNS 获取名字和地址信息,常用的替代方法还有静态主机文件(通常是 /etc/hosts 文件)、网络信息系统(Network Information System,NIS)以及轻权目录访问协议(Lightweight Directory Access Protocol, LDAP)等。对于开发人员来说,调用诸如 gethostbyname 和 gethostbyaddr 这样的解析器函数即可。
利用主机名查找 IP 最基本的函数是 gethostbyname。它会在调用成功时返回一个指向 hostent 结构的指针,其中就包含了给定主机的所有 IPv4 地址。只能返回 IPv4 地址正是该函数的局限所在,使用后面要介绍的 getaddrinfo 函数则能同时处理 IPv4 和 IPv6 地址,因而为便于兼容,推荐使用后者。而 gethostbyaddr 函数的行为与 gethostbyname 刚好相反,它是试图通过一个二进制的 IP 地址找到相应的主机名。
#includestruct hostent *gethostbyname(const char *hostname); struct hostent *gethostbyaddr(const char *addr, socklen_t len, int family); /* 两个函数的返回值:若成功则为非空指针;否则为 NULL 且设置 h_errno */ struct hostent{ char *h_name; // official (canonical) name of host char **h_aliases; // pointer to array of pointers to alias names int h_addrtype; // host address type: AF_INET int h_length; // length of address: 4 char **h_addr_list; // ptr to array of ptrs with IPv4 addrs };
由此可见,按照 DNS 的说法,gethostbyname 执行的是对 A 记录的查询,因为它返回的是 IPv4 地址,而 gethostbyaddr 则是在 in_addr.arpa 域中向一个名字服务器查询 PTR 记录。
另外要注意的是,这两个函数在发生错误时与其他很多套接字函数不同,它们不设置 errno 变量,而是将全局整数变量 h_errno 设置为在头文件
* HOST_NOT_FOUND;
* TRY_AGAIN;
* NO_RECOVERY;
* NO_DATA(等同于 NO_ADDRESS)。
这里的 NO_DATA 表示指定的名字有效,但是没有 A 记录。只有 MX 记录的主机名就是这样的一个例子。现在多数解析器都提供了一个名为 hstrerror 的函数,它接收一个 h_errno 值作为参数,返回对应的错误说明。
此外还需注意,gethostbyaddr 函数的 addr 参数实际上并不是“char *”类型,而是一个指向存放了 IPv4 地址的某个 in_addr 结构的指针,len 参数就是该结构的大小(对于 IPv4 地址为 4)。
下面这个简单例子演示了 gethostbyname 函数的用法。
#include#include #include #include #include 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){ printf("gethostbyname error for host: %s: %s\n", 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: for(pptr=hptr->h_addr_list; *pptr!=NULL; pptr++) printf("\taddress: %s\n", inet_ntop(AF_INET, *pptr, str, sizeof(str))); break; default: printf("Error: unknown address type!"); } } exit(0); }
类似于主机,服务也可以通过名字来认知。通常使用文件 /etc/services 来保存服务名字到端口号的映射关系。函数 getservbyname 可根据给定的名字和可选的协议参数来查找相应的服务,getservbyport 函数则是根据给定的端口号和可选的协议参数来查找相应的服务。
#includestruct servent *getservbyname(const char *servname, const char *protoname); struct servent *getservbyport(int port, const char *protoname); /* 两个函数的返回值:若成功则为非空指针;否则为 NULL */ struct servent{ char *s_name; // official service name char **s_aliases; // alias list int s_port; // port number, network byte order char *s_proto; // protocal to use };
注意,如果指定了 protoname 参数,那么对应的服务必须要有匹配的协议。由于有些网络服务既用 TCP 也用 UDP(如 DNS),有些服务又仅仅支持单个协议(如 FTP 服务就只使用 TCP),所以如果 protoname 未指定而 servname 服务支持多个协议,则返回哪个端口号取决于实现。尽管多数情况下支持多个协议的服务往往使用相同的端口号,但也不能保证一定如此。此外,有些端口号在不同的协议下所代表的服务是不一样的,比如 512~514 范围内的端口。
下面这个程序演示了 gethostbyname 和 getservbyname 函数的用法,它接收 2 个命令行参数:主机名和服务名。它会尝试连接到多宿服务器主机的每个 IP 地址,直到有一个连接成功或所有地址尝试完毕为止。
#include#include #include #include #include #include #include #include typedef struct sockaddr SA; #define MAXLINE 1024 int main(int argc, char *argv[]){ if(argc != 3){ printf("Usage: %s \n", argv[0]); exit(2); } struct hostent *p_hEnt = gethostbyname(argv[1]); struct in_addr **pptr = NULL; struct in_addr *inetaddrp[2]; struct in_addr inetaddr; if(p_hEnt == NULL){ if(inet_aton(argv[1], &inetaddr) == 0){ printf("hostname error: %s, %s\n", argv[1], hstrerror(h_errno)); exit(2); } inetaddrp[0] = &inetaddr; inetaddrp[1] = NULL; pptr = inetaddrp; }else{ pptr = (struct in_addr **)p_hEnt->h_addr_list; } struct servent *p_sEnt = getservbyname(argv[2], "tcp"); if(p_sEnt == NULL){ printf("wrong servename: %s\n", argv[2]); exit(2); } struct sockaddr_in servaddr; bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = p_sEnt->s_port; int sockfd; for(; *pptr != NULL; pptr++){ printf("trying %s\n", inet_ntoa(**pptr)); //servaddr.sin_addr = **pptr; // 直接利用结构体赋值 memcpy(&servaddr.sin_addr, *pptr, sizeof(struct in_addr)); sockfd = socket(AF_INET, SOCK_STREAM, 0); if(connect(sockfd, (SA *)&servaddr, sizeof(servaddr)) == 0) break; // success close(sockfd); // connect 失败的套接字不能再继续使用 printf("\t...connect error!\n"); } if(*pptr == NULL) printf("cannot connect to %s\n", argv[1]); char line[MAXLINE+1]; int n; while((n=read(sockfd, line, MAXLINE)) > 0){ line[n] = 0; // null terminate fputs(line, stdout); } exit(0); }