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

来源:互联网 发布:下载刷赞软件 编辑:程序博客网 时间:2024/06/10 03:16

以下转自: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>

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

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


0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 台湾人抗拒统大陆人中国人怎么办 如果一年的公休日耍不完怎么办 我一个月要歇三天班怎么办 学校没给报到证怎么办 员工休息一天老板拉着脸怎么办 换工作了报到证怎么办 幼儿园的孩子不写作业怎么办 幼儿园宝宝不写作业怎么办 幼儿园孩子不写作业怎么办 光盘数据面写字了怎么办 娃娃的手断了怎么办 联币金融跑路了怎么办 胃消化不了想吐怎么办 吃撑了恶心想吐怎么办 mac口红膏体晃动怎么办 excl图标和以前不一样了怎么办 冰箱显示板坏了怎么办 冰箱电脑显示屏坏了怎么办 冰箱的电子显示屏坏了怎么办 指导别人炒股亏损了怎么办 没有协议委托别人炒股亏损怎么办 炒股亏了很多钱崩溃了怎么办 帮人炒股亏了很多钱怎么办 破净买入继续跌怎么办 国画颜色上错了怎么办 宝宝5个月脸一直开裂怎么办 宝宝冻脸怎么办小妙招 3岁小儿长期便秘怎么办 脚裂了怎么办小妙招 宝宝脸风吹裂了怎么办 宝宝脸被风吹裂了怎么办 小宝贝的手有时候抖怎么办 打球把手戳肿了怎么办 阴茎上皮肤皴了怎么办 手皮肤干燥起皮怎么办 一到冬天手脚冰凉怎么办 脸上的皮肤被搓掉了怎么办 皮肤的表皮搓掉了怎么办 固体水彩经常掉出来怎么办 画水彩纸皱了怎么办 画完水彩纸皱了怎么办