UNIX(网络编程-基本用法):33---名字与地址转换之(地址/服务解析函数:getaddrinfo、freeaddrinfo、gai_strerror、getnameinfo、addrinfo)

一、struct  addrinfo结构体

struct addrinfo {
    int ai_flags;             /* AI_PASSIVE, AI_CANONNAME */
    int ai_family;            /* AF_xxx */
    int ai_socktype;          /* SOCK_xxx */
    int ai_protocol;          /* 0 or IPPROTO_xxx for IPv4 and IPv6 */
    socklen_t ai_addrlen;     /* length of ai_addr */
    char *ai_canonname;       /* ptr to canonical name for host */
    struct sockaddr *ai_addr; /* ptr to socket address structure */
    struct addrinfo *ai_next; /* ptr to next structure in linked list */
}; 

参数

  • ai_flags:标志(见下)
  • ai_family:协议族。同socket()函数参数1一致
  • ai_socktype:socket类型。同socket()函数参数2一致
  • ai_protocol:协议类型。同socket()函数参数3一致
  • ai_addrlen:ai_addr地址的长度
  • ai_canonname:该主机对应的标准名称
  • ai_addr:该结构体对应的一个网络地址
  • ai_next:指向下一个addrinfo结构体的指针

三、getaddrinfo函数

#include 
#include 
#include 
int getaddrinfo(const char *hostname, const char *service,
    const struct addrinfo *hints, struct addrinfo **result
);

函数概述

  • gethostbyname和gethostbyaddr这两个函数仅仅支持IPv4。正如前面介绍的那样, 解析IPv6地址的API经历了若干次反复,最终结果是getaddrinfo函数
  • 该函数函数能够处理名字到地址以及服务到端口这两种转换
  • 该函数返回的是一个sockaddr结构而不是一个地址列表,这些sockaddr结构随后可由套接字函数直接使用。如此一来,getaddrinfo函数把协议相关性完全隐藏在这个库函数内部。应用程序只需处理由getaddrinfo填写的套接字地址结构。 该函数在POSIX规范中定义

参数

  • hostname:是一个主机名或地址串(IPv4的点分十进制数串或IPv6的十六进制数串)
  • service:是一个服务名或十进制端口号数串
  • hints:可以填一个空指针或者一个指向某个addrinfo结构的指针,调用者在这个结构中填入关于期望返回的信息类型的暗示(在下面详细介绍这个参数)
  • result:如果此函数执行成功,那么由result参数指向的变量已被填入一个指针,它指向的是由其中的ai_next成员串接起来的addrinfo结构链表

hints参数详解

  • 这个参数可以为空,也可以是调用者事先设计好的一个struct addinfo结构体指针,然后传递给getaddinfo()函数
  • 使用此参数的一个案例:如果指定的服务既支持TCP也支持UDP (例如指代某个DNS服务器的domain服务),那么调用者可以把hints结构中的ai_socktype成员设置为SOCK_DGRAM,使得返回的仅仅是适用于数据报套接字的信息
  • hints结构中调用者可以设置的成员有:
    • ai_flags(零个或多个或在一起的AI_xxx值)
    • ai_family(某个AF_xxx值)
    • ai_socktype(某个SOCK_xxx值)
    • ai_protocol
  • 其中ai_flags成员可用的标志值及其含义如下:
    • AI_PASSIVE:套接字将用于被动打开
    • AI_CANONNAME:告知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:按照所在主机的配置选择返回地址类型,也就是只查找与所在主机回馈接口以外的网络接口配置的IP地址版本一致的地址
  • 如果hints参数这设置为空指针,那么getaddino()函数就假设ai_flag、ai_socktype和ai_protocol的值均为0,ai_family的值为AF_UNSPEC

返回值

  • 成功:返回0
  • 出错:返回非0(见下面的gai_strerror函数)

result参数返回的结果是可以直接使用的

  • struct addrinfo结构中返回的信息可以直接被调用。例如:
    • socket()函数的参数就是struct addrinfo结构中的ai_family、ai_socktype、和ai_addr成员
    • connect()或bind()函数的第2和第3个参数就是struct addrinfo结构中的ai_addr(一个指向适当类型套接字地址结构的指针,地址结构的内容由getaddrinfo()函数填写)和ai_addrlen(这个套接字地址结构的大小)成员
    • 另外也可以用于sendto()等调用等等...

函数返回多个地址的情况

  • 如果函数执行成功,那么由result参数指向的变量已被填入一个指针,它指向的是由其中的ai_next成员串接起来的addrinfo结构链表。可导致返回多个addrinfo结构的情形有以下两个:
    • ①如果与hostname参数关联的地址有多个,那么适用于所请求地址族(可通过hints结构的ai_family成员设置)的每个地址都返回一个对应的结构
    • ②如果service参数指定的服务支持多个套接字类型那么每个套接字类型都可能返回一个对应的结构,具体取决于hints结构的ai_socktype成员。(注意,getaddrinfo的多数实现认为只能按照由ai_socktype成员请求的套接字类型端口号数串到端口的转换,如果没有指定这个成员,那就返回一个错误)

演示案例

  • 举例来说,如果在没有提供任何暗示信息的前提下,请求查找有2个IP地址的某个主机上的domain服务,代码如下。那将返回4个addrinfo结构,分别是:
    • 第一个IP地址组合SOCK_STREAM套接字类型
    • 第一个IP地址组合SOCK_DGRAM套接字类型
    • 第二个IP地址组合SOCK_STREAM套接字类型
    • 第二个IP地址组合SOCK_DGRAM套接字类型
struct addrinfo hints, *res;

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

getaddrinfo("freebsd4", "domain", &hints, &res);
  • 下图展示了本例子:
    • 当有多个addrinfo结构返回时,这些结构的先后顺序没有保证,也就是说,我们并不能假定TCP服务总是先于UDP服务返回(尽管没有保证,本函数的实现却应该按照DNS返回的顺序返回各个IP地址。有些解析器 允许系统管理员在/etc/resolv.conf文件中指定地址的排序顺序。IPv6可指定地址选择规则,可能影响由getaddrinfo返回地址的顺序)
    • 另外,因为hints参数的ai_flags成员设置为AI_CANONNAME,所以返回的第一个addrinfo结构的ai_canonname成员指向所查找主机的规范名字。按照DNS的说法,规范名字通常是FQDN。注入telnet之类程序往往使用这个标志以显示所连接到主机的规范名字,这样即使用户给定的是一个简单名字或别名,他们也能搞清楚真正查找的名字(下图我们假设主机freebsd4的规范名字是freebsd4.unpbook.com,并且它在DNS中有2个IPv4地址)
    • 端口53用于domain服务,因此在下图中ai_addr指针所指的套接字地址结构中端口为53(以网络字节序存放)
    • 返回的ao_protocol的值为IPPROTO_TCP,或者为IPPROTO_UDP
    • 如果ai_family和ao_socktype组合能够完全指定TCP或UDP协议,那么返回的ai_protocol值为0也可以接受。例如:
      • 如果系统没有实现除TCP外的其它SOCK_STREAM协议(如SCTP),套接字类型值为SOCK_STREAM的那两个addrinfo结构体协议值可以为0
      • 同理,​​​​​​​如果系统没有实现除UDP外的其它SOCK_DREAM协议,套接字类型值为SOCK_DREAM的那两个addrinfo结构协议值可以为0
      • 不过,最安全的做法是让getaddrinfo()总是返回明确的协议值

UNIX(网络编程-基本用法):33---名字与地址转换之(地址/服务解析函数:getaddrinfo、freeaddrinfo、gai_strerror、getnameinfo、addrinfo)_第1张图片

返回的addrinfo结构的数目

  • 下图汇总了根据指定的服务名(可以是一个十进制端口号数串)和ai_socktype暗示信息为每个通过主机名查找获得的IP地址返回addrinfo结构的数目

UNIX(网络编程-基本用法):33---名字与地址转换之(地址/服务解析函数:getaddrinfo、freeaddrinfo、gai_strerror、getnameinfo、addrinfo)_第2张图片

  • 在不考虑SCTP的前提下,只有在未提供ai_socktype暗示信息时才可能为每个IP地址返回 多个addrinfo结构,此时或者服务以名字标识并且同时支持TCP和UDP(在/etc/services文 件中指明),或者服务以端口号标识
  • 如果枚举getaddrinfo所有64种可能的输入(因为它共有6个二值输入变量),那么许多是无效的,有些则没有多大意义。为此我们只查看一些常见的输入:
    • ①指定hostname和service。这是TCP或UDP客户进程调用getaddrinfo的常规输入。该调用 返回后,TCP客户在一个循环中针对每个返回的IP地址,逐一调用socket和connect, 直到有一个连接成功,或者所有地址尝试完毕为止。我们将在图11-10中随自行开发的 tcp_connect函数给出这样的一个例子。
      • 对于UDP客户,由getaddrinfo填入的套接字地址结构用于调用sendto或connect。 如果客户能够判定第一个地址看来不工作(其手段不外乎或者在已连接的UDP套接字上收 到出错消息,或者在未连接的套接字上经历消息接收超时),那么可以尝试其余的地址
      • 如果客户清楚自己只处理一种类型的套接字(例如Telnet和FTP客户只处理TCP, TFTP客户只处理UDP),那么应该把hints结构的ai_socktype成员设置成SOCK_STREAM 或SOCK_DGRAM
    • ②典型的服务器进程只指定service而不指定hostname,同时在hints结构中指定AI_PASSIVE 标 志 。 返 回 的 套 接 字地址结 构 中 应 含 有一个值为 INADDR_ANY(对于 IPv4 ) 或 IN6ADDR_ANY_INIT(对于IPv6)的IP地址。TCP服务器随后调用socket、bind和listen。 如果服务器想要malloc另一个套接字地址结构以从accept获取客户的地址,那么返回的 ai_addrlen值给出了这个套接字地址结构的大小。
      • UDP服务器将调用socket、bind和recvfrom。如果服务器想要malloc另一个套接字地 址结构以从recvfrom获取客户的地址,那么返回的ai_addrlen值给出了这个套接字地 址结构的大小
      • 与典型的客户一样,如果服务器清楚自己只处理一种类型的套接字,那么应该把hints结 构的ai_socktype成员设置成SOCK_STREAM或SOCK_DGRAM。这样可以避免返回多个结 构,其中可能出现错误的ai_socktype值
    • ③到目前为止,我们展示的TCP服务器仅仅创建一个监听套接字,UDP服务器也仅仅创建 一个数据报套接字。这也是我们讨论上一点隐含的一个假设。服务器程序的另一种设计 方法是使用select或poll函数让服务器进程处理多个套接字。这种情形下,服务器将 遍历由getaddrinfo返回的整个addrinfo结构链表,并为每个结构创建一个套接字, 再使用select或poll
      • 这个技术的问题在于,getaddrinfo返回多个结构的原因之一是该服务可同时由IPv4和 IPv6处理(见下面IPv4、IPv6处理中的图片)。然而正如将在https://blog.csdn.net/qq_41453285/article/details/89792003中看到的那样,这两个协议并非完全独立。也就是说, 如果我们为某个给定端口创建了一个IPv6监听套接字,那么没有必要为同一个端口再创建一 个IPv4套接字,因为来自IPv4客户的连接将由协议栈和IPv6监听套接字自动处理,而不论是 否设置了IPV6_V6ONLY套接字选项

getaddrinfo()在IPv4、IPv6处理中的几点注意

  • POSIX规范定义了getaddrinfo函数以及该函数为IPv4或IPv6返回的信息。在以下图汇总这些返回值之前,我们注意以下几点:
    • getaddrinfo在处理两个不同的输入:一个是套接字地址结构类型,调用者期待返回的 地址结构符合这个类型;另一个是资源记录类型,在DNS或其他数据库中执行的查找符 合这个类型
    • 由调用者在hints结构中提供的地址族指定调用者期待返回的套接字地址结构的类型。如 果调用者指定AF_INET,getaddrinfo函数就不能返回任何sockaddr_in6结构;如果调 用者指定AF_INET6,getaddrinfo函数就不能返回任何sockaddr_in结构。
    • POSIX声称如果调用者指定AF_UNSPEC,那么getaddrinfo函数返回的是适用于指定主 机名和服务名且适合任意协议族的地址。这就意味着如果某个主机既有AAAA记录又有 A记录,那么AAAA记录将作为sockaddr_in6结构返回,A记录将作为sockaddr_in结 构返回。在sockaddr_in6结构中作为IPv4映射的IPv6地址返回A记录没有任何意义,因 为这么做没有提供任何额外信息:这些地址已在sockaddr_in结构中返回过了
    • POSIX的这个声明也意味着如果设置了AI_PASSIVE标志但是没有指定主机名,那么IPv6 通配地址(IN6ADDR_ANY_INIT或0::0)应该作为sockaddr_in6结构返回,同样IPv4通 配地址(INADDR_ANY或0.0.0.0)应该作为sockaddr_in结构返回。首先返回IPv6通配地 址也是有意义的,因为我们将在12.2节看到双栈主机上的IPv6服务器能够同时处理IPv6 客户和IPv4客户
    • 在hints结构的ai_family 成员中指定的地址族以及在ai_flags成员中指定的AI_V4MAPPED和AI_ALL等标志决定了在DNS中查找的资源记录类型(A和/或AAAA),也决定了返回地址的类型(IPv4、IPv6和/或IPv4映射的IPv6)。下图对此作了汇总
    • 主机名参数还可以是IPv6的十六进制数串或IPv4的点分十进制数串。这个数串的有效性 取决于由调用者指定的地址族。如果指定AF_INET,那就不能接受IPv6的十六进制数串; 如果指定AF_INET6,那就不能接受IPv4的点分十进制数串。然而如果指定的是 AF_UNSPEC,那么这两种数串都可以接受,返回的是相应类型的套接字地址结构

  • 下图汇总了getaddrinfo如何处理IPv4和IPv6地址:
    • “结果”一栏是在给定前三栏的变量后,该函数返回给调用者的结果
    • “行为”一栏则说明该函数如何获取这些结果

UNIX(网络编程-基本用法):33---名字与地址转换之(地址/服务解析函数:getaddrinfo、freeaddrinfo、gai_strerror、getnameinfo、addrinfo)_第3张图片

  • 下图仅仅说明getaddrinfo如何处理IPv4和IPv6,也就是返回给调用者的地址数目。返回给调用者的addrinfo结构的确切数目还取决于指定的套接字类型和服务名,就如图11-6总结 的那样
  • 总结:
    • getaddrinfo()函数比gethostbyname()和getservbyname()函数好,因为它方便我们编写协议无关的程序代码,单个函数能够同时处理主机名和服务,所有返回信息都是动态分配的
    • 不过该函数使用起来比较复杂
    • getaddrinfo()解决了把主机名和服务名转换成套接字地址结构的问题。在下面我们介绍它的反义函数getnameinfo(),他把套接字地址结构转换为主机名和服务名

四、freeaddrinfo函数

#include 
#include 
#include 
void freeaddrinfo(struct addrinfo *ai);
  • 功能:由getaddrinfo返回的所有存储空间都是动态获取的(譬如来自malloc调用),包括addrinfo结构、ai_addr结构和ai_canonname字符串。这些存储空间通过调用freeaddrinfo 返还给系统
  • ai参数:指向由getaddrinfo返回的第一个addrinfo结构。这个链表中的所有结构以及由它们指向的任何动态存储空间(譬如套接字地址结构和规范主机名)都被释放掉

使用此函数存在的一个潜在问题

  • 假设我们调用getaddrinfo(),遍历返回的addrinfo结构链表后找到所需的结构。如果我们为保存其信息而仅仅复制这个addrinfo结构,然后调用freeaddrinfo(),那就引入了一个潜藏的错误。原因在于这个addrinfo结构本身指向动态分配的内存空间(用于存放套接字地址结构和可能有的规范主机名),因此由我们保存的结构指向的内存空间已在调用freeaddrinfo时返还给系统,稍后可能用于其他目的
  • 因此,当调用这个函数之后,getaddinfo()函数返回的addinfo结构的内容就不可以再使用了,因为已经被释放了

五、getnameinfo

#include 
#include 
int getnameinfo(
    const struct sockaddr *sockaddr, socklen_t addrlen,
    char *host, size_t hostlen,
    char *serv, size_t servlen, 
    int flags
);

//返回值:成功返回0;出错返回非0(见gai_strerror函数)
  • getnameinfo是getaddrinfo的互补函数,它以一个套接字地址为参数,返回描述其中的主机的一个字符串和描述其中的服务的另一个字符串。本函数以协议无关的方式提供这些信息, 也就是说,调用者不必关心存放在套接字地址结构中的协议地址的类型,因为这些细节由本函 数自行处理
  • sock_ntop和getnameinfo的差别在于,前者不涉及DNS,只返回IP地址和端口号的一个 可显示版本;后者通常尝试获取主机和服务的名字

参数

  • sockaddr:指向一个套接字地址结构,其中包含待转换成直观可读的字符串的协议地址
  • addrlen:是这个结构的长度(该结构及其长度通常由accept、recvfrom、getsockname或 getpeername返回)
  • host:调用者预先分配的存储空间,用来存储主机字符串
  • hostlen:对应于参数host的长度。如果调用者不想返回主机字符串,那就指定此参数为0
  • serv:调用者预先分配的存储空间,用来存储服务字符串
  • servlen:对应于参数serv的长度。如果调用者不想返回服务字符串,那就指定此参数为0
  • flags:标志,用于改变getnameinfo的操作。可用标志如下:

UNIX(网络编程-基本用法):33---名字与地址转换之(地址/服务解析函数:getaddrinfo、freeaddrinfo、gai_strerror、getnameinfo、addrinfo)_第4张图片

flags参数的使用

  • NI_DGRAM标志:当知道处理的是数据报套接字时,调用者应设置该标志,因为在套接字地址结构中 给出的仅仅是IP地址和端口号,getnameinfo无法就此确定所用协议(TCP或UDP)。有若干个 端口号在TCP上用于一个服务,在UDP上却用于截然不同的另一个服务。端口514就是这样的一 个例子,它在TCP上提供rsh服务,在UDP上提供syslog服务
  • NI_NAMEREQD标志:如果无法使用DNS反向解析出主机名,该标志将导致返回一个错误。需要把客 户的IP地址映射成主机名的那些服务器可以使用这个特性。这些服务器随后以这样返回的主机 名调用gethostbyname,以便验证gethostbyname返回的某个地址就是早先调用getnameinfo 指定的套接字地址结构中的地址
  • NI_NOFQDN标志:该标志导致返回的主机名第一个点号之后的内容被截去。举例来说,假设套接字 地址结构中的IP地址为192.168.42.2,那么不设置本标志的gethostbyaddr返回的主机名为 aix.unpbook.com,而设置本标志的gethostbyaddr返回的主机名为aix
  • NI_NUMERICHOST标志:该标志告知getnameinfo不要调用DNS(因为调用DNS可能耗时),而是以 数值表达格式以字符串的形式返回IP地址(可能通过调用inet_ntop实现)。类似地, NI_NUMERICSERV标志指定以十进制数格式作为字符串返回端口号,以代替查找服务名
  • NI_NUMERICSCOPE标志:该标志指定以数值格式作为字符串返回范围标识,以代替其名字。既然客户的 端口号通常没有关联的服务名——它们是临时的端口,服务器通常应该设置NI_NUMERICSERV标志
  • 对于这些标志有意义的组合(例如NI_DGRAM和NI_NUMERICHOST),可以把其中各个标志逻辑或在一起

六、gai_strerror函数

#include 
#include 
#include 
const char *gai_strerror(int errcode);

//返回:指向错误描述消息字符串的指针
  • 功能:当getaddrinfo()、getnameinfo()函数出错时都返回非0错误值。gai_strerror函数通过这些非0错误值返回一个错误字符串
  • getaddrinfo()、getnameinfo()返回的非0错误常值如下:

UNIX(网络编程-基本用法):33---名字与地址转换之(地址/服务解析函数:getaddrinfo、freeaddrinfo、gai_strerror、getnameinfo、addrinfo)_第5张图片

你可能感兴趣的:(UNIX(网络编程-基本用法))