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

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

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

相关文章

相关标签/搜索

getaddrinfo函数

超时处理

超时机制

处理机制

浅谈

getaddrinfo

处理函数

php时间处理函数

时间处理函数

cc++时间处理函数

getaddrinfo

getaddrinfo

处理函数

超时

超时

超时

浅谈ASP.NET内部机制

浅谈

浅谈

浅谈

python 超时处理

typescript 超时机制

事件处理 机制

浅谈共享单车

EmguCV图像处理函数

c++ 图片 处理函数

ti 饱和处理函数

contiki中的shell处理机制

浅谈PCA的适用范围

python2 try 处理requests连接超时

0

分享到微博 分享到微信 分享到QQ

每日一句

    每一个你不满意的现在,都有一个你没有努力的曾经。

最新文章

原文地址:https://www.cnblogs.com/qq8533/p/12219136.html

时间: 2024-10-13 03:04:29

浅谈getaddrinfo函数的超时处理机制的相关文章

(转)浅谈移动操作系统的跨应用通信机制

[核心提示] 对开发者来说,在 iOS 上实现跨应用的通信依然是一件头疼的事.对于 iOS 的竞争对手们来说,这一问题是如何处理的呢?本文浅谈目前主流移动操作系统的跨应用通信机制. 在“应用间通信——iOS 的孤岛困境”一文中,我们曾经讨论过 iOS 上跨应用通信与内容分享的难题.而直到现在,在 iOS 上想实现跨应用的通信和内容分享依然是一件头疼的事,虽然我们已经可以使用 iOS 系统内部整合的分享功能,实现通过 Twitter.电子邮件.短消息的内容分享,但此功能尚未向第三方开发者开放,用户

浅谈intval()函数用法

1 <?php 2 $sql=mysql_query("select count(*) as total from tb_leaveword ",$conn); 3 $infos=mysql_fetch_array($sql); 4 $total=$infos['total']; //获取总留言条数 5 if($total==0){ //如果总留言条数为0,则给出提示 6 echo "<div align=center>对不起,暂无留言!</div&g

浅谈javascript函数劫持

http://www.xfocus.net/articles/200712/963.html 浅谈javascript函数劫持 文章提交:hkluoluo (luoluonet_at_hotmail.com) by luoluo on 2007-11-30 luoluonet_at_yahoo.cn http://www.ph4nt0m.org 一.概述 javascript函数劫持,也就是老外提到的javascript hijacking技术.最早还是和剑心同学讨论问题时偶然看到的一段代码,大

【python】浅谈enumerate 函数

enumerate 函数用于遍历序列中的元素以及它们的坐标: >>> for i,j in enumerate(('a','b','c')):  print i,j 0 a 1 b 2 c >>> for i,j in enumerate([1,2,3]):  print i,j 0 1 1 2 2 3 >>> for i,j in enumerate({'a':1,'b':2}):  print i,j 0 a 1 b >>> fo

开发技术--浅谈Python函数

开发|浅谈Python函数 函数在实际使用中有很多不一样的小九九,我将从最基础的函数内容,延伸出函数的高级用法.此文非科普片~~ 前言 目前所有的文章思想格式都是:知识+情感. 知识:对于所有的知识点的描述.力求不含任何的自我感情色彩. 情感:用我自己的方式,解读知识点.力求通俗易懂,完美透析知识. 正文 首先介绍函数是什么,接着走进函数,并且发现函数的高级使用方法,最后列出常用的Python的内置函数. 函数是什么? 1.函数,在代码执行的是不执行,只有在调用函数的时候才会执行. 2.函数使用

libsvm代码阅读(2):svm.cpp浅谈和函数指针(转)

svm.cpp浅谈 svm.cpp总共有3159行代码,实现了svm算法的核心功能,里面总共有Cache.Kernel.ONE_CLASS_Q.QMatrix.Solver.Solver_NU.SVC_Q.SVR_Q 8个类(如下图1所示),而它们之间的继承和组合关系如图2.图3所示.在这些类中Cache.Kernel.Solver是核心类,对整个算法起支撑作用.在以后的博文中我们将对这3个核心类做重点注解分析,另外还将对svm.cpp中的svm_train函数做一个注解分析. 图1 图2 图3

浅谈回调函数

最近学习js的异步执行,用到回调函数,对这个机制不是很理解,故查了一些资料,在此整理分享一下自己的理解. 维基百科对回调函数的定义: 在计算机程序设计中,回调函数,或简称回调(Callback 即call then back 被主函数调用运算后会返回主函数),是指通过函数参数传递到其它代码的,某一块可执行代码的引用.这一设计允许了底层代码调用在高层定义的子程序. 知乎上一个网友提供的生动描述: 你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打

浅谈JS函数节流及应用场景

说完防抖,下面我们讲讲节流,规矩就不说了,先上代码: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no&

小学生之浅谈Struts2与struts1的运行机制

Struts1工作原理图: 1.初始化:struts框架的总控制器ActionServlet是一个Servlet,它在web.xml中配置成自动启动的Servlet,在启动时总控制器会读取配置文件(struts-config.xml)的配置信息,为struts中不同的模块初始化相应的对象.(面向对象思想) 2.发送请求:用户提交表单或通过URL向WEB服务器提交请求,请求的数据用HTTP协议传给web服务器. 3.form填充:struts的总控制器ActionServlet在用户提交请求时将数