Unix网络编程(5):DNS

1. 域名系统

        DNS中的条目称为资源记录 ( $resource$ $record$ , $RR$ )。

        每个组织往往运行一个或多个名字服务器 ( $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$> 中定义的下列常值之一:

        $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. getservbynamegetservbyport函数

        像主机一样,服务也通常靠名字来认知。如果我们在程序代码中通过名字而不是端口来代指一个服务,而且从名字到端口的映射关系保存在一个文件中 ( 通常是 $/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$ 成员可用的标志值及含义如下:

        如果 $hints$ 参数是一个空指针,那么就假设 $ai_-flags$ 、$ai_-socktype$ 和 $ai_-protocol$ 的值均为 $0$ ,$ai_-family$ 的值为 $AF_-UNSPEC$ 。如果函数成功返回,那么 $result$ 参数会被填入结果指针,指向一个链表。可导致返回多个 $addrinfo$ 结构的情形有:

  1. 与 $hostname$ 关联的地址有多个,那么适用于所请求地址族的每个地址都返回一个对应的结构;
  2. 如果 $service$ 参数指定的服务支持多个套接字类型,那么每个套接字类型都可能返回一个对应的结构,具体取决于 $hints$ 结构的 $ai_-socktype$ 成员。

        如果枚举 $getaddrinfo$ 所有可能的 $64$ 种输入,那么许多是无效的。我们只查看一些常见的输入:

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_rgethostbyaddr_r函数

        $gethostbyname$ 等函数是不可重入的,因为他们的返回值结构是一个静态全局变量。有两种方法可以把它们改为可重入函数:

  1. 把不可重入函数填写并返回静态结构的做法改为由调用者分配再由可重入函数填写结构,这是 $gethostbyname_-r$ 使用的技巧。这种方法比较复杂,因为调用者必须显式提供对应字符串数组的大小,此外,作为额外的参数,用于存放错误码的整数常量指针也是必要的,因为不能再使用 $h_-errno$ ;
  2. 由可重入函数调用 $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$ 字节的缓冲区应该足够了。

Unix网络编程(5):DNS