Unix网络编程(5):DNS
1. 域名系统
DNS
中的条目称为资源记录 ( $resource$ $record$ , $RR$ )。
- $A$ :$A$ 记录把一个主机名映射成一个 $32$ 位的
IPv4
地址; - $AAAA$ :四 $A$ ( $quad$ $A$ ) 记录把一个主机名映射成一个 $128$ 位的
IPv6
地址; - $PTR$ :指针记录 ( $pointer$ $record$ ) 把
IP
地址映射成主机名。对于IPv4
地址,$32$ 地址的 $4$ 字节先反转顺序,每字节都转换成各自的十进制ASCII
值后,再添加 $in-addr.arpa$ ;对于IPv6
地址,$128$ 位地址中的 $32$ 个四位组先反转顺序,每个四位组都被转换成相应的十六进制ASCII
值后,再添加 $ip6.arpa$ ; - $MX$ :$MX$ 记录把一个主机指定作为给定主机的邮件交换器 ( $mail$ $exchanger$ );
- $CNAME$ :$CNAME$ 代表规范名字 ( $canonical$ $name$ ),常见用法是为常用的服务指派 ( 例如 $ftp$ 和 $www$ )
CNAME
记录。如果人们使用这些服务名而不是真实的主机名,那么相应的服务挪到另一个主机时他们也不必知道。
每个组织往往运行一个或多个名字服务器 ( $name$ $server$ ),它们通常就是所谓的 $BIND$ ( $Berkeley$ $Internet$ $Name$ $Domain$ ) 程序。诸如我们编写的客户和服务器等应用程序通过调用称为解析器 ( $resolver$ ) 的函数库中的函数接触DNS
服务器。解析器代码通常包含在一个系统函数库中,在构造应用程序时被链编 ( $link-editing$ ) 到应用程序中。解析器使用UDP
向本地名字服务器发出查询,如果本地名字服务器不知道答案,通常就会使用UDP
在整个因特网上查询其他名字服务器。如果答案太常,超出了UDP
消息的承载能力,本地名字服务器和解析器会自动切换到TCP
。
不使用DNS
也可能获取名字和地址信息。常用的替代方法有静态主机文件 ( 通常是 $/etc/hosts$ 文件 )、网络信息系统 ( $Network$ $Information$ $System$ , $NIS$ ) 以及轻权目录访问协议 ( $Lightweight$ $Directory$ $Access$ $Protocol$ , $LDAP$ )。不幸的是,系统管理员如何配置一个主机以使用不同类型的名字服务是实现相关的。幸运的是,这些差异对于应用程序开发人员来说通常是透明的,我们只需调用诸如 $gethostbyname$ 和 $gethostbyaddr$ 这样的解析器函数。
2. gethostbyname
函数
查找主机名最基本的函数是 $gethostbyname$ 。如果调用成功,它就返回一个指向 $hostent$ 结构的指针,该结构中含有所查找主机的所有IPv4
地址。这个函数的局限是只能返回IPv4
地址。POSIX
规范预警可能在将来的某个版本中撤销 $gethostbyname$ 函数。
#include <netdb.h>
// 成功返回非空指针,出错返回NULL并设置h_errno
struct hostent *gethostbyname(const char *hostname);
struct hostent {
char *h_name; // 正式规范主机名
char **h_aliases; // 别名数组,以NULL结尾
int h_addrtype; // 主机地址类型:AF_INET
int h_length; // 地址长度:4
char **h_addr_list; // IPv4地址指针数组,以NULL结尾
};
按照DNS
的说法,$gethostbyname$ 执行的是对 $A$ 记录的查询,只能返回IPv4
地址。
$gethostbyname$ 与我们介绍过的其他套接字函数的不同之处在于:当发生错误时,它不设置 $errno$ ,而是将 $h_errno$ 设置为在头文件 <$netdb.h$> 中定义的下列常值之一:
- $HOST_-NOT_-FOUND$
- $TRY_-AGAIN$
- $NO_-RECOVERY$
- $NO_-DATA$ ( 等同于 $NO_-ADDRESS$ )
$NO_-DATA$ 错误表示指定的名字有效,但是没有 $A$ 记录。如今多数解析器提供名为 $hstrerror$ 的函数,它以某个 $h_-errno$ 值作为唯一的参数,返回的是一个 $const$ $char$ $\star$ 指针,指向相应的错误说明。
3. gethostbyaddr
函数
$gethostbyaddr$ 函数试图由一个二进制IP
地址找到对应的主机名。
#include <netdb.h>
// 成功返回非空指针,出错返回NULL并设置h_errno
struct hostent *gethostbyaddr(const char *addr, socklen_t len, int family);
$addr$ 参数实际上不是 $char$ $\star$ 类型,而是一个指向存放IPv4
地址的某个 $in_-addr$ 结构的指针。按照DNS
的说法,$gethostbyaddr$ 在 $in_-addr.arpa$ 域中向一个名字服务器查询 $PTR$ 记录。
4. getservbyname
和getservbyport
函数
像主机一样,服务也通常靠名字来认知。如果我们在程序代码中通过名字而不是端口来代指一个服务,而且从名字到端口的映射关系保存在一个文件中 ( 通常是 $/etc/services$ ),那么即使端口发生改变,我们仅需要修改文件中的一行即可。$getservbyname$ 函数用于根据给定名字查找相应的服务。
#include <netdb.h>
// 成功返回非空指针,出错返回NULL
struct servent *getservbyname(const char *servname, const char *protoname);
struct servent {
char *s_name; // 官方服务名
char **s_aliases; // 别名数组
int s_port; // 端口号,网络字节序
char *s_proto; // 协议
};
服务名参数 $servname$ 必须指定,如果同时指定了 $protoname$ ,那么指定服务必须有匹配的协议。如果 $protoname$ 未指定而 $servname$ 指定服务支持多个协议,那么返回哪个端口取决于实现。通常情况下这种选择无关紧要,因为支持多个协议的服务往往使用相同的TCP
端口号和UDP
端口号。
#include <netdb.h>
// 成功返回非空指针,出错返回NULL
struct servent *getservbyport(int port, const char *protoname);
$getservbyport$ 用于根据给定端口号和可选协议查找相应服务。$port$ 参数的值必须为网络字节序。
5. getaddrinfo
函数
$gethostbyname$ 和 $gethostbyaddr$ 函数仅仅支持IPv4
。$getaddrinfo$ 函数能够处理名字到地址以及服务到端口这两种转换,返回的是一个 $sockaddr$ 结构而不是一个地址列表,这些 $sockaddr$ 结构随后可由套接字函数直接使用。该函数在POSIX
规范中定义。
#include <netdb.h>
// 成功返回0,出错返回非0
int getaddrinfo(const char *hostname, const char *service,
const struct addrinfo *hints, struct addrinfo **result);
struct addrinfo {
int ai_flags; // AI_PASSIVE, AI_CANONNAME
int ai_family; // AF_xxx
int ai_socktype; // SOCK_xxx
int ai_protocol; // 0 或 IPPROTO_xxx
socklen_t ai_addrlen; // ai_addr长度
char *ai_canonname; // 主机规范名称
struct sockaddr *ai_addr; // 套接字地址结构
struct addrinfo *ai_next; // 链表后继节点
};
$hostname$ 是一个主机名或地址串,$service$ 参数是一个服务名或十进制端口号。$hints$ 参数可以是一个空指针,也可以是一个指向某个 $addrinfo$ 结构的指针,调用者在这个结构中填入关于期望返回的信息类型的暗示。例如,如果指定的服务既支持TCP
也支持UDP
,那么调用者可以把 $hints$ 结构中的 $ai_-socktype$ 字段设置为 $SOCK_-DGRAM$ ,使得返回的仅仅是适用于数据报套接字的信息。$hints$ 结构中调用者可以设置的成员有:
- $ai_-flags$
- $ai_-family$
- $ai_-socktype$
- $ai_-protocol$
其中 $ai_-flags$ 成员可用的标志值及含义如下:
- $AI_-PASSIVE$ :套接字被动打开;
- $AI_-CANONNAME$ :返回主机规范名称;
- $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$ :按照所在主机的配置选择返回地址类型,也就是只查找与所在主机反馈接口以外的网络接口配置的
IP
地址版本一致的地址。
如果 $hints$ 参数是一个空指针,那么就假设 $ai_-flags$ 、$ai_-socktype$ 和 $ai_-protocol$ 的值均为 $0$ ,$ai_-family$ 的值为 $AF_-UNSPEC$ 。如果函数成功返回,那么 $result$ 参数会被填入结果指针,指向一个链表。可导致返回多个 $addrinfo$ 结构的情形有:
- 与 $hostname$ 关联的地址有多个,那么适用于所请求地址族的每个地址都返回一个对应的结构;
- 如果 $service$ 参数指定的服务支持多个套接字类型,那么每个套接字类型都可能返回一个对应的结构,具体取决于 $hints$ 结构的 $ai_-socktype$ 成员。
如果枚举 $getaddrinfo$ 所有可能的 $64$ 种输入,那么许多是无效的。我们只查看一些常见的输入:
- 指定 $hostname$ 和 $service$ 。这是
TCP
或UDP
客户进程调用 $getaddrinfo$ 的常规输入。该调用返回后,TCP
客户在一个循环中针对每个返回的IP
地址,逐一调用 $socket$ 和 $connect$ ,直到有一个连接成功,或者所有地址尝试完毕; - 指定 $service$ ,同时在 $hints$ 中指定 $AI_-PASSIVE$ 。这是典型的服务器进程,返回的套接字地址结构中应当只含有一个值为 $INADDR_-ANY$ (
IPv4
) 或 $IN6ADDR_-ANY$ (IPv6
) 的IP
地址。TCP
服务器随后调用 $socket$ 、$bind$ 和 $listen$ 。
6. gai_strerror
函数
#include <netdb.h>
// 返回指向错误描述消息字符串的指针
const char *gai_strerror(int error);
常值 | 说明 |
---|---|
$EAI_-AGAIN$ | 名字解析中临时失败 |
$EAI_-BADFLAGS$ | $ai_-flags$ 的值无效 |
$EAI_-FAIL$ | 名字解析中不可恢复地失败 |
$EAI_-FAMILY$ | 不支持 $ai_-family$ |
$EAI_-MEMORY$ | 内存分配失败 |
$EAI_-NONAME$ | $hostname$ 或 $service$ 未提供或不可知 |
$EAI_-OVERFLOW$ | 用户参数缓冲区溢出 |
$EAI_-SERVICE$ | 不支持 $ai_-socktype$ 类型的 $service$ |
$EAI_-SYSTEM$ | 在 $errno$ 变量中有系统错误返回 |
7. freeaddrinfo
函数
由 $getaddrinfo$ 返回的所有存储空间都是动态获取的,包括 $addrinfo$ 结构、$ai_-addr$ 结构和 $ai_-canonname$ 字符串。这些存储空间通过 $freeaddrinfo$ 返还给系统。
#include <netdb.h>
void freeaddrinfo(struct addrinfo *ai);
8. getnameinfo
函数
$getnameinfo$ 函数是 $getaddrinfo$ 的互补函数。
#include <netdb.h>
// 成功返回0,出错返回非0
int getnameinfo(const struct sockaddr *sockaddr, socklen_t addrlen,
char *host, socklen_t hostlen,
char *serv, socklen_t servlen, int flags);
$sockaddr$ 指向一个套接字地址结构,其中包含待转换成直观可读的字符串的协议地址,$addrlen$ 是这个结构的长度,该结构及其长度通常由 $accept$ 、$recvfrom$ 、$getsockname$ 或 $getpeername$ 返回。待返回的 $2$ 个直观可读字符串由调用者预先分配存储空间,$host$ 和 $hostlen$ 指定主机字符串,$serv$ 和 $servlen$ 指定服务字符串。如果调用者不想返回主机字符串,那就指定 $hostlen$ 为 $0$ ,同样的,也可以通过指定 $servlen$ 为 $0$ 来不返回服务字符串。$flags$ 可以改变 $getnameinfo$ 的操作,如下表所示。
常值 | 说明 |
---|---|
$NI_-DGRAM$ | 数据报服务 |
$NI_-NAMEREQD$ | 若不能从地址解析出名字则返回错误 |
$NI_-NOFQDN$ | 只返回FQDN 的主机名部分 |
$NI_-NUMERICHOST$ | 以数串格式返回主机字符串 |
$NI_-NUMERICSCOPE$ | 以数串格式返回范围标识字符串 |
$NI_-NUMERICSERV$ | 以数串格式返回服务字符串 |
当知道处理的是数据报套接字时,调用者应设置 $NI_-DGRAM$ 标志,因为在套接字地址结构中给出的仅仅是IP
地址和端口号,$getnameinfo$ 无法就此确定所使用的协议。
9. gethostbyname_r
和gethostbyaddr_r
函数
$gethostbyname$ 等函数是不可重入的,因为他们的返回值结构是一个静态全局变量。有两种方法可以把它们改为可重入函数:
- 把不可重入函数填写并返回静态结构的做法改为由调用者分配再由可重入函数填写结构,这是 $gethostbyname_-r$ 使用的技巧。这种方法比较复杂,因为调用者必须显式提供对应字符串数组的大小,此外,作为额外的参数,用于存放错误码的整数常量指针也是必要的,因为不能再使用 $h_-errno$ ;
- 由可重入函数调用 $malloc$ 动态分配内存空间,这是 $getaddrinfo$ 使用的技巧。这种方法的问题就是调用者必须调用 $freeaddrinfo$ 释放空间。
#include <netdb.h>
struct hostent *gethostbyname_r(const char *hostname,
struct hostent *result, char *buf, int buflen, int *h_errnop);
struct hostent *gethostbyaddr_r(const char *addr, int len, int type,
struct hostent *result, char *buf, int buflen, int *h_errnop);
上面两个函数每个都需要 $4$ 个额外的参数,其中 $result$ 指向用户分配的用于存储 $hostent$ 结构,$buf$ 指向由调用者分配的大小为 $buflen$ 的缓冲区,用于存放规范主机名、别名指针数组等,$result$ 中所有指针都指向该缓冲区内部。$gethostbyname$ 当前的实现最多能够返回 $35$ 个别名指针和 $35$ 个地址指针,并且内部使用了一个 $8192$ 字节的缓冲区存放,因此大小为 $8192$ 字节的缓冲区应该足够了。