转自: https://blog.csdn.net/haima1998/article/details/51745685
以下转自:http://x3ge.com/?p=1485
在sockproxy上发现,getaddrinfo 解析域名相比ping对域名的解析,慢很多。我觉得ping用了gethostbyname解析域名。问题变为getaddrinfo解析域名,是否比 gethostbyname慢。写测试程序,分别用getaddrinfo和gethostbyname解析,发现getaddrinfo确实慢。 strace跟踪发现,getaddrinfo和DNS服务器通信10次,gethostbyname和DNS服务器通信2次。
gethostbyname是古老的域名解析方式,它的缺点是不支持IPV6,于是有gethostbyname2替换 gethostbyname,支持IPV4和IPV6。但是现在的教科书都推荐使用getaddrinfo。慢的原因是getaddrinfo默认解析 IPV6和IPV4,如果设置getaddrinfo只解析IPV4,速度和gethostbyname一样,和DNS通信2次。
域名解析函数gethostbyname和getaddrinfo,都是阻塞的,这个在非阻塞大行其道的今天,是个妨碍并发的因素。可以用 c-ares 库,实现异步解析。另外 libresolv 是一个dns解析库。
测试中调用两次gethostbyname2,分别解析IPV6和IPV4,相当于调用一次getaddrinfo。
以下转自:http://zx-star2002.blog.163.com/blog/static/3044645020153993321890/
可参考:http://blog.sina.com.cn/s/blog_56dee71a0100t36d.html
一 getaddrinfo简介
getaddrinfo提供独立于协议的名称解析,它的作用是将网址和服务,转换为IP地址和端口号的。比如说,当我们输入一个http://www.baidu.com之类的网址,getaddrinfo函数就会去DNS服务器上查找对应的IP地址,以及http服务所对应的端口号。因为一个网址往往对应多个IP地址,所以getaddrinfo得输出参数res是一个addrinfo结构体类型的链表指针,而每个addrinfo都包含一个sockaddr结构体。这些sockaddr结构体随后可由套接口函数直接使用,去尝试进行连接。
无论是Linux还是Windows操作系统下,都支持getaddrinfo函数。Linux下需要#include<netdb.h>,而Windows下需要#include <ws2tcpip.h>。
1.getaddrinfo函数原型
函数 | 参数说明 |
int getaddrinfo( const char* nodename const char* servname, const struct addrinfo* hints, struct addrinfo** res ); | nodename:节点名可以是主机名,也可以是数字地址。(IPV4的10进点分,或是IPV6的16进制) servname:包含十进制数的端口号或服务名如(ftp,http) hints:是一个空指针或指向一个addrinfo结构的指针,由调用者填写关于它所想返回的信息类型的线索。 res:存放返回addrinfo结构链表的指针 |
函数的前两个参数分别是节点名和服务名。节点名可以是主机名,也可以是地址串(IPv4的点分十进制数表示或IPv6的十六进制数字串)。服务名可以是十进制的端口号,也可以是已定义的服务名称,如ftp、http等。注意:其中节点名和服务名都是可选项,即节点名或服务名可以为NULL,此时调用的结果将取缺省设置,后面将详细讨论。
函数的第三个参数hints是addrinfo结构的指针,由调用者填写关于它所想返回的信息类型的线索。
函数的输出参数是一个指向addrinfo结构的链表指针res。而返回值为0代表函数成功,否则说明函数返回失败。
2.addrinfo结构
结构 | 固定的参数 |
typedef struct addrinfo { int ai_flags; int ai_family; int ai_socktype; int ai_protocol; size_t ai_addrlen; char* ai_canonname; struct sockaddr* ai_addr; struct addrinfo* ai_next; } | ai_addrlen must be zero or a null pointer ai_canonname must be zero or a null pointer ai_addr must be zero or a null pointer ai_next must be zero or a null pointer |
可以改动的参数 | |
ai_flags:AI_PASSIVE,AI_CANONNAME,AI_NUMERICHOST ai_family: AF_INET,AF_INET6 ai_socktype:SOCK_STREAM,SOCK_DGRAM ai_protocol:IPPROTO_IP, IPPROTO_IPV4, IPPROTO_IPV6 etc. |
3.参数说明
在getaddrinfo函数之前通常需要对以下6个参数进行以下设置:nodename、servname、hints的ai_flags、ai_family、ai_socktype、ai_protocol。在6项参数中,对函数影响最大的是nodename,sername和hints.ai_flag。而ai_family只是有地址为v4地址或v6地址的区别。而ai_protocol一般是为0不作改动。
其中ai_flags、ai_family、ai_socktype说明如下:
参数 | 取值 | 值 | 说明 |
ai_family | AF_INET | 2 | IPv4 |
AF_INET6 | 23 | IPv6 | |
AF_UNSPEC | 0 | 协议无关 | |
ai_protocol | IPPROTO_IP | 0 | IP协议 |
IPPROTO_IPV4 | 4 | IPv4 | |
IPPROTO_IPV6 | 41 | IPv6 | |
IPPROTO_UDP | 17 | UDP | |
IPPROTO_TCP | 6 | TCP | |
ai_socktype | SOCK_STREAM | 1 | 流 |
SOCK_DGRAM | 2 | 数据报 | |
ai_flags | AI_PASSIVE | 1 | 被动的,用于bind,通常用于server socket |
AI_CANONNAME | 2 | ||
AI_NUMERICHOST | 4 | 地址为数字串 |
对于ai_flags值的说明:
AI_NUMERICHOST | AI_CANONNAME | AI_PASSIVE |
0/1 | 0/1 | 0/1 |
如上表所示,ai_flagsde值范围为0~7,取决于程序如何设置3个标志位,比如设置ai_flags为 “AI_PASSIVE|AI_CANONNAME”,ai_flags值就为3。三个参数的含义分别为:
(1)AI_PASSIVE 当此标志置位时,表示调用者将在bind()函数调用中使用返回的地址结构。当此标志不置位时,表示将在connect()函数调用中使用。当节点名为NULL,且此标志置位,则返回的地址将是通配地址。如果节点名为NULL,且此标志不置位,则返回的地址将是回环地址。
(2)AI_CANNONAME当此标志置位时,在函数所返回的第一个addrinfo结构中的ai_cannoname成员中,应该包含一个以空字符结尾的字符串,字符串的内容是节点名的正规名。
(3)AI_NUMERICHOST当此标志置位时,此标志表示调用中的节点名必须是一个数字地址字符串。
二 定时器解决getaddrinfo阻塞
我们知道,域名到IP地址的DNS解析过程的大致过程如下:当某一个应用需要把主机名解析为IP地址时,该应用进程就调用解析程序,并称为DNS的一个客户,把待解析的域名放在DNS请求报文中,以UDP用户数据报方式发给本地域名服务器。本地域名服务器在查找域名后,把对应的IP地址放在回答报文中返回。应用程序获得目的主机的IP地址后即可进行通信。
若本地域名服务器不能回答该请求,则此域名服务器就暂时称为DNS的另一个客户,并向其他域名服务器发出查询请求。这种过程直至找到能够回答该请求的域名服务器为止。由于DNS是分布式系统,因此这种迭代过程也许会重复很久。
Getaddrinfo即遵循上述过程进行DNS解析的。因此它有个最重要的特征——同步阻塞。这就是说,getaddrinfo会一直阻塞,直到返回成功或者失败。根据实测,成功时一般几十毫秒即可,失败时往往需要30秒以上。这对于实际应用中来说,一般是不可忍受的。那么问题就来了:如果我需要getaddrinfo 5s超时返回,该怎么办呢?
定时器无疑是一个好办法。下面我们把项目中的实际代码拿出来一部分,来说明定时器如何使用来中止getaddrinfo的执行。
static sigjmp_buf jmpbuf;//jump from and to here
static volatile sig_atomic_t canjump;//0 = not need, 1 = need to jump
int tcl_getaddrinfo (const char *node, unsigned port, const struct addrinfo *hints, struct addrinfo **res)
{
……
/*设置SIGALRM消息的回调函数tcl_sig_alrm,下文将有该函数的定义 */
if (signal(SIGALRM, tcl_sig_alrm) == SIG_ERR)
{
return -1;
}
/* 保存起跳点。Sigsetjmp第一次被调用的时候会返回0,如果是再次跳回到这里会返回非0,从而退出 函数 */
if (sigsetjmp(jmpbuf, 1))
{
printf("getaddrinfo time out\n");
return -1;
}
/*预设调转标志canjump为1,假如getaddrinfo在5s内成功,则canjump清0,就不用跳转了*/
canjump = 1;
/*启动5s定时器*/
alarm(5);
/*进入阻塞函数getaddrinfo*/
int ret = getaddrinfo (node, servname, hints, res);
/* canjump清0,无需跳转了*/
canjump = 0;
return ret;
}
定时器SIGALRM消息处理函数tcl_sig_alrm的实现如下:
/**
* SIGALRM callback.
* @param signo: signal num, now is SIGALRM=14
*/
static void tcl_sig_alrm(int signo)
{
if (!canjump)
{
/* canjump标志已经被清0,说明getaddrinfo成功,无需跳转 */;
return;
}
/* canjump标志未被清0,说明getaddrinfo超过5s仍未返回,长跳转到sigsetjmp处 */;
siglongjmp(jmpbuf, 1); /* jump back to main, don't return */
}
我们首先利用sigsetjmp设置一个跳转恢复点,然后等定时器超时的时候,在回调函数里判断标志位以确定是否需要跳转。如果需要,那么程序会再次执行到sigsetjmp处,返回-1,从而退出getaddrinfo的阻塞。
这个方法经过验证,行之有效。可是当tcl_getaddrinfo需要被多个线程调用的时候,由于有静态全局变量jmpbuf、canjump的存在,程序就会崩溃。我们不得不寻找可重入的解决方案。
三 多线程解决getaddrinfo阻塞
多线程是个解决重入的好办法。思路是这样的:tcl_getaddrinfo函数里新启动一个子线程,在子线程里调用getaddrinfo。随后tcl_getaddrinfo判断子线程是否成功,如果5s不成功,则杀死子线程即可。
经过修改的tcl_getaddrinfo函数如下:
int tcl_getaddrinfo (const char *node, unsigned port, const struct addrinfo *hints, struct addrinfo **res)
{
……
tcl_thread_t pid;
st_addrinfoparas paras;
/* 把输入参数放入一个结构体传给子线程 */
memset(¶s, 0, sizeof(st_addrinfoparas));
paras.node = node;
paras.servname = servname;
paras.hints = hints;
paras.res = res;
paras.state = -1;/* the successful flag of tcl_thread_getaddrinfo */
/* 创建子线程,子线程函数为tcl_thread_getaddrinfo */
int ret = tcl_clone( &pid, tcl_thread_getaddrinfo, (void *)¶s, TCL_THREAD_PRIORITY_INPUT );
if( ret )
{
return -1;
}
/* 循环等待tcl_thread_getaddrinfo退出或超时,当然在这里也可以用更加高效的互斥量+信号量 */
mtime_t start = mdate();
int64_t nWaitSec = 5*1000*1000; //5s
while((mdate()-start)<nWaitSec)
{
ret = pthread_kill(pid,0);
if (0 == ret)/*子线程仍然存在,说明getaddrinfo仍然在阻塞状态*/
{
usleep(50*1000); //sleep 50ms
}
else if(ESRCH == ret) /*子线程已经不存在,说明getaddrinfo成功返回了*/
{
break;
}
};
if (-1== paras.state) /*getaddrinfo仍然在阻塞状态,杀死子线程*/
{
tcl_cancel(pid);
}
tcl_join(pid, NULL);
return paras.state;
}
子线程主函数tcl_thread_getaddrinfo定义就很简单了,只是在getaddrinfo成功之后设置了state这个标志位为0:
void* tcl_thread_getaddrinfo( void *obj )
{
st_addrinfoparas* paras = (st_addrinfoparas*)obj;
paras->state = -1;
int ret = getaddrinfo (paras->node, paras->servname, paras->hints, paras->res);
if (0 == ret)
{
paras->state = 0;
}
pthread_exit(NULL);
}
到目前为止,这个解决方案看上去很完美。但是如果我们特意给tcl_getaddrinfo反复输入无效的url,这段代码会造成很明显的内存泄露。为什么会内存泄露呢?
前面DNS的原理中谈到,主机会发送DNS请求给DNS服务器,如果这个网址是无效的,很显然DNS服务器是无法解析此网址,会把请求转达给上级DNS服务器的。发送DNS报文,同样是需要建立socket连接的。如果在socket没有关闭的时候,我们kill了这个线程,那么这个socket的资源就泄露了。多次的泄露就会明显地看出来,这在有些应用场景下,可是致命的,我们必须修改。
四 改进的多线程解决方案
好在getaddrinfo是个负责任的函数,它再慢也是会返回的。那么我们是不是可以让子线程成为可分离线程,当5s超时的时候,主线程独自返回,而令子线程其自生自灭呢?
在这种情况下,子线程getaddrinfo成功之后,探测主线程是否还存在,是不能使用互斥量、信号量的。因为这些变量都需要主线程传递进入子线程,然后父子线程通过这些变量来同步。如果主线程已经返回,甚至退出了(因为这里的主线程其实有可能是其他线程的子线程,是有可能立刻结束的),子线程一旦调用已经消失了的互斥量、信号量,就会造成程序崩溃。当然信号量、互斥量也不能定义成全局的,我们还需要可重入。在这种情况下,loop循环用pthread_kill探测就是不二法宝了。
改造后的tcl_getaddrinfo如下:
int tcl_getaddrinfo (const char *node, unsigned port, const struct addrinfo *hints, struct addrinfo **res)
{
……
tcl_thread_t pid;
st_addrinfoparas paras;
/* 把输入参数放入一个结构体传给子线程 */
memset(¶s, 0, sizeof(st_addrinfoparas));
paras.node = node;
paras.servname = servname;
paras.hints = hints;
paras.res = res;
paras.pid = pthread_self(); /*主线程自己的pid,传给子线程*/
paras.endflag = END_FLAG;/* END_FLAG = 12345,函数退出时置0,标志本函数退出*/
/* 创建子线程,子线程入口函数为tcl_thread_getaddrinfo */
int ret = tcl_clone( &pid, thread_getaddrinfo, (void *)¶s, TCL_THREAD_PRIORITY_INPUT );
if( ret )
{
return -1;
}
/* 循环查看tcl_thread_getaddrinfo是否成功返回*/
mtime_t start = mdate();
int64_t nWaitSec = 5*1000*1000; //5s
int btimeout = 1;
while((mdate()-start)<nWaitSec)
{
ret = pthread_kill(pid,0);
if (0 == ret) /*子线程仍然存在,说明getaddrinfo仍然在阻塞状态*/
{
usleep(50*1000); //sleep 50ms
}
else if(ESRCH == ret) /*子线程已经不存在,说明getaddrinfo成功返回了*/
{
btimeout = 0;// not timeout
break;
}
};
/* 根据超时标志和输出参数,判断子线程是否自行结束,是则返回成功,否则返回失败*/
if ((0 == btimeout) && (NULL != *res))
{
ret = 0;
}
else
{
ret = -1;
}
paras.endflag = 0;/*清零本函数标志*/
return ret;
}
tcl_getaddrinfo简单了,可是子线程函数thread_getaddrinfo就变复杂了:
void* thread_getaddrinfo( void *obj )
{
mtime_t start = mdate();
/* 设置自己为可分离线程 */
tcl_thread_t pid = pthread_self();
pthread_detach(pid);
/* 把输入参数都复制到本地, 以避免thread_getaddrinfo早于本线程退出,造成参数失效*/
st_addrinfoparas* inputparas = (st_addrinfoparas*)obj;
const char *node = strdup(inputparas->node);
char *servname = strdup(inputparas->servname);
struct addrinfo hints;
hints.ai_socktype = inputparas->hints->ai_socktype;
hints.ai_protocol = inputparas->hints->ai_protocol;
hints.ai_flags = inputparas->hints->ai_flags;
struct addrinfo* res = NULL;
tcl_thread_t pid_master = inputparas->pid;
/* getaddrinfo 也许会阻塞很长时间 */
int ret = getaddrinfo (node, servname, &hints, &res);
if (0 != ret)
{
goto exit;
}
/* getaddrinfo返回了,现在看看tcl_getaddrinfo线程是否还存在 */
ret = pthread_kill(pid_master, 0);
if (0 == ret && (mdate()-start)<4500000)/*存在且getaddrinfo实际上的执行时间小于4.5s*/
{
if ((inputparas == NULL) || (inputparas->res == NULL))
{
printf("thread_getaddrinfo pid:%u: inputparas == NULL\r\n", pid);
freeaddrinfo(res);
goto exit;
}
if (inputparas->endflag != END_FLAG)
{
printf("thread_getaddrinfo pid:%u: tcl_getaddrinfo %u has gone\r\n", pid, pid_master);
freeaddrinfo(res);
goto exit;
}
/*写输出参数*/
*(inputparas->res) = res;
}
else /* cl_getaddrinfo线程不存在了 */
{
freeaddrinfo(res);
}
exit:
free(node);
free(servname);
pthread_exit(NULL);
}
改造完成,经过实测没有问题。至此,getaddrinfo的超时问题总算圆满解决了!
五 总结
这篇文章,探讨了给getaddrinfo增加超时机制的方法。看起来这些步骤是一气呵成,其实中间很多周折。比如内存泄露,刚开始并不能想到就是这段代码引起的。在定位过程中,采用代码折半法,不断屏蔽代码,最终发现问题所在。反过头来才去思考、搜索资料,最终确定了泄露的原因。希望看了这篇文章的软件工程师,能够少走一些弯路,节省一点时间。
另外,有些开源库如libevent,提供了非阻塞式的getaddrinfo函数。但是由于移植开源库工程量大、占用资源、耗费时间,因此没有考虑。
水平有限,不足之处,敬请指正。