9.1.3 Socket地址处理接口
为了处理Socket地址,APR中提供了四个操作接口,这些接口定义在apr_network_io.h中,而实现则sockaddr.c中。这四个接口分别是:
9.1.3.1地址获取
由于APR中仅仅使用apr_sockaddr_t结构描述套接字地址,因此其余的各类描述信息最终都要转换为该结构,APR中提供apr_sockaddr_info_get函数实现该功能:
APR_DECLARE(apr_status_t) apr_sockaddr_info_get(apr_sockaddr_t **sa,
const char *hostname,
apr_int32_t family,
apr_port_t port,
apr_int32_t flags,
apr_pool_t *p);
该函数允许从主机名hostname,地址协议族family和端口port创建新的apr_sockaddr_t地址,并由sa返回。
hostname参数允许是实际的主机名称,或者也可以是字符串类型的IP地址,比如”127.0.0.1”,甚至可以是NULL,此时默认的地址是”0.0.0.0”。
family的值可以是AF_INET,AF_UNIX等系统定义类型,也可以是APR_UNSPEC类型,此时,地址协议族由系统决定。
flags参数用以指定Ipv4和Ipv6处理的 优先级,它的取值包括两种:APR_IPV4_ADDR_OK和APR_IPV6_ADDR_OK。这两个标志并不是在所有的情况下都有效,这可以从函数的实现中看出它的用法:
{
apr_int32_t masked;
*sa = NULL;
if ((masked = flags & (APR_IPV4_ADDR_OK | APR_IPV6_ADDR_OK))) {
if (!hostname ||
family != APR_UNSPEC ||
masked == (APR_IPV4_ADDR_OK | APR_IPV6_ADDR_OK)) {
return APR_EINVAL;u
}
#if !APR_HAVE_IPV6
if (flags & APR_IPV6_ADDR_OK) {
return APR_ENOTIMPL;
}
#endif
}
#if !APR_HAVE_IPV6
if (family == APR_UNSPEC) {
family = APR_INET; v
}
#endif
return find_addresses(sa, hostname, family, port, flags, p); w
}
从实现代码可以看出,函数的内部实际的地址转换过程是由函数find_address完成的。不过在调用find_address之前,函数进行了相关检查和预处理,这些检查和预处理包括:
1、APR_IPV4_ADDR_OK标记只有在hostname为NULL,同时family为APR_UNSPEC的时候才会有效,而APR_IPV6_ADDR_OK和APR_IPV4_ADDR_OK是相互排斥的,一旦定义了APR_IPV4_ADDR_OK,就不能使用APR_IPV6_ADDR_OK,反之亦然。只有在hostname为NULL,同时family为APR_UNSPEC并且没有定义APR_IPV4_ADDR_OK的时候APR_IPV6_ADDR_OK才会有效。
2、如果操作系统平台并不支持IPV6,同时并没有限定获取的地址族,那么此时将默认为IPV6。如果指定必须获取IPV6的地址信息,但系统并不提供支持,此时返回APR_EINVAL。
一般情况下,在IPV4中从主机名到网络地址的解析可以通过gethostbyname()函数完成,不过该API不允许调用者指定所需地址类型的任何信息,这意味着它仅返回包含IPV4地址的信息,对于目前新的IPV6则无能为力。一些平台中为了支持IPV6地址的解析,提供了新的地址解析函数getaddrinfo()以及新的地址描述结构struct addrinfo。APR中通过宏HAVE_GETADDRINFO判断是否支持IPV6地址的解析。目前Window 2000/XP以上的操作系统都能支持新特性。为此APR中根据系统平台的特性采取不同的方法完成地址解析。
首先我们来看支持IPV6地址解析平台下的实现代码,find_address函数的实现如下:
static apr_status_t find_addresses(apr_sockaddr_t **sa,
const char *hostname, apr_int32_t family,
apr_port_t port, apr_int32_t flags,
apr_pool_t *p)
{
if (flags & APR_IPV4_ADDR_OK) {
apr_status_t error = call_resolver(sa, hostname, AF_INET, port, flags, p);
#if APR_HAVE_IPV6
if (error) {
family = AF_INET6; /* try again */ u
}
else
#endif
return error;
}
#if APR_HAVE_IPV6
else if (flags & APR_IPV6_ADDR_OK) {
apr_status_t error = call_resolver(sa, hostname, AF_INET6, port, flags, p);
if (error) { v
family = AF_INET; /* try again */
}
else {
return APR_SUCCESS;
}
}
#endif
return call_resolver(sa, hostname, family, port, flags, p); w
}
从上面的代码可以清晰的看到APR_IPV4_ADDR_OK和APR_IPV6_ADDR_OK的含义:对于前者,函数内部首先查询对应主机的IPV4地址,只有在IPV4查询失败的时候才会继续查询IPV6地址;而后者则与之相反,对于给定的主机名称,首先查询IPV6地址,只有在查询失败的时候才会查询IPV4。因此APR_IPV4_ADDR_OK和APR_IPV6_ADDR_OK决定了查询的优先性,任何时候一旦查询成功都不会继续查询另外协议地址,即使被查询主机具有该协议地址。
查询的核心代码封装在内部函数call_resolve中,该函数的参数和apr_sockaddr_info_get函数的参数完全相同且对应,call_resolve中的宏处理比较的多,因此我们将分开描述:
static apr_status_t call_resolver(apr_sockaddr_t **sa,
const char *hostname, apr_int32_t family,
apr_port_t port, apr_int32_t flags,
apr_pool_t *p)
{
struct addrinfo hints, *ai, *ai_list;
apr_sockaddr_t *prev_sa;
int error;
char *servname = NULL;
memset(&hints, 0, sizeof(hints));
hints.ai_family = family;
hints.ai_socktype = SOCK_STREAM;
#ifdef HAVE_GAI_ADDRCONFIG
if (family == APR_UNSPEC) {
hints.ai_flags = AI_ADDRCONFIG;
}
#endif
在了解上面的代码之前我们首先简要的了解一些getaddrinfo函数的用法,该函数定义如下:
int getaddrinfo(const char *hostname, const char *service, const struct addinfo *hints,struct addrinfo **result);
hostname是需要进行地址解析的主机名称或者是二进制的地址串(IPV4的点分十进制或者Ipv6的十六进制数串),service则是一个服务名或者是一个十进制的端口号数串。其中hints是addfinfo结构,该结构定义如下:
struct addrinfo {
int ai_flags; /* AI_PASSIVE, AI_CANONNAME, AI_NUMERICHOST */
int ai_family; /* PF_xxx */
int ai_socktype; /* SOCK_xxx */
int ai_protocol; /* 0 or IPPROTO_xxx for IPv4 and IPv6 */
size_t ai_addrlen; /* length of ai_addr */
char *ai_canonname; /* canonical name for nodename */
struct sockaddr *ai_addr; /* binary address */
struct addrinfo *ai_next; /* next structure in linked list */
};
hints参数可以是一个空置针,也可以是一个指向某个addrinfo结构的指针,调用者在该结构中填入关于期望返回的信息类型的暗示,这些暗示将控制内部的转换细节。比如,如果指定的服务器既支持TCP,也支持UDP,那么调用者可以把hints结构中的ai_socktype成员设置为SOCK_DGRAM,使得返回的仅仅是适用于数据报套接口的信息。
hints结构中调用者可以设置的成员包括ai_flags,ai_family,ai_socktype和ai_protocol。
其中,ai_flags成员可用的标志值及含义如下:
标志名称 |
标志含义 |
AI_PASSIVE |
套接口将用于被动打开 |
AI_CANONNAME |
告知getaddrinfo函数返回主机的规范名称 |
AI_NUMERICHOST |
防止任何类型的名字到地址的映射;hostname必须是一个地址串 |
AI_NUMERICSERV |
防止任何类型的名字到服务的映射,service参数必须是一个十进制端口号数串 |
AI_V4MAPPED |
如果同时指定ai_family成员的值为AF_INET6和AF_INET,那么如果没有可用的AAAA记录就返回与A记录对应得Ipv4映射的IPV6地址 |
AI_ALL |
如果同时指定AI_V4MAPPED标志,那么除了返回与AAAA对应得IPV6地址之外,还会返回与A记录对应的IPV4映射的Ipv6地址。 |
AI_ADDRCONFIG |
按照所在主机的配置选择返回的地址类型,也就是只查找与所在主机回馈接口以外的网络接口配置的IP地址版本一直的地址。只有当本地系统中配置仅仅配置了IPV4地址才会将主机名称转换位IPV4地址;同样只有当本地系统中仅配置了IPV6地址的时候才会返回IPV6地址。Loopback地址并不在这种限制之中。 |
ai_family参数指定调用者期待返回的套接口地址结构的类型。它的值包括三种:AF_INET,AF_INET6和AF_UNSPEC。如果指定AF_INET,那么函数九不能返回任何IPV6相关的地址信息;如果仅指定了AF_INET6,则就不能返回任何IPV4地址信息。AF_UNSPEC则意味着函数返回的是适用于指定主机名和服务名且适合任何协议族的地址。如果某个主机既有AAAA记录(IPV6)地址,同时又有A记录(IPV4)地址,那么AAAA记录将作为sockaddr_in6结构返回,而A记录则作为sockaddr_in结构返回。
if(hostname == NULL) {
#ifdef AI_PASSIVE
hints.ai_flags |= AI_PASSIVE;
#endif
#ifdef OSF1
hostname = family == AF_INET6 ? "::" : "0.0.0.0";
servname = NULL;
#ifdef AI_NUMERICHOST
hints.ai_flags |= AI_NUMERICHOST;
#endif
#else
#ifdef _AIX
if (!port) {
servname = "1";
}
else
#endif /* _AIX */
servname = apr_itoa(p, port);
#endif /* OSF1 */
}
#ifdef HAVE_GAI_ADDRCONFIG
if (error == EAI_BADFLAGS && family == APR_UNSPEC) {
hints.ai_flags = 0;
error = getaddrinfo(hostname, servname, &hints, &ai_list);
}
#endif
if (error) {
#ifndef WIN32
if (error == EAI_SYSTEM) {
return errno;
}
else
#endif
{
#if defined(NEGATIVE_EAI)
error = -error;
#endif
return error + APR_OS_START_EAIERR;
}
}
9.1.3.2主机名获取
apr_sockaddr_info_get函数用以完成从主机名到网络地址的转换,而APR中提供的apr_getnameinfo则可以实现从网络地址到主机名的转换,该函数定义如下:
APR_DECLARE(apr_status_t) apr_getnameinfo(char **hostname, apr_sockaddr_t *sa, apr_int32_t flags);
参数sa指定需要转换的网络地址,转换后的主机名由hostname返回。fags是标志位,用以控制内部的转换过程。
{
#if defined(HAVE_GETNAMEINFO)
int rc;
#if defined(NI_MAXHOST)
char tmphostname[NI_MAXHOST];
#else
char tmphostname[256];
#endif
SET_H_ERRNO(0);
#if APR_HAVE_IPV6
if (sockaddr->family == AF_INET6 &&
IN6_IS_ADDR_V4MAPPED(&sockaddr->sa.sin6.sin6_addr)) {
struct sockaddr_in tmpsa;
tmpsa.sin_family = AF_INET;
tmpsa.sin_addr.s_addr = ((apr_uint32_t *)sockaddr->ipaddr_ptr)[3];
#ifdef SIN6_LEN
tmpsa.sin_len = sizeof(tmpsa);
#endif
rc = getnameinfo((const struct sockaddr *)&tmpsa, sizeof(tmpsa),
tmphostname, sizeof(tmphostname), NULL, 0,
flags != 0 ? flags : NI_NAMEREQD);
}
else
#endif
#endif
rc = getnameinfo((const struct sockaddr *)&sockaddr->sa, sockaddr->salen,
tmphostname, sizeof(tmphostname), NULL, 0,
flags != 0 ? flags : NI_NAMEREQD);
在函数的内部实现从地址到主机名称的解析是由函数getnameinfo完成的,该函数是getaddrinfo的互补函数,它以一个套接口地址为参数,返回描述其中的主机的一个字符串和描述其中的服务的另一个字符串。另外该函数以协议无关的方式提供这些信息,调用者必须关心存放在套接口地址结构中的协议地址的类型,这些由函数自行处理。
需要转换的地址到底是IPv4还是IPv6,这由地址结构中的family参数决定。尽管理想中的做法是将apr_getnameinfo()中的参数直接传递给getnameinfo()函数,但是在一些平台上还是会出现一些问题。
MacOS X Panther has a lousy getnameinfo() implementation that doesn't fill the buffer when no DNS entry is found for a host and a numerical result wasn't explicitely asked. As a result, Pure-FTPd didn't even start on Panther (saying "bad IP address") . We now check for EAI_NONAME if available and we retry with NI_NUMERICHOST if this is what getnameinfo() returns. Thanks to Yann Bizeul for his valuable help on this issue. Will research it more and see if I can come up with a patch (I am NOT good at C!)
在一些操作系统中,比如老版本的Mac OS X,如果Ipv6地址是由Ipv4地址映射的结果,那么该地址在传递给getnameinfo函数的时候将会产生错误,这是系统本身实现的BUG。因此对于这种情况,解决的方法就是将这种Ipv6地址重新转换为Ipv4地址。Ipv6地址是否是由Ipv4地址进行映射而成,通过宏IN6_IS_ADDR_V4MAPPED可以实现检测。IPV4到IPV6地址的映射可以用下图描述:
Ipv4地址通过在其十六进制前面添加前导零的方式映射为IPV6地址。反之如果一个IPV6地址是由IPV4地址映射而成,则只要剔除前面的前导零即可,剔除后的地址通常为((apr_uint32_t *)sockaddr->ipaddr_ptr)[3];一旦获取了实际的IPV4地址,则可以将其传递给getnameinfo函数。
对于其余的IP地址,包括普通的Ipv4地址,非Ipv4映射的Ipv6地址,由于不存在BUG,因此可以直接调用getnameinfo。
getnameinfo函数原型如下:
Int getnameinfo(const struct sockaddr* sockaddr, socklen_t addrlen,
char *host, socklen_t hostlen,
char *serv, socklen_t servlen, int flag);
函数的前面几个参数都非常容易理解,只有最后一个参数flag,它用于控制getnameinfo的操作,它允许的值如下面所列:
NI_DGRAM
当知道处理的是数据报套接口的时候,调用者应该设置NI_DGRAM标志,因为在套接口地址结构中给出的仅仅是IP地址和端口号,getnameinfo无法就此确定所用协议是TCP还是UDP。比如端口514,在TCP端口上提供rsh服务,而在UDP端口上则提供syslog服务。
NI_NOFQDN
该标志导致返回的主机名称被截去第一个点号之后的内容。比如假设套接口结构中的IP地址为912.168.42.2,那么不设置该标志返回的主机名为aix.unpbook.com,那么如果设置了该标志后返回的主机名则为aix。
NI_NUMERICHOST,NI_NUMERICSERV,NI_NUMERICSCOPE
该标志通知getnameinfo不要调用DNS,而是以数值表达格式作为字符串返回IP地址;类似的,NI_NUMERICSERV标志指定以十进制数格式作为字符串返回端口号,以代替查找服务名;NI_NUMERICSCOPE则指定以数值格式作为字符串返回范围标识,以代替其名字
NI_NAMEREQD
该标志通知getnameinfo函数如果无法适用DNS反向解析出主机名,则直接返回一个错误。需要把客户的IP地址映射成主机名的那些服务器可以使用该特性。
如果flag没有指定,即为零,那么NI_NAMEREQD将是Apache中默认的标志项,如果不设置该标志,那么在反向解析失败的时候getnameinfo将返回一个数值地址字符串,显然这并不是Apache所需要的结果。
if (rc != 0) {
*hostname = NULL;
#ifndef WIN32
if (rc == EAI_SYSTEM) {
if (h_errno) { /* for broken implementations which set h_errno */
return h_errno + APR_OS_START_SYSERR;
}
else { /* "normal" case */
return errno + APR_OS_START_SYSERR;
}
}
else
#endif
{
#if defined(NEGATIVE_EAI)
if (rc < 0) rc = -rc;w
#endif
return rc + APR_OS_START_EAIERR; /* return the EAI_ error */
}
}
*hostname = sockaddr->hostname = apr_pstrdup(sockaddr->pool, tmphostname);
return APR_SUCCESS;
上面的代码是对getnameinfo发生错误时候的处理(rc==0意味着成功,否则意味着转换失败)。此时将需要返回的主机名称设置为NULL。当getnameinfo发生错误的时候通常会返回EAI_XXXX的错误码,在所有这些错误码中比较特殊的就是EAI_SYSTEM,它意味着同时在errno变量中有系统错误返回,而其余的EAI_XXXX错误并不会设置errno变量。
对于非EAI_SYSTEM错误码,APR并不能直接返回。正如第一章所说,APR中对于apr_status_t返回码有自己的布局和规则,因此这些错误码必须转换至APR返回码。EAI_XXXX错误码的起始偏移是APR_OS_START_EAIERR,因此返回值实际上是rc+APR_OS_START_EAIERR。不过在一些平台上比如glibc,为了防止和h_errno的值冲突,系统将使用EAI_XXXX的负值, 这正是上面的代码w的原因。
上面的代码有一个假设前提,就是系统中必须提供getnameinfo()函数。但是由于getnameinfo()是比较新的一个函数,并不是每个操作系统平台都支持该函数。目前大部分Ipv4平台上不过都提供了gethostbyaddr()函数,通过该函数也能完成从主机地址到主机名称的转换,不过该函数仅仅支持Ipv4协议,不支持Ipv6协议。具体的代码如下所示:
#else
#if APR_HAS_THREADS && !defined(GETHOSTBYADDR_IS_THREAD_SAFE) && \
defined(HAVE_GETHOSTBYADDR_R) && !defined(BEOS)
#ifdef GETHOSTBYNAME_R_HOSTENT_DATA
struct hostent_data hd;
#else
char tmp[GETHOSTBYNAME_BUFLEN];
#endif
int hosterror;
struct hostent hs, *hptr;
#if defined(GETHOSTBYNAME_R_HOSTENT_DATA)
/* AIX, HP/UX, D/UX et alia */
gethostbyaddr_r((char *)&sockaddr->sa.sin.sin_addr, u
sizeof(struct in_addr), AF_INET, &hs, &hd);
hptr = &hs;
#else
#if defined(GETHOSTBYNAME_R_GLIBC2)
/* Linux glibc2+ */
gethostbyaddr_r((char *)&sockaddr->sa.sin.sin_addr, v
sizeof(struct in_addr), AF_INET,
&hs, tmp, GETHOSTBYNAME_BUFLEN - 1, &hptr, &hosterror);
#else
/* Solaris, Irix et alia */
hptr = gethostbyaddr_r((char *)&sockaddr->sa.sin.sin_addr, w
sizeof(struct in_addr), AF_INET,
&hs, tmp, GETHOSTBYNAME_BUFLEN, &hosterror);
#endif /* !defined(GETHOSTBYNAME_R_GLIBC2) */
if (!hptr) {
*hostname = NULL;
return hosterror + APR_OS_START_SYSERR;
}
#endif /* !defined(GETHOSTBYNAME_R_HOSTENT_DATA) */
#else
struct hostent *hptr;
hptr = gethostbyaddr((char *)&sockaddr->sa.sin.sin_addr, x
sizeof(struct in_addr), AF_INET);
#endif
if (hptr) {
*hostname = sockaddr->hostname = apr_pstrdup(sockaddr->pool, hptr->h_name);
return APR_SUCCESS;
}
*hostname = NULL;
#if defined(WIN32)
return apr_get_netos_error();
#elif defined(OS2)
return h_errno;
#else
return h_errno + APR_OS_START_SYSERR;
#endif
#endif
函数中众多的预定义让人眼花缭乱。不过最主要的预定义处理还在于对gethostbyaddr()函数的调用。从上面的代码中可以看出,gethostbyaddr有一个函数变形gethostbyaddr_r,而且不同平台下的gethostbyaddr_t函数的参数也不相同,要了解详细的原因,必须了解一些函数可重入的概念。
所谓可重入函数是指一个可以被多个任务调用的函数,任务在调用时候不必担心数据会出错;通常情况下下面的函数是不可重入的:
(1)、函数体内使用了静态的数据结构;
(2)、函数体内调用了malloc()或者free()函数;
(3)、函数体内调用了标准I/O函数。
通常情况下,在一个UNIX进程中发生重入问题的条件是:从主程序中和某个信号处理函数中同时调用某个不可重入函数.。另外在多线程应用中也会出现函数重入的问题。不幸的是由于历史的原因,我们经常使用的gethostbyaddr也是一个不可重入的函数,因为它们都返回指向同一个静态结构的指针。关于gethostbyaddr的重入问题,《Unix网络编程 第一卷:套接口API》中文版第二版的第207页中有一段描述,摘抄如下:
不幸的是,重入问题比他表面看起来更要严重。首先,关于gethostbyname和gethostbyaddr的重入问题无标准可循。POSIX规范声明这两个函数不必是可重入的。Unix98只说这两个函数必须是线程安全的。
其次,关于_r函数也没有标准可循。Solaris 2.X,Digital Unix 4.0和HP-UX 10.30都提供了可重入版本的gethostbyaddr_r函数,不过它们的参数并不相同,不同版本的gethostbyaddr_r函数原型如下表所示:
操作系统平台 |
函数原型 |
solaris |
struct hostent* gethostbyaddr_r(const char *addr, int len, int type,
struct hostent *result, char *buf, int buflen, int * h_errnop); |
AIX,HP-UX,Digital Unix |
int gethostbyaddr_r(const char *addr, int len, int type, struct hostent *result,
struct hostent_data *buffer); |
Linux glibc2+ |
int gethostbyaddr_r(const char *addr,int len, int type,struct hostent *result,
char* buf, int buflen, struct hostent *hs, int* h_errnop); |
大部分gethostbyaddr_r函数的前四个参数都相同,第一个是需要转换的地址;第二个地址的字节大小,用sizeof(struct in_addr)表示;第三个是需要转换地址的协议族,或者是AF_INET,或者是AF_INET6;第四个则是描述主机的hostent结构。区别通常在后几个参数:
对于Solaris,Irix等操作系统而言,后面还需要三个额外的参数,buf是由调用者分配的并且大小为buflen的缓冲区,该缓冲区用于存放规范主机名称,别名指针数组,各个别名字符串,地址指针数组以及各个实际地址。如果初出错,错误码通过h_errnop指针返回,注意不是我们通常所说的h_errno返回。
对于AIX,HP-UX,Digital Unix等平台而言,后面的三个参数则被组合为一个新的数据结构hostent_data,指向该结构的指针构成本函数的第三个和最后一个参数。Apache中默认的缓冲区大小为GETHOSTBYNAME_BUFLEN,即512字节。
对于Linux glibc2+而言,gethostbyaddr_r的参数与前两者又存在一定的差异,它共计有八个参数,与Solaris平台相比多了struct hostent* hs参数。
如果操作系统平台不支持可重入的gethostaddr_r函数,那么只能使用不可重入的gethostbyaddr函数,如x所示。
返回的主机名称保存在hostent结构中,如果查询成功,从hostname参数中返回即可。
9.1.3.3 IP地址解析
APR_DECLARE(apr_status_t) apr_parse_addr_port(char **addr,
char **scope_id,
apr_port_t *port,
const char *str,
apr_pool_t *p);
9.1.3.4 子网掩码