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

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

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

  • 5
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
getaddrinfo函数是用于将主机名和服务名转化为套接字地址的函数。该函数可以根据传入的参数获取与主机名和服务名相匹配的地址信息,返回一个addrinfo结构体链表,每个结构体包含了主机名、服务名、协议、IP地址等信息。 下面是该函数的使用方法: ```c #include <sys/types.h> #include <sys/socket.h> #include <netdb.h> int getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); ``` 其中,node表示主机名或IP地址,service表示服务名或端口号,hints表示筛选条件,res表示返回的addrinfo结构体链表。 示例代码: ```c #include <stdio.h> #include <stdlib.h> #include <string.h> #include <netdb.h> #include <arpa/inet.h> int main(int argc, char *argv[]) { struct addrinfo hints, *res; int status; memset(&hints, 0, sizeof hints); // 清空hints结构体 hints.ai_family = AF_UNSPEC; // 既可以是IPv4也可以是IPv6 hints.ai_socktype = SOCK_STREAM; // 流式套接字 if (argc != 2) { fprintf(stderr, "usage: %s hostname\n", argv[0]); return 1; } if ((status = getaddrinfo(argv[1], NULL, &hints, &res)) != 0) { fprintf(stderr, "getaddrinfo error: %s\n", gai_strerror(status)); return 2; } printf("IP addresses for %s:\n\n", argv[1]); struct addrinfo *p; char ipstr[INET6_ADDRSTRLEN]; for (p = res; p != NULL; p = p->ai_next) { void *addr; char *ipver; if (p->ai_family == AF_INET) { // IPv4 struct sockaddr_in *ipv4 = (struct sockaddr_in *)p->ai_addr; addr = &(ipv4->sin_addr); ipver = "IPv4"; } else { // IPv6 struct sockaddr_in6 *ipv6 = (struct sockaddr_in6 *)p->ai_addr; addr = &(ipv6->sin6_addr); ipver = "IPv6"; } inet_ntop(p->ai_family, addr, ipstr, sizeof ipstr); printf("%s: %s\n", ipver, ipstr); } freeaddrinfo(res); // 释放addrinfo链表 return 0; } ``` 该程序接受一个主机名作为命令行参数,然后使用getaddrinfo函数获取该主机名对应的IP地址信息,并打印出所有的IP地址。其中,将hints结构体的ai_family成员设置为AF_UNSPEC表示既可以是IPv4也可以是IPv6,hints结构体的ai_socktype成员设置为SOCK_STREAM表示流式套接字。在for循环中,判断addrinfo结构体的ai_family成员,来确定IPv4或IPv6的地址信息,并使用inet_ntop函数将网络字节序转换为主机字节序的IP地址,并打印出来。最后,记得释放addrinfo链表。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值