UNIX网络编程卷一 学习笔记 第十一章 名字与地址转换

2023-10-31

到目前为止,本书中所有例子都用数值地址表示主机(如206.6.226.33),用数值端口号来标识服务器(如端口13代表daytime服务器)。但出于某些理由,我们应使用名字而非数值:名字比较容易记住;数值地址可以变动而名字保持不变;随着往IPv6上转移,数值地址变得非常长,手工键入数值地址更易出错。

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

严格来说,FQDN也称为绝对名字(absolute name),且必须以一个点号来结尾,但用户往往省略结尾的点号,用户输入这个点号可以告知DNS解析器输入的域名是全限定的,从而解析器不必搜索自己维护的、用于找到用户输入的主机名的FQDN的可能域名列表。

DNS中的条目称为资源记录(RR,Resource Record),一些RR类型:
1.A记录把一个主机名映射成一个32位的IPv4地址,例如,以下是unpbook.com域中关于主机freebsd的4个DNS记录,其中第一个是一个A记录:
在这里插入图片描述
2.AAAA称为四A(quad A)记录,把一个主机名映射成一个128位的IPv6地址。称其为四A是因为128位地址位数是32位地址的四倍。

3.PTR指针记录,用于把IP地址映射成主机名。对于IPv4地址,32位地址的4个字节先反转顺序,每个字节都转换成各自的十进制ASCII值(0~255)后,再在后面加上.in-addr.arpa,结果字符串用于PTR查询。

对于IPv6地址,128位地址中的32个四位组先反转顺序,每个四位组都被转换成相应的16进制ASCII值(0~9,a~f),后面再加上.ip6.arpa

例如,上例主机freebsd的两个PRT记录分别为254.32.106.12.in-addr.arpab.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

早期标准指定在ip6.int域中反向查找IPv6地址,现IPv6的反向查找域已改为ip6.arpa,以与IPv4保持一致。在两者转换时存在一个过渡期,期间两者都可以使用。

4.MX记录把一个主机指定为给定主机的邮件交换器,上例中主机freebsd有2个MX记录,第一个优先级为5,第二个优先级为10,值越小优先级越高。

5.CNAME记录表示规范名字(Canonical Name),它将一个域名(别名)映射到另一个域名(规范名称),它的常见用法是为常用服务(如ftp和www)指派CNAME记录,如果人们用的是别名而非真实的主机名,那么相应的服务挪到另一个主机时用户也不用知道,例如我们的linux主机有以下2个CNAME记录:
在这里插入图片描述
上图中最左边一列都是非限定主机名,非限定主机名是指不包含完整域名信息的主机名,例如,如果一个计算机的完全限定域名(FQDN)是host.example.com,那么它的非限定主机名就是host。非限定主机名通常用于本地网络中,因为它们可以方便地引用同一域中的其他计算机。当使用非限定主机名调用gethostbyname函数时,它会尝试将该主机名解析为全限定域名(FQDN),这是通过在主机名后面添加本地域名来实现的,例如,如果你的本地域名是example.com,并且你使用非限定主机名host1调用gethostbyname函数,则该函数将尝试解析 host1.example.com的IP地址。对于上例,本地域名为unpbook.com,因此用户输入ftp.unpbook.com或www.unpbook.com都可访问同一台主机linux.unpbook.com.(此处给出的FQDN是以点号结尾的,但日常使用中人们通常省略最后的点号),如果我们想把这两个服务放到其他主机上,只需更改CNAME记录将ftp或www映射到同一域中(需要加上同一域这一限制条件是因为最左边一列是非限定主机名)其他主机的域名上即可。

目前处于IPv6部署的极早期,系统管理员们会给同时支持IPv4和IPv6的主机使用什么样的域名命名约定尚不清楚。一种可能的约定是:把A记录和AAAA记录都置于主机名之下;再创建一个以主机名-4为主机名,含有A记录的RR;再创建一个以主机名-6为主机名,含有AAAA记录的RR;再创建一个以主机名-611结尾,含有指向链路本地地址的AAAA记录(为了便于调试)。以下aix主机有这些记录:
在这里插入图片描述
每个组织机构往往运行一个或多个名字服务器,他们通常是所谓的BIND(Berkeley Internet Name Domain)程序。客户和服务器通过调用称为解析器的函数库中的函数接触DNS服务器。常见的解析器函数是gethostbyname和gethostbyaddr,前者把主机名映射为IPv4地址,后者执行相反的映射。
在这里插入图片描述
解析器代码通常包含在一个系统函数库中,在构造应用程序时被link-editing(即把多个目标文件链接成可执行文件的过程)到程序中。还有些系统提供一个由全体应用进程共享的集中式解析器守护进程,并提供向这个守护进程执行RPC的系统函数库代码。

解析器代码通过读取其系统相关配置文件确定本组织机构的名字服务器们(出于可靠可冗余的目的,必须要设置多个名字服务器)所在位置。文件/etc/resolv.conf通常包含本地名字服务器主机的IP地址。

既然名字比地址好记易配,如果能在/etc/resolv.conf文件中也使用名字服务器主机的名字该多好,但这样做名字服务器主机自身的名字到地址转换由谁执行呢。

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

不使用DNS也可能获取名字和地址信息,常用的替代方法有静态主机文件(/etc/hosts)、网络信息系统(NIS,Network Information System)、轻权目录访问协议(LDAP,Lightweight Directory Access Protocol)。但系统管理员如何配置主机使用哪种类型的名字服务是实现相关的,Solaris 2.x、HP-UX 10及之后版本、FreeBSD 5.x及之后版本使用文件/etc/nsswitch.conf;AIX使用文件/etc/netsvc.conf;BIND 9.2.2提供了自己的名为信息检索服务(IRS,Information Retrival Sevice)的版本,使用文件/etc/irs.conf。如果使用名字服务器,那么所有这些系统都使用文件/etc/resolv.conf指定名字服务器的地址。但这些差异对应用开发人员是透明的,我们只需调用诸如gethostbyname和gethostbyaddr这样的解析器函数。

大多应用应处理名字而非地址,尤其是往IPv6转移时,因为IPv6地址(十六进制数串)比IPv4点分十进制数串长得多。

gethostbyname函数返回一个指向hostent结构的指针,该结构含有所查主机的所有IPv4地址。此函数的局限是只能返回IPv4地址,而后面要说的getaddrinfo函数能同时处理IPv4和IPv6地址。POSIX规范预警可能会在将来版本中撤销gethostbyname函数。

gethostbyname函数不大可能真正消失,除非整个因特网改为使用IPv6,那可能在遥遥无期的将来,从POSIX规范中撤销该函数意在声明新的程序不该再使用它,鼓励在新程序中改用getaddrinfo函数。
在这里插入图片描述
hostent结构:
在这里插入图片描述
gethostbyname函数执行对A记录的查询,返回IPv4地址时的情况:
在这里插入图片描述
上图中,假设所查的主机名有2个别名和3个IPv4地址,所查询主机的正式主机名和所有别名都是以空字符结尾的C字符串。

h_name字段称为所查询主机的规范(canonical)名字。以上面我们的CNAME例子为例,主机ftp.unpbook.com的规范名字是linux.unpbook.com。如果我们在主机aix上以一个非限定主机名(如solaris)调用gethostbyname,则作为规范名返回的是它的FQDN(即solaris.unpbook.com)。

有些版本的gethostbyname函数实现允许hostname参数是一个点分十进制数串。POSIX规范允许但不强求如此处理hostname参数,因此考虑可移植性的程序不能依赖这个特性。

gethostbyname函数在发生错误时,不设置errno变量,而是将全局变量h_errno设为以下常值之一(这些常值定义在头文件netdb.h中):
1.HOST_NOT_FOUND。

2.TRY_AGAIN。

3.NO_RECOVERY。

4.NO_DATA(等同于NO_ADDRESS)。表示指定的名字有效,但它没有A记录,如只有MX记录的主机就是这样的例子。

多数解析器提供名为hstrerror的函数,它以某个h_errno值作为唯一参数,返回一个const char *指针,指向响应错误的说明。

调用gethostbyname函数,并显示返回的所有信息(代码11-3):

#include "unp.h"

int main(int argc, char **argv) {
    char *ptr, **pptr;
    // INET_ADDRSTRLEN定义在头文件arpa/inet.h中,含义为用点分十进制数串表示IPv4地址时所需的最大字符数
    char str[INET_ADDRSTRLEN];
    struct hostent *hptr;

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

以主机aix的名字作为参数运行该程序,该主机只有一个IPv4地址:
在这里插入图片描述
正式主机名(official host)就是FQDN。即使主机aix有IPv6地址,返回的也只有IPv4地址。

有多个IPv4地址的Web服务器主机的输出:
在这里插入图片描述
下例中有一个CNAME记录:
在这里插入图片描述
在这里插入图片描述
为了查看由hstrerror函数返回的错误信息串,我们先指定一个不存在的主机名,再指定一个仅有MX记录的名字:
在这里插入图片描述
gethostbyaddr函数试图由一个二进制IP地址找到相应主机名,与gethostbyname函数的行为刚好相反:
在这里插入图片描述
gethostbyaddr函数返回一个指向hostent结构的指针,对于该结构,我们感兴趣的通常是存放规范主机名的h_name字段。

addr参数实际上不是char *类型,而是一个指向存放IPv4地址的某个in_addr结构的指针。len参数是这个结构的大小,对于IPv4地址为4。family参数为AF_INET。

gethostbyaddr函数在in_addr.arpa域中向一个名字服务器查询PTR记录。

像主机一样,服务也通常靠名字来认知,如果我们在代码中通过其名字而非其端口号来指代一个服务,且从名字到端口号的映射保存在一个文件中(通常是/etc/services),那么即使端口号发生变动,我们需修改的仅仅是/etc/services文件中的某一行,而不用重新编译应用程序。getservbyname函数根据给定名字查找相应服务。

赋予各个服务的端口号列表由IANA维护,/etc/services文件通常包含由IANA维护的端口号及其对应服务名列表的某个子集。
在这里插入图片描述
getservbyname函数返回的非空指针指向如下的servent结构:
在这里插入图片描述
在这里插入图片描述
servname参数必须指定,如果同时指定了协议(protoname参数为非空指针),那么指定服务必须有匹配的协议。有些因特网服务既用TCP也用UDP提供(如DNS),有些因特网服务则仅支持单个协议(如FTP要求使用TCP)。如果protoname参数未指定而servname参数指定的服务支持多个协议,那么返回哪个端口号取决于实现,通常这是无关紧要的,因为支持多个协议的服务往往使用相同的TCP和UDP端口号,但这点并没有保证。

servent结构中我们关心的主要字段是端口号,端口号是以网络字节序返回的,把它存到套接字地址结构时不用再调用htons。

getservbyname函数的典型调用:
在这里插入图片描述
上图中第三个getservbyname调用的注释是使用TCP的FTP服务,虽然第2个参数是NULL,但FTP服务只有tcp提供,因此返回的一定是使用TCP的FTP服务。

以下是/etc/services文件中的典型行,grep命令的-e选项指定要搜索的模式,下图中命令的含义是搜索文件/etc/services中以ftp或domain开头的行:
在这里插入图片描述
getservbyport函数根据给定端口号和可选协议查找相应服务名:
在这里插入图片描述
port参数的值必须是网络字节序。getservbyport函数的典型调用如下:
在这里插入图片描述
有些端口号在TCP上用于一种服务,在UDP上用于另一种服务:
在这里插入图片描述
端口514在TCP上由rsh命令(该命令可以在计算机网络上的另一台计算机上以另一个用户身份执行shell命令。rsh连接到远程系统并运行rsh守护程序(rshd),该守护程序使用TCP端口514)使用,在UDP上由syslog守护进程使用。512~514范围内的端口都有这个特性。

把TCP的daytime客户程序改为使用gethostbyname和getservbyname函数,并改用2个命令行参数:主机名和服务名,以下是改动后的程序,它还会尝试连接到多宿服务器主机的每个IP地址,直到有一个连接成功或所有地址尝试完毕,以下是程序11-4:

#include "unp.h"

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

    if ((hp = gethostbyname(argv[1])) == NULL) {
        // 如果gethostbyname函数失效,则直接尝试使用inet_aton函数,确定该参数是否已经是ASCII格式地址
        // inet_aton函数仅可用于IPv4,inet_pton既可用于IPv4,也可用于IPv6
        if (inet_aton(argv[1], &inetaddr) == 0) {
		    err_quit("hostname error for %s: %s", argv[1], 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) {
        err_quit("getservbyname error for %s", argv[2]);
    }

    // 该循环为服务器主机的每个地址执行一次
    for (; *pptr != NULL; ++pptr) {
        sockfd = Socket(AF_INET, SOCK_STREAM, 0);

        // 我们可以把bzero调用和后面两个赋值语句置于循环体外提高执行效率,但这样更易读
		bzero(&servaddr, sizeof(servaddr));
		servaddr.sin_family = AF_INET;
		servaddr.sin_port = sp->s_port;
		memcpy(&servaddr.sin_addr, *pptr, sizeof(struct in_addr));
		// 使用自定义函数sock_ntop将套接字地址结构中的地址转换为可读的文本形式
		printf("trying %s\n", Sock_ntop((SA *)&servaddr, sizeof(servaddr)));
	
		if (connect(sockfd, (SA *)&servaddr, sizeof(servaddr)) == 0) {
		    break;    /* success */
		}
		// 自定义的err_ret函数打印消息并记入系统日志,然后返回
	    err_ret("connect error");
	    // connect调用失败的套接字描述符必须关闭,不能再用
		close(sockfd);
	}
	    
	if (*pptr == NULL) {
	    err_quit("unable to connect");
	}
	
	while ((n = Read(sockfd, recvline, MAXLINE)) > 0) {
	    recvline[n] = 0;    /* null terminate */
    	Fputs(recvline, stdout);
	}
    exit(0);
}

运行以上客户程序:
在这里插入图片描述
对一个没有运行标准daytime服务器的多宿系统运行以上程序:
在这里插入图片描述
11-4程序中,如果把主机名换成点分十进制数串,只有某些较新版本的BIND(一套与域名系统DNS交互的软件套件)才支持,但POSIX没有规定如何处理这种情况,可移植程序中不能依赖它。我们可以修改以上程序为支持点分十进制数串形式的地址和数字形式的服务:

#include "unp.h"

int main(int argc, char **argv) {
    int sockfd, n;
    char recvline[MAXLINE + 1];
    struct sockaddr_in servaddr;
    struct in_addr **pptr, *addrs[2];
    struct hostent *hp;
    struct servent *sp;

    if (argc != 3) {
        err_quit("usage: daytimetcpcli2 <hostname> <service>");
    }

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;

    if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) == 1) {
        addrs[0] = &servaddr.sin_addr;
		addrs[1] = NULL;
    } else if ((hp = gethostbyname(argv[1])) != NULL) {
        pptr = (struct in_addr **)hp->h_addr_list;
    } else {
        err_quit("hostname error for %s: %s", argv[1], hstrerror(h_errno));
    }

    if ((n = atoi(argv[2])) > 0) {
        servaddr.sin_port = htons(n);
    } else if ((sp = getservbyname(argv[2], "tcp")) != NULL) {
        servaddr.sin_port = sp->s_port;
    } else {
        err_quit("getservbyname error for %s", argv[2]);
    }

    for (; *pptr != NULL; ++pptr) {
        sockfd = Socket(AF_INET, SOCK_STREAM, 0);
        // 此处将memcpy函数改为memmove函数,两者功能相同,但后者能正确处理源、目的内存重叠的情形
        // 如果主机名字符串是一个点分十进制IP地址,则此时调用memmove的源和目的地址是相同的
		memmove(&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 */
		}
		err_ret("connect error");
		close(sockfd);
    }
    if (*pptr == NULL) {
        err_quit("unable to connect");
    }

    while ((n = Read(sockfd, recvline, MAXLINE)) > 0) {
        recvline[n] = 0;    /* null terminate */
		Fputs(recvline, stdout);
    }
    exit(0);
}

以上代码中,我们先调用了inet_pton检查是否是点分十进制数串,然后再调用gethostbyname,这是因为inet_pton函数会在本地判定主机名字符串是否是一个有效的点分十进制地址,只有当这种测试失效时我们才调用gethostbyname,后者往往涉及某些网络资源,因此需要花一些时间。

11-4程序只支持IPv4,以下修改使得它同时支持IPv4和IPv6(以下代码需要gethostbyname函数的实现支持返回IPv6地址):

#include "unp.h"

int main(int argc, char **argv) {
    int sockfd, n;
    char recvline[MAXLINE + 1];
    struct sockaddr_in servaddr;
    struct sockaddr_in6 servaddr6;
    struct sockaddr *sa;
    socklen_t salen;
    struct in_addr **pptr;
    struct hostent *hp;
    struct servent *sp;

    if (argc != 3) {
        err_quit("usage: daytimetcpcli3 <hostname> <service>");
    }

    if ((hp = gethostbyname(argv[1])) == NULL) {
        err_quit("hostname error for %s: %s", argv[1], hstrerror(h_errno));
    }

    if ((sp = getservbyname(argv[2], "tcp")) == NULL) {
        err_quit("getservbyname error for %s", argv[2]);
    }

    pptr = (struct in_addr **)hp->h_addr_list;
    for (; *pptr != NULL; ++pptr) {
        sockfd = Socket(hp->h_addrtype, SOCK_STREAM, 0);

		if (hp->h_addrtype == AF_INET) {
		    sa = (SA *)&servaddr;
		    salen = sizeof(servaddr);
		} else if (hp->h_addrtype == AF_INET6) {
		    sa = (SA *)&servaddr6;
		    salen = sizeof(servaddr6);
		} else {
		    err_quit("unknown addrtype %d", hp->h_addrtype);
		}
	
		bzero(sa, salen);
		sa->sa_family = hp->h_addrtype;
		// 自定义函数sock_set_port使用协议无关的方式设置端口到指定套接字地址结构
		sock_set_port(sa, salen, sp->s_port);
		// 自定义函数sock_set_addr使用协议无关的方式设置二进制地址到指定套接字地址结构
		sock_set_addr(sa, salen, *pptr);
	
		printf("trying %s\n", Sock_ntop(sa, salen));
	
		if (connect(sockfd, sa, salen) == 0) {
		    break;    /* success */
		}
		err_ret("connect error");
		close(sockfd);
    }
    if (*pptr == NULL) {
        err_quit("unable to connect");
    }

    while ((n = Read(sockfd, recvline, MAXLINE)) > 0) {
        recvline[n] = 0;    /* null terminate */
		Fputs(recvline, stdout);
    }
    exit(0);
}

以上程序使用gethostbyname函数返回的h_addrtype字段判断地址类型,并使用我们自编写的sock_set_port和sock_set_addr函数用合适的方式(IPv4地址用IPv4的方式,IPv6同理)设置套接字地址结构中的端口和地址两个字段。

以上程序虽然能工作,但有两个局限:一是我们要处理IPv4和IPv6的所有差异,在查看h_addrtype字段后再设置合适的套接字地址结构(sa变量)和套接字地址结构长度(salen),更好的方式是由某个库函数完成整个套接字地址结构的填写(如getaddrinfo函数);二是以上程序只能在支持IPv6的主机上编译,如果要在仅支持IPv4的主机上编译,就要加很多#ifdef,从而使代码变得很复杂。

gethostbyname和gethostbyaddr函数的很多实现只支持IPv4。getaddrinfo函数能处理域名到地址以及服务名到端口的转换,且支持IPv6,它返回的是一个addrinfo结构的列表,这些addrinfo结构中的sockaddr结构可由套接字函数直接使用,这样getaddrinfo函数就把协议相关性完全隐藏在此库函数内部,该函数在POSIX中定义。
在这里插入图片描述
getaddrinfo函数通过result指针参数返回一个指向addrinfo结构链表的指针,addrinfo结构定义在头文件netdb.h中:
在这里插入图片描述
hostname参数是一个主机名或地址串(IPv4的点分十进制串或IPv6的十六进制串)。service参数是一个服务名或十进制端口号数串。

hints参数可以是一个空指针,也可以是一个指向addrinfo结构的指针,调用者在该结构中填入期望返回的信息类型,例如,如果指定的服务既支持TCP也支持UDP(如某DNS服务器的domain服务),而我们需要其UDP服务,那么调用者可以把hints结构中的ai_socktype成员设为SOCK_DGRAM,使得函数返回仅仅适用于数据报套接字的信息。

hints参数指针指向的结构中,调用者可设置的成员:
1.ai_flags:0个或多个被OR在一起的AI_XXX值。可用值及含义如下:
(1)AI_PASSIVE:套接字将用于被动打开。

(2)AI_CANONNAME:告知getaddrinfo函数返回主机的规范名字。

(3)AI_NUMERICHOST:限定hostname参数必须是一个地址串而不能是名字。

(4)AI_NUMERICSERV:限定service参数必须是一个十进制端口号而不能是服务名。

(5)AI_V4MAPPED:如果同时指定ai_family成员为AF_INET6,则如果没有可用的AAAA记录,就返回A记录对应的IPv4地址映射的IPv6地址(即将IPv4地址放在IPv6地址的低32位,高96位用0:0:0:0:0:FFFF填充,这样可以用通用格式保存IPv4和IPv6地址)。

(6)AI_ALL:如果同时指定AI_V4MAPPED标志,那么除返回与AAAA记录对应的IPv6地址外,还返回与A记录对应的IPv4映射的IPv6地址。

(7)AI_ADDRCONFIG:按所在主机的接口配置选择返回的地址类型,只有某非环回接口配置了IPv6地址时,才会查找IPv6地址,IPv4地址同理。

2.ai_family:某个AF_XXX值。

3.ai_socktype:某个SOCK_XXX值,如SOCK_STREAM。

4.ai_protocol。

如果hints参数是一个空指针,getaddrinfo函数假设ai_flag、ai_socktype、ai_protocol的值为0,ai_family的值为AF_UNSPEC。

如果getaddrinfo函数返回成功(即返回值为0),那么由result函数指向的变量已被填入一个指针,该指针指向由addrinfo结构中的ai_next成员串接起来的addrinfo结构链表。以下情形会返回多个addrinfo结构:
1.与hostname参数关联的地址有多个,则所请求地址族(通过hints结构的ai_family成员设置)的每个地址都返回一个对应addrinfo结构。

2.service参数指定的服务支持多个套接字类型,则每个套接字类型都可能返回一个对应结构,具体取决于ai_socktype成员。多数getaddrinfo函数实现只返回ai_socktype成员请求的套接字类型的信息,如果没有这个成员,就返回一个错误。

一个例子,如果没有任何hints信息,请求查找有两个IP地址的某主机上的domain服务,将返回4个addrinfo结构:
1.第一个IP+SOCK_STREAM套接字类型。

2.第一个IP+SOCK_DGRAM套接字类型。

3.第二个IP+SOCK_STREAM套接字类型。

4.第二个IP+SOCK_DGRAM套接字类型。

上例返回的信息如下图,addrinfo结构的先后顺序没有保证:
在这里插入图片描述
上图是以下代码的执行返回:

struct addrinfo hints, *res;

bzero(&hints, sizeof(hints));
hints.ai_flags = AI_CANONNAME;
hints.ai_family = AF_INET;

getaddrinfo("freebsd4", "domain", &hints, &res);

上图中,除res变量外的所有内容都是由getaddrinfo函数动态分配的空间(如来自malloc调用)。我们假设主机freebsd4的规范名字是freebsd4.unpbook.com,且它在DNS中有2个IPv4地址。

上图中,端口53用于domain服务,这个端口号在套接字地址结构中按网络字节序存放。getaddrinfo函数返回的ai_protocol值为IPPROTO_TCP或IPPROTO_UDP。如果返回的addrinfo结构中的ai_family和ai_socktype的组合能完全指定TCP或UDP协议,则返回的ai_protocol为0也可接受,例如,当返回的ai_socktype值为SOCK_STREAM且系统没有实现除TCP外的其他SOCK_STREAM协议(如SCTP)时,套接字类型值为SOCK_STREAM的那两个addrinfo结构的协议值字段ai_protocol可为0。getaddrinfo函数最安全的做法是始终明确返回特定协议。

尽管没有保证,但一个getaddrinfo函数应该按DNS返回顺序返回各个IP地址。有些解析器允许系统管理员在/etc/resolv.conf文件中指定地址的排序顺序。IPv6在RFC中指定了地址选择规则(这些规则确定了源地址的选择顺序,以及在选择源地址时考虑的因素),可能会影响到getaddrinfo函数返回地址的顺序。

addrinfo结构中的信息可直接用于socket调用,以及之后的connect、sendto函数(客户端)和bind函数(服务器)。socket函数的参数就是addrinfo结构中的ai_family、ai_socktype、ai_addr成员;connect或bind函数的第二个和第三个参数就是该结构中的ai_addr(一个指向适当类型套接字地址结构的指针,地址结构的内容由getaddrinfo函数填写)和ai_addrlen(这个套接字地址结构的大小)成员。

如果hints结构中设置了AI_CANONNAME,则getaddrinfo函数返回的第一个addrinfo结构的ai_canonname成员指向所查找主机的规范名字。规范名字通常是FQDN。telnet之类的客户程序使用此标志显示所连接到主机的规范名字,这样即使用户给定的是一个简单名字或别名,也能知道真正查找的名字。
在这里插入图片描述
如上图,在不考虑SCTP的前提下,只有在未提供ai_socktype暗示信息且该服务支持多个运输层协议时才可能为每个IP地址返回多个addrinfo结构。

以下是getaddrinfo函数常见的输入:
1.指定hostname和service参数。TCP客户可在一个循环中针对每个返回的IP地址,逐一调用socket和connect,直到有一个连接成功,或所有地址尝试完毕。UDP客户可将由getaddrinfo函数填入的套接字地址结构用于调用sendto和connect,如果客户能判定第一个地址看起来不工作(如已连接UDP套接字上收到出错消息、未连接套接字上接收应答消息超时),则可以尝试其余地址。

如果客户只处理一种类型的套接字(如Telnet和FTP客户只处理TCP,TFTP客户只处理UDP),则应设置hints参数结构的ai_socktype成员。

2.典型的服务器进程只指定service参数,不指定hostname参数,同时在hints结构中指定AI_PASSIVE标志。返回的套接字地址结构中应该只有一个值为INADDR_ANY或INADDR6_ANY_INIT的IP地址。TCP服务器随后调用socket、bind、listen。如果服务器想malloc另一个套接字地址结构以从accept函数获取客户地址,则返回的ai_addrlen的值给出了这个套接字地址结构的大小。

如果服务器只处理一种类型的套接字,则应设置hints参数结构中的ai_socktype成员,可避免返回不需要的结构。

对于IPv6,INADDR6_ANY_INIT常量和in6addr_any变量是不同的,它们的作用都表示任意IPv6地址,但IN6ADDR_ANY_INIT是一个常量表达式,可以用于静态初始化IPv6地址,而in6addr_any是一个类型为in6_addr的全局变量,可以在赋值中使用。它们都以网络字节序表示。这两个IPv6通配地址的使用方式如下:

// 使用in6addr_any
struct sockaddr_in6 addr;
memset(&addr, 0, sizeof(addr));
addr.sin6_family = AF_INET6;
addr.sin6_addr = in6addr_any;

// 使用INADDR6_ANY_INIT进行静态初始化
struct sockaddr_in6 addr = {
    .sin6_family = AF_INET6,
    .sin6_addr = IN6ADDR_ANY_INIT
};

以上静态初始化过程中,每个成员字段前都有一个点号,这是称为designated initializer的赋值方式,它可以显式指定每个成员的赋值。如果有以下类:

struct A { int x; char c; };

则以下初始化方式相同:

// 按声明顺序,1赋值给字段x,'c'赋值给字段c
struct A a1 = {1, 'c'};
// 显式指定各个字段的赋值
struct A a2 = {
    .c = 'c', 
    .i = 1
};

3.之前的例子中,TCP服务器仅创建一个监听套接字,UDP服务器也只创建一个数据报套接字,但服务器可能会使用select或poll函数让服务器进程处理多个套接字,此时,服务器应遍历getaddrinfo函数返回的所有addrinfo结构链表,并为每个结构创建一个套接字,再使用select或poll函数。

但这样做的技术问题在于,getaddrinfo函数返回多个结构的原因之一是有服务可同时由IPv4和IPv6处理,这两个版本的IP协议并非完全独立,如果我们为某给定端口创建了一个IPv6监听套接字,则没有必要再为同一个端口再创建一个IPv4套接字,因为来自IPv4客户的连接将由协议栈和IPv6监听套接字处理(假设IPV6_V6ONLY套接字选项未设置且系统配置正确(如Linux中,要将net.ipv6.bindv6only设为0))。

getaddrinfo函数可方便我们编写协议无关的代码,它的反义函数为getnameinfo,把套接字地址结构转换成主机名和服务名。
在这里插入图片描述
gai_strerror函数以getaddrinfo函数返回的非0错误值为参数,返回一个指向对应的出错信息串的指针:
在这里插入图片描述
getaddrinfo函数返回的所有空间都是动态获取的,包括addrinfo结构、ai_addr结构、ai_canonname字符串,这些空间通过freeaddrinfo函数还给系统:
在这里插入图片描述
ai参数应指向由getaddrinfo函数返回的第一个addrinfo结构,这个链表中的所有结构及它们指向的动态存储空间都被释放掉。

假设我们调用getaddrinfo,遍历返回的addrinfo结构链表后找到了所需结构,如果我们为保存所需结构而仅仅复制了这个addrinfo结构本身而没有复制结构中的ai_addr结构、ai_canonname字符串,然后调用freeaddrinfo,就出现了错误,因为addrinfo结构中有指针字段指向动态分配的内存空间,因此我们复制的结构中的指向动态分配的内存的指针字段所指向的内存已被还给系统。

只复制这个addrinfo结构而不复制由它指向的其他结构称为浅复制,既复制这个addrinfo结构又复制由它指向的所有其他结构称为深复制。

POSIX规范定义了getaddrinfo函数以及该函数为IPv4和IPv6返回的信息。对于getaddrinfo函数,我们需要注意以下几点:
1.getaddrinfo函数会处理:调用者想要的地址结构类型(sockaddr_in或sockaddr_in6);在DNS或其他数据库中搜索的记录类型(A记录或AAAA记录)。

2.由调用者在hints结构中提供的地址族来指定调用者期待返回的套接字地址结构的类型,如果调用者指定的是AF_INET,getaddrinfo函数就不能返回sockaddr_in6结构;如果调用者指定AF_INET6,getaddrinfo函数就不能返回sockaddr_in结构。

3.如果调用者指定的地址族为AF_UNSPEC,则getaddrinfo函数返回适用于指定主机名和服务名的任意协议族的地址。如果某个主机既有AAAA记录,又有A记录,则AAAA记录将作为sockaddr_in6结构返回,A记录将作为sockaddr_in结构返回。但不会为IPv4地址返回sockaddr_in6结构(IPv4映射的IPv6地址),因为这么做没有提供额外信息,这些地址已经在sockaddr_in结构中返回过了。

4.如果设置了AI_PASSIVE标志但没有指定主机名,则IPv6通配地址(IN6ADDR_ANY_INIT或0::0)会作为sockaddr_in6结构返回,IPv4通配地址(INADDR_ANY或0.0.0.0)会作为sockaddr_in结构返回。且会先返回IPv6通配地址,因为双栈主机上IPv6服务器能同时处理IPv6客户和IPv4客户。

IPv6通配地址的缩写规则:
(1)在每个四位组中,删除开头和连续的0(0001 -> 1,0000 -> 0,0092 -> 92)。

(2)如果有两个或多个连续的全0四位组,则用双冒号(::)替换它,但在单个地址中只能使用一次双冒号。

因此0::0表示全0的IPv6地址,也可直接用::表示。

5.hints结构中的ai_family成员指定的地址族和ai_flags成员指定的AI_V4MAPPED、AI_ALL标志决定了在DNS中查找的资源记录类型(A、AAAA),也决定了返回地址的类型(IPv4、IPv6、IPv4映射的IPv6)。

6.主机名参数也可以是IPv6的十六进制数串或IPv4的点分十进制数串,如果指定AF_INET,就不会接受IPv6的十六进制数串;如果指定AF_INET6,就不会接受IPv4的点分十进制数串;如果指定的是AF_UNSPEC,则两种数串都能接受,返回的是数串相应类型的套接字地址结构。

可能有人会争论说,如果指定了AF_INET6,则点分十进制数串会作为IPv4映射的IPv6地址在sockaddr_in6结构中返回,这是正确的,但另一个方法也可获得同样的结果,就是在点分十进制数串前加上0::ffff:,并指定AF_INET6。
在这里插入图片描述
上图中,结果列是在给定前三列后,该函数返回给调用者的结果,行为列说明函数如何获取这些结果。

使用一个测试程序展示getaddrinfo函数,该程序允许我们输入:主机名、服务名、地址族、套接字类型、AI_CANONNAME、AI_PASSIVE。该程序输出getaddrinfo函数返回的addrinfo结构中的信息,以及用该结构中信息如何调用socket,以下是与图11-5同样的例子:
在这里插入图片描述
如上图,-f指定地址族,-c表示返回规范主机名,-h指定主机名,-s指定服务名。

通常的客户情景会查询地址族、套接字类型(-t选项)、主机名、服务名,以下是对一个有3个IPv4地址的多宿主机查询这些:
在这里插入图片描述
指定主机为aix,它有1个AAAA记录和1个A记录,不指定地址族,但指定服务名为ftp,此服务仅在TCP上提供:
在这里插入图片描述
最后我们指定AI_PASSIVE标志(-p选项),但不指定地址族,也不指定主机名(即使用通配地址),指定端口号为8888,指定套接字类型为SOCK_STREAM:
在这里插入图片描述
上图返回了2个结构(分别是IPv4和IPv6的通配地址,且IPv6地址结构早于IPv4地址结构返回,因为双栈主机上IPv6既能与IPv6对端通信,也能与IPv4对端通信),因为我们是在一个同时支持IPv6和IPv4的主机上运行此例,且没有指定地址族。

可以自定义一个getaddrinfo函数的接口函数,使得我们每次调用getaddrinfo时不用再分配并填写hints结构,而是将我们感兴趣的两个字段(地址族和套接字类型)作为该函数的参数:
在这里插入图片描述
以下是该函数的源码:

#include "unp.h"

struct addrinfo *host_serv(const char *host, const char *serv, int family, int socktype) {
    int n;
    struct addrinfo hints, *res;

    bzero(&hints, sizeof(struct addrinfo));
    hints.ai_flags = AI_CANONNAME;    /* always return canonical name */
    hints.ai_family = family;    /* AF_UNSPEC, AF_INET, AF_INET6, etc. */
    hints.ai_socktype = socktype;    /* 0, SOCK_STREAM, SOCK_DGRAM, etc. */

    if ((n = getaddrinfo(host, serv, &hints, &res)) != 0) {
        return NULL;
    }
    
    return res;    /* return pointer to first on linked list */
}

编写一个使用getaddrinfo函数的函数,用于创建一个TCP套接字并连接到一个服务器:
在这里插入图片描述
以下是该函数的源码:

#include "unp.h"

int tcp_connect(const char *host, const char *serv) {
    int sockfd, n;
    struct addrinfo hints, *res, *ressave;

    bzero(&hints, sizeof(struct addrinfo));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    if ((n = getaddrinfo(host, serv, &hints, &res)) != 0) {
        err_quit("tcp_connect error for %s, %s: %s", host, serv, gai_strerror(n));
    }
    ressave = res;
    
    do {
        sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
        // socket函数失败不是致命错误,如果返回地址中有IPv6地址而主机内核不支持IPv6,这种失败就可能发生
        if (sockfd < 0) {
            continue;    /* ignore this one */
        }
        
        if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0) {
            break;    /* success */
        }
        
        Close(sockfd);    /* ignore this one */
    } while ((res = res->ai_next) != NULL);
    
    if (res == NULL) {    /* errno set from final connect() */
        err_sys("tcp_connect error for %s, %s", host, serv);
    }
    
    freeaddrinfo(ressave);
    
    return sockfd;
}

以上函数和其他我们自己编写的getaddrinfo函数的接口函数都没有返回getaddrinfo函数返回的错误码(某EAI_xxx常值),这意味着它们的包裹函数什么都没做:

int Tcp_connect(const char *host, const char *serv) {
    return tcp_connect(host, serv);
}

尽管如此,为保持全书一致性,我们照样使用包裹函数。

以上问题在于tcp_connect函数返回的描述符是非负的,但我们不清楚EAI_xxx是正的还是负的,如果这些值也是正的,那么我们可以在getaddrinfo函数失败时返回这些值的相反数(负值),但我们还需要某个负值还表明所有结构都尝试完毕,但无一成功。

把获取时间的客户程序改写成使用tcp_connect函数:

#include "unp.h"

int main(int argc, char **argv) {
    int sockfd, n;
    char recvline[MAXLINE + 1];
    socklen_t len;
    struct sockaddr_storage ss;
    if (argc != 3) {
       err_quit("usage: daytimetcpconnect <hostname/IPaddress> <service/port#>");
    }
    sockfd = Tcp_connect(argv[1], argv[2]);
    len = sizeof(ss);
    Getpeername(sockfd, (SA *)&ss, &len);
    // 自定义函数sock_ntop_host中调用了inet_ntop,从而以协议无关的方式将地址从二进制格式转换为可读形式
    printf("connected to %s\n", Sock_ntop_host((SA *)&ss, len));
    while ((n = Read(sockfd, recvline, MAXLINE)) > 0) {
        recvline[n] = 0; /* null terminate */
        Fputs(recvline, stdout);
    }
    exit(0);
}

tcp_connect函数只返回了连接后的套接字,而我们不知道具体套接字地址结构的大小,因此我们调用getpeername时使用了sockaddr_storage类型的套接字地址结构,它足以存放系统支持的任何套接字地址类型,又能满足它们的对齐限制。

以上版本的客户程序同时支持IPv4和IPv6。

运行以上程序,指定一个只支持IPv4的主机名:
在这里插入图片描述
再次运行该程序,指定一个同时支持IPv4和IPv6的主机名aix:
在这里插入图片描述
上图使用IPv6的原因在于,该主机同时有1个AAAA记录和一个A记录,且tcp_connect函数把地址族设为AF_UNSPEC,getaddrinfo函数会先搜索AAAA记录,再搜索A记录,connect顺序靠前的IPv6地址成功后,就不再尝试顺序靠后的IPv4地址。

下例中,我们指定aix-4为主机名,从而强制使用IPv4地址,这是一个约定,这样的主机名只有A记录:
在这里插入图片描述
recv函数的MSG_PEEK标志可读出接收缓冲区中的数据,但并不从接收缓冲区中移除读到的数据;以FIONREAD参数调用ioctl函数可获取套接字缓冲区中的字节数。修改以上程序,先指定MSG_PEEK标志调用recv,然后以FIONREAD为参数调用ioctl,最后再调用read真正读入数据,以下是修改后的循环:

for (; ; ) {
    if ((n = Recv(sockfd, recvline, MAXLINE, MSG_PEEK)) == 0) {
        break;
    }
    
    Ioctl(sockfd, FIONREAD, npend);
    printf("%d bytes from PEEK, %d bytes pending\n", n, npend);
    
    n = Read(sockfd, recvline, MAXLINE);
    recvline[n] = 0;    /* null terminate */
    Fputs(recvline, stdout);
}

下一个我们自己编写的函数会执行TCP服务器的通常步骤:创建一个TCP套接字,给它绑定服务器的众所周知端口,并允许接受外来请求:
在这里插入图片描述
以下是它的源码:

#include "unp.h"

int tcp_listen(const char *host, const char *serv, socklen_t *addrlenp) {
    int listenfd, n;
    const int on = 1;
    struct addrinfo hints, *res, *ressave;
    
    bzero(&hints, sizeof(struct addrinfo));
    hints.ai_flags = AI_PASSIVE;    // 本函数供服务器使用
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_STREAM;

    // 如果不指定主机名,则返回的地址为通配地址
    // 对于AI_PASSIVE和AI_UNSPEC暗示,IPv4和IPv6的地址都会返回(假定运行在双栈主机上)
    if ((n = getaddrinfo(host, serv, &hints, &res)) != 0) {
        err_quit("tcp_listen error for %s, %s: %s", host, serv, gai_strerror(n));
    }
    ressave = res;

    do {
        listenfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
        if (listenfd < 0) {
            continue;    /* error, try next one */
        }
        
        Setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
        if (bind(listenfd, res->ai_addr, res->ai_addrlen) == 0) {
            break;    /* success */
        }
        
        Close(listenfd);    /* bind error, close and try next one */
    } while ((res = res->ai_next) != NULL);
    
    // 如果每个地址结构的socket调用或bind调用都失败,显示一个出错消息并终止
    // err_sys函数中会调用exit
    if (res == NULL) {    /* errno from final socket() or bind() */
        err_sys("tcp_listen error for %s, %s", host, serv);
    }
    
    Listen(listenfd, LISTENQ);
    
    // 返回协议地址大小,此值允许调用者在调用accept获取客户协议地址时分配一个套接字地址结构的内存空间
    if (addrlenp) {
        *addrlenp = res->ai_addrlen;    /* return sizeof protocol address */
    }
    
    freeaddrinfo(ressave);
    
    return listenfd;
}

在tcp_listen函数中,调用者必须传一个addrlenp参数来获取协议地址的大小,如果调用者传一个空指针,调用者如果想知道协议地址的大小,可以分配一个比任何套接字地址结构都要大的大缓冲区并调用getsockname,getsockname函数的第三个参数是值-结果参数,它会返回协议地址真正的大小。

使用tcp_listen函数改写时间获取服务器:

#include "unp.h"
#include <time.h>

int main(int argc, char **argv) {
    int listenfd, connfd;
    socklen_t len;
    char buff[MAXLINE];
    time_t ticks;
    struct sockaddr_storage cliaddr;
    
    // 虽然daytime服务器的众所周知端口是13,此处我们还是用一个命令行参数来指定服务名或端口号
    // 因为daytime服务器的端口号是保留端口号,绑定需要超级用户特权,我们为了便于测试,将其绑定在非保留端口
    if (argc != 2) {
        err_quit("usage:daytimetcpsrv1 <service or port#>");
    }
    
    // 我们将使用sockaddr_storage结构,它足以容纳任何支持的套接字地址结构,因此第3个参数(地址结构长度)填空指针
    listenfd = Tcp_listen(NULL, argv[1], NULL);
    
    for (; ; ) {
        len = sizeof(cliaddr);
        connfd = Accept(listenfd, (SA *)&cliaddr, &len);
        // sock_ntop函数是自定义函数,它输出客户的地址,无论是IPv4还是IPv6地址,都会输出IP地址和端口号
        printf("connection from %s\n", Sock_ntop((SA *)&cliaddr, len));
        // 此处我们也可输出客户的主机名,我们可用getnameinfo函数尝试获取客户主机的主机名
        // 但这会涉及DNS的PTR记录查询,需要花一段时间,特别是在查询失败时
        // 且TCPv3指出,与web服务器建立连接的所有客户主机中,25%没有PTR记录
        // 因为我们不想让服务器(特别是迭代服务器)为PTR查询等待数秒,我们就不显示客户的主机名了,只显示客户的IP和端口号
        
        // 调用time获取UNIX时间戳
        ticks = time(NULL);
        // ctime函数将UNIX时间戳转换为可读的时间格式
        snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
        Write(connfd, buff, strlen(buff));
        
        Close(connfd);
    }
}

上例中,我们可以改用getnameinfo函数替代sock_ntop函数,具体做法可以是先分配存放主机名和服务名的数组:

// NI_MAXHOST和NI_MAXSERV常量定义在netdb.h头文件中,分别代表主机名和服务名的最大长度
// 用于动态分配足够的缓冲区来存储主机名和服务名
char host[NI_MAXHOST], serv[NI_MAXSERV];

然后在accept函数返回后改为调用getnameinfo以取代sock_ntop函数:

// NI_NUMERICHOST表示使getnameinfo函数返回IP地址而非主机名,避免DNS PTR记录查询
// NI_NUMERICHOST表示使getnameinfo函数返回端口号而非服务名,避免端口号的解析(通常是查找/etc/services文件)
// 只需要数值形式的原因为,这是服务器程序,通常不需要可读形式的主机名或服务名
if (getnameinfo(cliaddr, len, host, NI_MAXHOST, serv, NI_MAXSERV, NI_NUMERICHOST | NI_NUMERICSERV) == 0) {
    printf("connection from %s.%s", host, serv);
}

上例中,我们调用tcp_listen时第一个参数是NULL,且我们指定的地址族为AF_UNSPEC,这在双栈主机上返回的第一个套接字地址结构将是IPv6的,但我们可能希望服务器仅处理IPv4。

对于客户主机没有这种问题,因为客户需要输入一个IP地址或主机名,因此客户可以输入一个与特定类型的IP地址关联的主机名(即-4或-6后缀的主机名),要么直接输入IPv4的点分十进制串或IPv6的十六进制数串以强制使用IPv4或IPv6。

有一个技巧可以让我们强制以上这种调用tcp_listen函数的服务器使用某个给定的IP协议版本,我们可以让用户输入一个本地IP地址或主机名(如想用IPv4,就用以-4为后缀的主机名,IPv6同理)。

因此我们可以把服务器程序改为接收一个可选参数,如果键入:

% server

在双栈主机上就默认使用IPv6。

如果键入:

% server 0.0.0.0

则显式指定使用IPv4。

键入:

% server 0::0

则显式指定IPv6。

以下是获取时间服务器的最终版本,它允许用户指定一个主机名或IP地址供服务器捆绑:

#include "unp.h"
#include <time.h>

int main(int argc, char **argv) {
    int listenfd, connfd;
    socklen_t len, addrlen;
    char buff[MAXLINE];
    time_t ticks;
    struct sockaddr_storage cliaddr;

    if (argc == 2) {
        listenfd = Tcp_listen(NULL, argv[1], &addrlen);
    } else if (argc == 3) {
        // 在tcp_listen函数中指定AI_PASSIVE和AF_UNSPEC的条件下:
        // 如果host选项指定的是点分十进制串,则getaddrinfo函数只返回sockaddr_in结构
        // 如果host选项指定的是十六进制数串,则getaddrinfo函数只返回sockaddr_in6结构
        // 如果host选项指定的是以-4结尾的主机名,则按照惯例,该主机名应该只有A记录
        // 如果host选项指定的是以-6结尾的主机名,则按照惯例,该主机名应该只有AAAA记录
        listenfd = Tcp_listen(argv[1], argv[2], &addrlen);
    } else {
        err_quit("usage: daytimetcpser2 [<host>] | <service or port>");
    }

    for (; ; ) {
        len = sizeof(cliaddr);
		connfd = Accept(listenfd, (SA *)&cliaddr, &len);
		printf("connection from %s\n", Sock_ntop((SA *)&cliaddr, len));
	
		ticks = time(NULL);
		snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
		Write(connfd, buff, strlen(buff));
	
		Close(connfd);
    }
}

运行以上程序,以IPv4套接字启动服务器:
在这里插入图片描述
以IPv6套接字启动服务器:
在这里插入图片描述
如上图,后两个连接使用的是IPv4映射的IPv6地址,这展示了运行在双栈主机上的IPv6服务器既能处理IPv4客户,又能处理IPv6客户。

对于inet_pton函数,其第1个参数(协议族)和第2个参数(可读形式的IP地址)必须是匹配的,否则函数会失败:
在这里插入图片描述
自编写的使用getaddrinfo函数创建未连接UDP套接字的函数:
在这里插入图片描述
以上函数创建一个未连接UDP套接字,并返回3项数据,首先,返回值是该套接字的描述符;其次,saptr参数是指向某个由udp_client函数动态分配的套接字地址结构的指针的地址,本函数把目的IP和端口存放在这个结构中,用于稍后调用sendto;最后,这个套接字地址结构的大小在lenp参数指向的变量中返回,此参数不能是空指针,因为sendto和recvfrom调用都需要知道套接字地址结构的长度。

以下是udp_client函数的源码:

#include "unp.h"

int udp_client(const char *host, const char * serv, SA **saptr, socklen_t *lenp) {
    int sockfd, n;
    struct addrinfo hints, *res, *ressave;

    bzero(&hints, sizeof(struct addrinfo));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_DGRAM;

    if ((n = getaddrinfo(host, serv, &hints, &res)) != 0) {
        err_quit("udp_client error for %s, %s: %s", host, serv, gai_strerror(n));
    }
    ressave = res;

    do {
        sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
		if (sockfd >= 0) {
		    break;    /* success */
		}
    } while ((res = res->ai_next) != NULL);

    if (res == NULL) {    /* errno set from final socket() */
        err_sys("udp_client error for %s, %s", host, serv);
    }

    *saptr = Malloc(res->ai_addrlen);
    memcpy(*saptr, res->ai_addr, res->ai_addrlen);
    *lenp = res->ai_addrlen;

    freeaddrinfo(ressave);

    return sockfd;
}

把获取时间的客户程序改为使用UDP和udp_client函数:

#include "unp.h"

int main(int argc, char **argv) {
    int sockfd, n;
    char recvline[MAXLINE + 1];
    socklen_t salen;
    struct sockaddr *sa;

    if (argc != 3) {
        err_quit("usage: daytimeudpcli1 <hostname/IPaddress> <service/port#>");
    }

    sockfd = Udp_client(argv[1], argv[2], (void **)&sa, &salen);

    printf("sending to %s\n", Sock_ntop_host(sa, salen));

    // 发送一个null字符
    Sendto(sockfd, "", 1, 0, sa, salen);    /* send 1-byte datagram */

    n = Recvfrom(sockfd, recvline, MAXLINE, 0, NULL, NULL);
    recvline[n] = '\0';    /* null terminate */
    Fputs(recvline, stdout);

    exit(0);
}

运行以上程序,我们先指定一个拥有AAAA记录和A记录各1个的主机名,由于getaddrinfo函数首先返回的是对应AAAA记录的结构,所以应该创建一个IPv6套接字:
在这里插入图片描述
接着我们指定一个点分十进制地址:
在这里插入图片描述
我们自编写的创建一个已连接UDP套接字的函数:
在这里插入图片描述
由于udp_connect函数创建的是已连接UDP套接字,因此相比于udp_connect函数,udp_client函数结尾的两个参数就不需要了,调用者可用write函数代替sendto函数。

以下是udp_connect函数的源码:

#include "unp.h"

int udp_connect(const char *host, const char *serv) {
    int sockfd, n;
    struct addrinfo hints, *res, *ressave;

    bzero(&hints, sizeof(struct addrinfo));
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_DGRAM;

    if ((n = getaddrinfo(host, serv, &hints, &res)) != 0) {
        err_quit("udp_connect error for %s, %s: %s", host, serv, gai_strerror(n));
    }
    ressave = res;

    do {
        sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
		if (sockfd < 0) {
		    continue;    /* ignore this one */
		}
	
		if (connect(sockfd, res->ai_addr, res->ai_addrlen) == 0) {
		    break;    /* success */
		}
	
		Close(sockfd);    /* ignore this one */
    } while ((res = res->ai_next) != NULL);

    if (res == NULL) {    /* errno set from final connect() */
        err_sys("udp_connect error for %s, %s", host, serv);
    }

    freeaddrinfo(ressave);

    return sockfd;
}

以上函数几乎与tcp_connect函数相同,两者差别之一是UDP套接字上的connect调用不会发送任何东西到对端,如果存在错误(如对端不可达或指定端口上没有服务器),调用者就得等到向对端发送一个数据报后才能发现。

自编写的调用getaddrinfo的UDP接口函数udp_server:
在这里插入图片描述
此函数的参数与tcp_listen函数一样,有一个可选的hostname参数和一个必选的service参数,以及一个可选的指向某个socklen_t类型变量的指针,用于返回套接字地址结构的大小。

udp_server函数的源码:

#include "unp.h"

int udp_server(const char *host, const char *serv, socklen_t *addrlenp) {
    int sockfd, n;
    struct addrinfo hints, *res, *ressave;

    bzero(&hints, sizeof(struct addrinfo));
    hints.ai_flags = AI_PASSIVE;
    hints.ai_family = AF_UNSPEC;
    hints.ai_socktype = SOCK_DGRAM;

    if ((n = getaddrinfo(host, serv, &hints, &res)) != 0) {
        err_quit("udp_server error for %s, %s: %s", host, serv, gai_strerror(n));
    }
    ressave = res;

    do {
        sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
		if (sockfd < 0) {
		    continue;    /* error - try next one */
		}
	
		if (bind(sockfd, res->ai_addr, res->ai_addrlen) == 0) {
		    break;    /* success */
		}
	
		Close(sockfd);    /* bind error - close and try next one */
    } while ((res = res->ai_next) != NULL);

    if (res == NULL) {    /* errno from final socket() or bind() */
        err_sys("udp_server error for %s, %s", host, serv);
    }

    if (addrlenp) {
        *addrlenp = res->ai_addrlen;    /* return size of protocol address */
    }

    freeaddrinfo(ressave);

    return sockfd;
}

除了没有调用listen外,以上函数几乎等同于tcp_listen函数。我们把以上函数中的地址族设置成AF_UNSPEC,但调用者可用前面说到的技巧强制使用IPv4或IPv6。

以上代码中,对于UDP套接字我们不设置SO_REUSEADDR选项,本套接字选项允许在支持多播的主机上把同一UDP端口捆绑到多个套接字上。既然UDP套接字没有类似于TCP的TIME_WAIT的状态,所以启动服务器时就没有设置这个套接字选项的必要。

修改时间获取服务器程序,使其使用udp_server函数:

#include "unp.h"
#include <time.h>

int main(int argc, char **argv) {
    int sockfd;
    ssize_t n;
    char buff[MAXLINE];
    time_t ticks;
    socklen_t len;
    struct sockaddr_storage cliaddr;

    if (argc == 2) {
        sockfd = Udp_server(NULL, argv[1], NULL);
    } else if (argc == 3) {
        sockfd = Udp_server(argv[1], argv[2], NULL);
    } else {
        err_quit("usage: daytimeudpsrv [<host>] <service or port>");
    }

    for (; ; ) {
        len = sizeof(cliaddr);
		n = Recvfrom(sockfd, buff, MAXLINE, 0, (SA *)&cliaddr, &len);
		printf("datagram from %s\n", Sock_ntop((SA *)&cliaddr, len));
	
		ticks = time(NULL);
		snprintf(buff, sizeof(buff), "%.24s\r\n", ctime(&ticks));
		Sendto(sockfd, buff, strlen(buff), 0, (SA *)&cliaddr, len);
    }
}

对于以上程序,如果我们在一个窗口中启动该服务器的一个实例,给它捆绑通配地址和某个端口;然后在另一窗口中启动一个客户,验证服务器可以正常处理客户请求(服务器会进行printf调用);然后在第三个窗口中启动服务器的另一个实例,给它绑定该主机的一个单播地址和与第一个服务器相同的端口,此时会无法绑定与第一个服务器相同的端口,这是因为没有设置SO_REUSEADDR套接字选项,最容易的解决办法是复制一份udp_server函数,把它命名为udp_server_reuseaddr,然后由它设置这个套接字选项,再让服务器调用这个新函数。修复该问题后,重新启动第二个服务器,启动一个客户,可以验证第二个服务器已盗用了第一个服务器的端口。然后关闭第二个服务器,然后使用一个不同的账号登录,并再次启动第二个服务器,看能否继续成功盗用,有些厂商只允许用户ID相同的进程再次捆绑之前某个进程捆绑过的端口。

getnameinfo函数是getaddrinfo函数的互补函数,它以一个套接字地址为参数,返回描述其中主机的字符串和描述其中服务的另一字符串,本函数以协议无关方式提供这些信息:
在这里插入图片描述
sockaddr参数指向一个套接字地址结构,其中包含待转换成直观可读字符串的协议地址,addrlen参数是这个结构的长度。该结构及其长度通常由accept、recvfrom、getsockname、getpeername函数返回。

待返回的2个直观可读字符串由调用者预先分配空间,host和hostlen参数指定主机名字符串,serv和servlen参数指定服务名字符串。如果不想返回某个串,需要将其对应的len参数设为0。

sock_ntop函数和getnameinfo函数的差别在于,前者不涉及DNS,只返回IP地址和端口号的一个可显示版本,而后者尝试获取主机和服务的名字。

以下标志可改变getnameinfo函数的操作:
在这里插入图片描述
当知道处理的套接字地址结构是用于数据报时,调用者应设置NI_DGRAM标志,因为套接字地址结构中只有IP地址和端口号,getnameinfo函数无法确定所用协议是TCP还是UDP。有很多端口号在TCP上用于某服务,但在UDP上用于另一个服务,如端口514,它在TCP上提供rsh服务,在UDP上提供syslog服务。

如果无法使用DNS反向解析出主机名,NI_NAMEREQD标志将导致返回一个错误。有些服务器需要把客户的IP地址映射为主机名,然后以返回的主机名调用gethostbyname,来验证得到的地址与调用getnameinfo函数指定的套接字地址结构中的地址确实是相同的。

NI_NOFQDN标志导致返回的主机名第一个点号之后的内容被截去,如gethostbyaddr返回的某IP地址的主机名是aix.unpbook.com,则设置此标志的getnameinfo函数返回的主机名是aix。

NI_NUMERICHOST标志告知getnameinfo函数不要调用DNS,因为调用DNS可能耗时,而是以可读的数值字符串形式返回IP地址(可能通过inet_ntop函数实现)。NI_NUMERICSERV标志指定以十进制数字符串返回端口号。NI_NUMERICSCOPE标志指定以数值字符串返回范围标识,只有IPv6使用它有意义,它将数值形式的IPv6地址范围返回给调用方,如IPv6地址fe80::1%eth0,其中%eth0就是范围标识符,如果指定了此标志,会返回接口eth0的数值索引。对于客户的端口(临时端口),应设置NI_NUMERICSERV标志。

我们可以把以上标志中有意义的组合逻辑或在一起。

gethostbyname和gethostbyaddr函数是不可重入的,函数的源码大体如下:
在这里插入图片描述
上图显示,3个函数共用一个static的host变量(gethostbyname2函数是gethostbyname函数的进化版本,它是在BIND 4.9.4中为支持IPv6引入的,现已被淘汰)。

在一个UNIX进程中发生重入问题的条件是,它的主控制流和某信号处理函数中同时调用gethostbyname或gethostbyaddr:
在这里插入图片描述
如果主控制流被SIGALRM信号暂停时正处于执行gethostbyname函数期间,如主控制流该函数已填写好host变量并即将返回,但此时信号处理函数中会调用gethostbyname,host变量将被重用,因为该进程中只存在该变量的单个副本。这样原来由主控制流计算出的值被重写成了由信号处理函数计算出的值。

关于重入问题注意以下几点:
1.在过去,gethostbyname、gethostbyaddr、getservbyname、getservbyport函数不可重入,因为它们返回指向一个静态结构的指针。

支持线程的一些实现(如Solaris 2.x)还提供这4个函数的可重入版本,它们的名字以_r结尾。

支持线程的另一些实现(如HP-UX 10.30及以后版本)使用线程特定数据提供这些函数的可重入版本。

2.inet_pton、inet_ntop函数总是可重入的。

3.在过去,inet_ntoa函数是不可重入的,但支持线程的一些实现提供了使用线程特定数据的可重入版本。

4.getaddrinfo函数可重入的前提是它调用的函数都可重入,即它应该调用可重入版本的gethostbyname函数以解析主机名,调用可重入版本的getservbyname函数以解析服务名。getaddrinfo函数动态分配结果结构是令该函数变得可重入的原因。

5.getnameinfo函数可重入的前提是它调用的函数都可重入,即它应该调用可重入版本的gethostbyaddr函数以反向解析主机名,调用可重入版本的getservbyport函数以反向解析服务名。getnameinfo函数的2个结果字符串(主机名和服务名)由调用者分配存储空间,从而使它可重入。

上述gethostbyname函数的问题可以通过不在信号处理函数中调用不可重入的函数来解决。

errno变量也有同样问题,这个整型变量在过去每个进程各有一个副本,如果一个进程执行的某个系统调用返回错误,该进程的errno变量就被存入一个整数错误码,例如,当调用C标准库的close函数时,进程可能执行如下伪代码:
1.把系统调用的参数(一个整数描述符)置于一个寄存器。

2.把一个值置于另一个寄存器,以指出close系统调用将被调用。

3.调用该系统调用(用一条特殊指令切换到内核态)。

4.测试一个寄存器的值以判定是否发生过某个错误。

5.如果没有错误则执行return 0。

6.否则把另外某个寄存器的值存入errno。

7.执行return -1。

我们注意到如果没有错误发生,则errno的值不会变,因此除非发生了错误(通常由函数返回-1指示),否则不应查看errno的值。

如果一个程序先测试close函数的返回值,判定发生了错误后,再显示errno的值:

if (close(fd) < 0) {
    fprintf("stderr, "close error, errno = %d\n", errno);
    exit(1);
}

从系统调用返回时把错误码存入errno到稍后程序显示errno值之间存在一个时间窗口,期间同一个进程内的另一个线程(如信号处理函数的某次调用)可能改变了errno的值。

上述errno问题可通过在信号处理函数中预先保存并事后恢复errno值来解决:

void sig_alrm(int signo) {
    int errno_save;
    
    errno_save = errno;    /* save its value on entry */
    if (write( ... ) != nbytes) {
        fprintf(stderr, "write error, errno = %d\n", errno);
    }
    errno = errno_save;    /* restore its value on return */
}

上例代码中,在信号处理函数中还调用了fprintf函数,它引入了另一个重入问题。许多版本的标准IO库函数是不可重入的,即我们不应该从信号处理函数中调用标准IO函数。

有两种方法可以把gethostbyname函数这类不可重入的函数改为可重入函数:
1.把由不可重入函数填写并返回静态结构的做法改为由调用者分配空间再由可重入函数填写结构。这就是把不可重入的gethostbyname函数改为可重入的gethostbyname_r函数所用的技巧。但这种方法比较复杂,因为不仅调用者必须提供待填写的hostent结构,还要提供一个存放该结构指向的信息(如规范名字、别名指针数组、各个别名字符串、地址指针数组及该指针指向的各个in_addr结构)的大缓冲区,函数填写的内容包括多个指向这个大缓冲区的指针。这样函数至少要增设3个参数:指向待填写的hostent结构的指针、指向大缓冲区的指针、该大缓冲区的大小,gethostbyname_r函数还增加了第四个额外参数,用于存放错误码的某整型变量的指针,这第四个额外参数也是必要的,因为全局变量h_errno也不可用了(与errno一样,有可重入性问题)。h_errno变量是一个全局变量,被gethostbyname函数这样的域名解析函数使用,如当gethostbyname函数返回空指针时,可以像检查errno一样,检查h_errno变量的值。

getnameinfo和inet_ntop函数也用这种方法。

2.直接由可重入函数调用malloc以动态分配空间。这是getaddrinfo函数所用的技巧。这种方法的问题是调用该函数的进程必须调用freeaddrinfo来释放动态分配的空间,如果不这么做会导致内存泄漏,即进程每调用一次动态分配内存空间的函数,所用内存量就相应增长,如果进程长时间运行(网络服务器的特性之一),那么内存耗用量会不断增加。

以下讨论Solaris 2.x用于从名字到地址和从地址到名字解析的可重入函数:
在这里插入图片描述
每个函数都需要4个额外参数:
1:result参数指向由调用者分配并由被调用函数填写的hostent结构,成功返回时本指针同时作为函数的返回值返回。

2、3:buf参数指向由调用者分配且大小为buflen参数大小的缓冲区。该缓冲区用于存放规范主机名、别名指针数组、各个别名字符串、地址(in_addr结构)指针数组、各个实际地址(in_addr结构)。由result参数指向的hostent结构中的所有指针字段都指向该缓冲区内部。但对于该缓冲区大小,大多手册页面只是含糊地说该缓冲区必须足够大以存放与hostent结构关联的所有数据。gethostbyname函数当前的实现最多能返回35个别名指针和35个地址指针,且内部用了8192字节的缓冲区存放这些别名和地址,因此大小为8192字节的缓冲区应该足够了。

4:如果出错,错误码通过h_errnop指针参数返回,而非全局变量h_errno。

但重入问题比表面看更严重,首先,关于gethostbyname和gethostbyaddr函数的重入问题无标准可循,POSIX声明这两个函数不必是可重入的,Unix 98只说这两个函数不必是线程安全的。

其次,关于_r函数也没有标准可循。上例的_r函数由Solaris 2.x提供。Linux也提供类似的_r函数,但函数会通过其倒数第二个值-结果参数返回一个hostent结构,该函数将查找操作的成功与否作为函数的返回值,同时也将是否成功保存在h_errno参数中。Digital Unix 4.0和HP-UX 10.30也提供这两个函数的_r版本,只是参数不同,它们的版本与Solaris版本有同样的前2个参数,但Solaris版本的后3个参数在前者中组合成一个新hostent_data结构(它由调用者分配空间),指向该hostent_data结构的指针组成该函数的第三个兼最后一个参数。Digital Unix 4.0和HP-UX 10.30中普通的gethostbyname和gethostbyaddr函数通过使用线程特定数据,也是可重入的。

最后,虽然gethostbyname函数的可重入版本可能会在不同的线程同时调用时提供安全性,但这并不意味着底层解析器函数(用于进行主机名解析的函数)也具有可重入性。

开发IPv6期间,用于查找IPv6地址的API经历了多次反复,这些早期API既复杂又没有足够灵活性,于是在RFC 2553中被淘汰掉。RFC 2553又引入了新函数,它们最终在RFC 3493中被简单替换成getaddrinfo和getnameinfo函数。以下介绍一些早期API,以便转换已经使用它们的程序。

gethostbyname函数没有像getaddrinfo函数的hints.ai_family这种指定我们所关心的地址族的参数,因此第一版gethostbyname函数API使用RES_USE_INET6常值,启用RES_USE_INET6会使gethostbyname函数先查找AAAA记录,若找不到AAAA记录,则接着查找A记录。因为hostent结构只有一个地址长度字段,因此gethostbyname函数要么只返回IPv6地址,要么只返回IPv4地址,不能同时返回这两种地址。

启用RES_USE_INET6还会使gethostbyname2函数以IPv4映射的IPv6地址形式返回IPv4地址。

gethostbyname2函数给gethostbyname函数增加了一个地址族参数:
在这里插入图片描述
当af参数为AF_INET时,gethostbyname2函数的行为与gethostbyname函数一样,即查找并返回IPv4地址。当af参数为AF_INET6时,gethostbyname2函数只查找AAAA记录并返回IPv6地址。

RFC 2553因为RES_USE_INET6标志的全局特性以及想对返回信息进行更多控制而废除了RES_USE_INET6和gethostbyname2函数,并引入了getipnodebyname函数:
在这里插入图片描述
af和flags参数直接映射到getaddrinfo的hints.ai_family和hints.ai_flags参数。为了线程安全,返回值是动态分配的,因此必须使用freehostent函数释放:
在这里插入图片描述
getipnodebyname和getipnodebyaddr函数被RFC 3493废除,并代之以getaddrinfo和getnameinfo函数。

进程可能想查找四类与网络相关的信息:主机、网络、协议、服务。大多查找针对的是主机(gethostbyname和gethostbyaddr函数),一小部分查找针对的是服务(getservbyname和getservbyport函数),更小一部分查找针对的是网络和协议。

这四类信息可存放在文件中,每类信息都定义了三个访问函数:
1.函数getXXXent读出文件中的下一个表项,如文件未打开,则会先打开文件。

2.函数setXXXent打开文件(如尚未打开),并rewind文件。

3.函数endXXXent关闭文件。

每类信息都定义了各自的结构,包括hostent、netent、protoent、servent,这些结构定义通常在头文件netdb.h中。

除了用于顺序处理文件的get、set、end三个函数外,每类信息还提供一些键值查找函数,这些键值查找函数遍历整个文件(如通过getXXXent函数读出每一行),但只把某个参数匹配的表项返回调用者。这些键值查找函数具有getXXXbyYYY的名字。例如,针对主机信息的两个键值查找函数为gethostbyname(查找匹配某个主机名的表项)和gethostbyaddr(查找匹配某个IP地址的表项)。
在这里插入图片描述
只有主机和网络信息可通过DNS获取,协议和服务信息总是从相应文件中读取。不同实现有不同方法供系统管理员指定是使用DNS还是使用文件来查找主机和网络信息。

如果使用DNS来查找主机和网络信息,则只有键值查找函数有意义,你不能使用gethostent函数并期待顺序遍历DNS中所有表项。调用gethostent时,它仅读取/etc/hosts文件,不会访问DNS。

虽然网络信息可以做成通过DNS访问到,但很少有人这么做。典型的做法是,系统管理员创建并维护一个/etc/networks文件,网络信息通过它而非DNS获取。如果存在这个文件,指定-i选项的netstat程序就使用它显示每个网络的名字。但无类寻址使得获取网络信息的函数几近无用,且这些函数也不支持IPv6,因此新的网络应用应避免使用网络名字。

应用把主机名转换成IP地址或做相反转换的一组函数称为解析器,gethostbyname和gethostbyaddr函数是解析器曾经常用的入口。随着向IPv6和线程化编程模型的转移,getaddrinfo和getnameinfo函数更为有用,因为它们既能解析IPv6地址,又符合线程安全调用约定。

处理服务名和端口号的常用函数是getservbyname,它接受一个服务名作为参数,并返回一个包含相应端口号的结构。这种映射关系通常包含在一个文本文件中。还有用于把协议名映射成协议号、把网络名映射为网络号的函数,但很少使用。

有一种代替gethostbyname和gethostbyaddr函数的方法,即直接调用解析器函数。这样直接使用DNS的程序之一是sendmail,因为它需要搜索MX资源记录,这是gethostbyxxx函数无法做到的。解析器函数都有以res_开头的名字,如res_init函数,可执行man resolver命令得到这些函数的手册页面。

getaddrinfo函数可允许我们编写协议无关的代码,但直接调用它要花很多步骤,且对于不同情形仍有反复出现的细节要处理,如遍历所有返回的结构、忽略socket函数返回的错误、为TCP服务器设置SO_REUSEADDR套接字选项等,因此我们编写了5个访问getaddrinfo函数的接口函数tcp_connect、tcp_listen、udp_client、udp_connect、udp_server,以简化这些细节。

gethostbyname和gethostbyaddr函数通常是不可重入的,这两个函数使用同一个静态的结果结构,这意味着如果先后调用这两个函数各一次,后一次调用会覆盖前一次调用的结果。因此一些厂商提供这两个函数的_r版本,但需要对调用这些函数的应用加以修改。

修改11-3中的程序,为每个返回的地址调用gethostbyaddr,然后显示由它返回的h_name:

#include "unp.h"

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

    while (--argc > 0) {
        ptr = *++argv;
		if ((hptr = gethostbyname(ptr)) == NULL) {
		    err_msg("gethostbyname error for host: %s: %s", ptr, hstrerror(h_errno));
		    continue;
		}
		printf("official hostname: %s\n", hptr->h_name);
	
		for (pptr = hptr->h_aliases; *pptr != NULL; ++pptr) {
		    printf("    alias: %s\n", *pptr);
		}
	
		switch (hptr->h_addrtype) {
		case AF_INET:
#ifdef AF_INET6
		case AF_INET6:
#endif
	        pptr = hptr->h_addr_list;
		    for (; *pptr != NULL; ++pptr) {
		        printf("\taddress: %s\n", Inet_ntop(hptr->h_addrtype, *pptr, str, sizeof(str)));
	            // 与11-3中代码相比,多了以下if-else语句
				if ((hptr = gethostbyaddr(*pptr, hptr->h_length, hptr->h_addrtype)) == NULL) {
				    printf("\t(gethostbyaddr failed)\n");
				} else if (hptr->h_name != NULL) {
				    printf("\tname = %s\n", hptr->h_name);
				} else {
				    printf("\t(no hostname returned by gethostbyaddr)\n");
				}
		    }
		    break;
	
		default:
		    err_ret("unknown address type");
		    break;
		}
    }
    exit(0);
}

以上程序针对只有一个IP地址的主机没有问题,但对于有多个IP地址的主机就有问题了。我们先针对拥有8个IP地址的主机调用11-3中的程序:
在这里插入图片描述
但如果我们对该主机运行以上程序,则它只会输出一个IP地址:
在这里插入图片描述
问题在于gethostbyname和gethostbyaddr两个函数共享同一个hostent结构,当以上程序调用gethostbyname后,再次调用gethostbyaddr时,它重用了这个结构以及它指向的存储区(即h_addr_list指针数组及由该数组指向的数据),结果冲掉了其余7个地址。

在我的机器上(Linux rh 2.6.39-400.17.1.el6uek.x86_64 #1 SMP Fri Feb 22 18:16:18 PST 2013 x86_64 x86_64 x86_64 GNU/Linux)运行以上程序时,会出现段错误,应该是指针已失效(可能是指针指向的内存已被释放):
在这里插入图片描述
有两种方法可以修复上例问题:
1.如果系统支持,可使用可重入版本的gethostbyaddr和gethostbyname函数。

2.在调用gethostbyaddr前复制由gethostbyname返回的指针数组及该数组中指针字段指向的数据。

chargen服务器一直向客户发送数据,直到客户关闭连接为止。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

UNIX网络编程卷一 学习笔记 第十一章 名字与地址转换 的相关文章

  • 使用 sigaction(),c

    我正在读一些关于sigaction 来源来自我的课程笔记 我不确定我是否理解这段文字 信号掩码仅在以下持续时间内计算和安装 信号处理程序 默认情况下 信号 sig 发生时也会被阻塞 使用 sigaction 为特定信号安装操作后 它会保持安
  • Locale.getDefault() 始终返回 en

    unix 机器上的服务器始终使用 en 作为默认区域设置 以下是区域设置输出 LANG en US LC CTYPE C LC NUMERIC C LC TIME C LC COLLATE C LC MONETARY C LC MESSAG
  • 如何使用 UNIX shell 计算字母在文本文件中出现的次数?

    我有几个文本文件 我想计算每个字母在每个文件中出现的次数 具体来说 我想使用 UNIX shell 来执行此操作 形式为 cat file 做东西 有没有办法让 wc 命令来执行此操作 grep char o filename wc l
  • 如何在 shell 脚本中操作 $PATH 元素?

    有没有一种惯用的方法从类似 PATH 的 shell 变量中删除元素 这就是我想要的 PATH home joe bin usr local bin usr bin bin path to app bin and remove or rep
  • 如何在数组中存储包含双引号的命令参数?

    我有一个 Bash 脚本 它生成 存储和修改数组中的值 这些值稍后用作命令的参数 对于 MCVE 我想到了任意命令bash c echo 0 0 echo 1 1 这解释了我的问题 我将用两个参数调用我的命令 option1 without
  • 针对库编译时出现“未定义引用”错误

    我在代码中添加了第三方库 但在运行时遇到这样的错误make 请帮助我理解这个错误 text 0x9b4 undefined reference to snd strerror home bet Tent tun app Common hl
  • 如何在 Bash 中给定超时后终止子进程?

    我有一个 bash 脚本 它启动一个子进程 该进程时不时地崩溃 实际上是挂起 而且没有明显的原因 闭源 所以我对此无能为力 因此 我希望能够在给定的时间内启动此进程 如果在给定的时间内没有成功返回 则将其终止 有没有simple and r
  • php exec 返回的结果比直接进入命令行要少

    我有一个 exec 命令 它的行为与通过 Penguinet 给 linux 的相同命令不同 res exec cd mnt mydirectory zcat log file gz echo res 当将命令直接放入命令行时 我在日志文件
  • waitpid() 的作用是什么?

    有什么用waitpid 它通常用于等待特定进程完成 或者如果您使用特殊标志则更改状态 基于其进程 ID 也称为pid 它还可用于等待一组子进程中的任何一个 无论是来自特定进程组的子进程还是当前进程的任何子进程 See here http l
  • 通过 sed 使用 unix 变量将数据附加到每行末尾[重复]

    这个问题在这里已经有答案了 我有一个文件 我想使用 SED 将值附加到每行末尾的 unix 变量中 我已经通过 AWK 实现了这一点 但我想在 SED 中实现 像这样的东西 我已经尝试过以下命令 但它不起作用 sed i s BATCH R
  • 使用 Grep 查找两个短语之间的文本块(包括短语)

    是否可以使用 grep 来高亮所有以以下内容开头的文本 mutablePath CGPathCreateMutable 并以以下内容结尾 CGPathAddPath skinMutablePath NULL mutablePath 这两个短
  • Python 如何找到 sys.prefix(或 sys.base_prefix)的值?

    锡上写着什么 我已经解开了如何解开的谜团sys prefix使用虚拟环境时设置 Python 寻找pyvenv cfg file 1 https www python org dev peps pep 0405 specification
  • 如何在gnuplot中将字符串转换为数字

    有没有办法将表示数字 以科学格式 的字符串转换为 gnuplot 中的数字 IE stringnumber 1 0e0 number myconvert stringnumber plot 1 1 number 我可能使用 shell 命令
  • GnuTLS 错误 -110:TLS 连接未正确终止

    我发现我的一个 Windows 服务没有连接到 Unix 服务器上的 FTP 位置 我在我的 PC 上运行了可执行文件 因为开发人员没有记录任何错误 并且我在尝试从 FTPWebRequest 获取响应时遇到超时错误C 中的对象 尝试使用
  • 转换为科学计数法时出现双精度错误

    我正在构建一个程序 将双精度值转换为科学值格式 尾数 指数 然后我注意到下面的内容 369 7900000000000 gt 3 6978999999999997428 68600000 gt 6 8599999999999994316 我
  • 相当于 UNIX diff 和 patch 的本机 PowerShell

    我需要潜在地修补文件作为脚本的一部分 为了使脚本所做的事情更具可读性 我想以类似于 UNIX diff 和 patch 方法的方式来实现它 在标准 UNIX 系统上 diff 可以生成特殊格式的文本文件 表示两个文件之间的差异 这可以与要修
  • Node.js 请求随机开始挂起,直到服务器重新启动后才会清除

    我在我们的网络应用程序上遇到了一个非常奇怪且看似随机的问题 我似乎无法成功调试 它可以正常运行 10 分钟到 6 小时 然后突然无法向服务器发出或从服务器发出远程请求 它们只是挂起 这包括常规的 http 和 Web 套接字请求 奇怪的是
  • 如果目录不存在,有没有办法让 mv 创建要移动到的目录?

    因此 如果我在主目录中并且想将 foo c 移动到 bar baz foo c 但这些目录不存在 是否有某种方法可以自动创建这些目录 以便你只需要输入 mv foo c bar baz 一切都会顺利吗 似乎您可以将 mv 别名为一个简单的
  • 在 bash 中快速引用 stdout(即上一个命令的输出)?

    有没有办法快速 例如通过键盘快捷键等 引用写入到 stdout 的上一个命令的输出 例如 如果我这样做 which rails 它回来了 usr local bin rails然后我想在 textmate 中打开该文件 我可以像这样重新输入
  • 如何使用 Perl 在 Unix 中获取文件创建时间

    如何使用 perl 在 unix 中获取文件创建时间 我有这个命令显示文件的最后修改时间 perl MPOSIX le print strftime d b Y H M localtime lstat 9 for ARGV file txt

随机推荐

  • 分布式Netty集群方案 加代码 SpringBoot 版

    目录 单机netty是怎么通信的 多节点集群netty是怎么通信的呢 netty集群是怎么搭建的呢 连接上的 client 的 channelId 怎么存入 redis 中 在集群模式中 客户端1向客户端2发送信息 演示效果 完整的讲解 n
  • unity_控制物体移动代码

    目录 2D游戏控制 简单的上下左右移动 第一种 使用Rigidbody2D 第二种 上下左右移动加上旋转 2D空战飞机的移动 汽车 坦克等移动 坦克的控制 2D游戏控制 简单的上下左右移动 第一种 使用Rigidbody2D using S
  • css3绘制扫描图片效果

    html
  • KMP算法(思想真的不复杂)

    在了解KMP之前 我们需要了解两个概念 字符串的前缀 和字符串的后缀 字符串的前缀 我举个例子你们就懂了 一个字符串abcde 它包含的前缀有 a ab abc abcd 字符串的后缀 bcde cde de e 知道这两个概念后 我们就可
  • 欧式距离计算公式

    欧式距离也称欧几里得距离 是最常见的距离度量 衡量的是多维空间中两个点之间的绝对距离 也可以理解为 m维空间中两个点之间的真实距离 或者向量的自然长度 即该点到原点的距离 在二维和三维空间中的欧氏距离就是两点之间的实际距离 下面是具体的计算
  • 代码质量(单元测试+代码审查)

    代码质量 1 单元测试 2 代码审查 1 单元测试 单元测试的目的 尽早在尽量小的范围内暴露错误 错误率恒定定律 一定量的代码 必然会产生一定量的BUG a 刚写完一个方法就发现BUG 修改只要几分钟 方法提供给其他人使用后 再发现BUG
  • 12. ShardingSphere-JDBC 分库分表

    Spring Cloud 微服务系列文章 点击上方合集 1 简介 ShardingSphere 是国产的 开源的 配置简单的分布式数据库解决方案 可以通过简单的配置实现分库分表和读写分离 ShardingSphere 提供了两种分布式数据库
  • 后起之秀svelte和solid是否值得花时间学习?

    前言 大家好我是爱分享的老前端羊村长 国外最近两年涌现两个新锐框架Svelte和Solid 大家可能忙工作没太关注 但是t它们大有后来居上的意思 来看一下github的star数量感受一下 image 20220920225835049 i
  • 特殊行业微信小程序备案所需准备资料,特殊行业微信小程序备案流程,特殊行业微信小程序如何备案

    微信官方提示 如你的小程序从事新闻 出版 药品和医疗器械 网约车等需须经有关主管部门审核同意的互联网信息服务 在履行备案手续时 应提供业务对应前置审批文件 相关类目参考如下 前置审批类目 对应材料 办理部门 参考 法律法规依据 参考 出版
  • 一本通1619【例 1】Prime Distance

    1619 例 1 Prime Distance 题目描述 原题来自 Waterloo local 题面详见 POJ 2689 给定两个整数 L R 求闭区间 L R 中相邻两个质数差值最小的数对与差值最大的数对 当存在多个时 输出靠前的素数
  • 数据结构:顺序+链式线性表(C语言)

    写线性表的时候 简直离大谱的出现了很多问题 如下 顺序线性表 目标 动态存储的线性表顺序表示和实现 重点实现 插入和删除 操作 思考 1创建线性表2初始化3插入 删除操作4验证结果 1 创建线性表 使用动态存储的方式 可以对于线性表动态增添
  • 【前端——vue】:过滤器、侦听器、计算属性、vue-cli、vue组件、动态组件、插槽、自定义属性、路由

    一 过滤器 1 过滤器Filters 只能在vue2中使用 p标签里面看到的是后面函数的返回值 message相当于作为参数传给后面 竖线代表要调用过滤器 过滤器函数必须定义到filters节点之下 过滤器的本质是函数 字符串charAt
  • 雪球--数据的爬取并存入数据库

    往数据库添加数据的一般步骤 lt 1 gt 导包 import pymysql def add pymysql house id target description lt 2 gt 创建连接 内部参数为ip 用户 密码 端口号 默认 数据
  • 云计算的历程

    本内容来自 本词条由 科普中国 科学百科词条编写与应用工作项目 审核 云计算 cloud computing 是分布式计算的一种 指的是通过网络 云 将巨大的数据计算处理程序分解成无数个小程序 然后 通过多部服务器组成的系统进行处理和分析这
  • 备份MySQL数据库并以其他名称恢复

    目标效果 将mysql数据库的A库备份 并还原到B库 数据库基本信息 数据库安装在 data1 usr local mysql8 0 18 数据库的指令工具在 data1 usr local mysql8 0 18 bin 实施步骤 1 备
  • 交换python: a、b、c 三个变量的值。首先从键盘输入 a、b、c 三个变量的原值,然后将变量 a 的值赋给 b,将变量 b 的值赋给 c,将变量 c 的值赋给 a。

    a b c map int input 请输入a b c的值 split 输入的数由空格分开 print a b c format a b c a b c b c a print a b c format a b c 执行结果为
  • DBWn 进程和 DB_WRITER_PROCESSES/DBWR_IO_SLAVES 参数

    1 DBWn进程 Database Writer Process DBWn The database writer process DBWn writes the contents of database buffers to data f
  • 【Qt】modbus之串口模式写操作

    00 目录 文章目录 00 目录 01 概述 02 开发环境 03 写Coils程序示例 04 写HoldingRegisters程序示例 05 综合示例 01 概述 Qt中几个常用的串口modbus类 QModbusRtuSerialSl
  • 网站迁移或者调整页面链接的方法

    背景 这两天我在重新规划我的博客网站逐步前行STEP 将改版导航以及整体内容结构 将单纯的博客网站打造成集博客 资讯 工具 社区于一体的综合站点 这必然涉及到要重新规划原有的路由 直接修改路由将导致搜索引擎收录的链接或者访客收藏的网址失效
  • UNIX网络编程卷一 学习笔记 第十一章 名字与地址转换

    到目前为止 本书中所有例子都用数值地址表示主机 如206 6 226 33 用数值端口号来标识服务器 如端口13代表daytime服务器 但出于某些理由 我们应使用名字而非数值 名字比较容易记住 数值地址可以变动而名字保持不变 随着往IPv