浅谈getaddrinfo函数的超时处理机制

以下转自: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,而Windows下需要#include

1getaddrinfo函数原型

函数

参数说明

int getaddrinfo(

const char* nodename

const char* servname,

const struct addrinfo* hints,

struct addrinfo** res

);

nodename:节点名可以是主机名,也可以是数字地址。(IPV410进点分,或是IPV616进制)

servname:包含十进制数的端口号或服务名如(ftp,http

hints:是一个空指针或指向一个addrinfo结构的指针,由调用者填写关于它所想返回的信息类型的线索。

res:存放返回addrinfo结构链表的指针

函数的前两个参数分别是节点名和服务名。节点名可以是主机名,也可以是地址串(IPv4的点分十进制数表示或IPv6的十六进制数字串)。服务名可以是十进制的端口号,也可以是已定义的服务名称,如ftphttp等。注意:其中节点名和服务名都是可选项,即节点名或服务名可以为NULL,此时调用的结果将取缺省设置,后面将详细讨论。

函数的第三个参数hintsaddrinfo结构的指针,由调用者填写关于它所想返回的信息类型的线索。

函数的输出参数是一个指向addrinfo结构的链表指针res。而返回值为0代表函数成功,否则说明函数返回失败。

2addrinfo结构

结构

固定的参数

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个参数进行以下设置:nodenameservnamehintsai_flagsai_familyai_socktypeai_protocol。在6项参数中,对函数影响最大的是nodenamesernamehints.ai_flag。而ai_family只是有地址为v4地址或v6地址的区别。而ai_protocol一般是为0不作改动。

其中ai_flagsai_familyai_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;

    }

 

    /*预设调转标志canjump1,假如getaddrinfo5s内成功,则canjump0,就不用跳转了*/

canjump = 1;

/*启动5s定时器*/

    alarm(5);

   

/*进入阻塞函数getaddrinfo*/

    int ret = getaddrinfo (node, servname, hints, res);

   

    /* canjump0,无需跳转了*/

    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需要被多个线程调用的时候,由于有静态全局变量jmpbufcanjump的存在,程序就会崩溃。我们不得不寻找可重入的解决方案。

 多线程解决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)

    {

        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)

    {

        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函数。但是由于移植开源库工程量大、占用资源、耗费时间,因此没有考虑。

水平有限,不足之处,敬请指正。


你可能感兴趣的:(编程知识)