淺談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:節點名可以是主機名,也可以是數字地址。(IPV410進點分,或是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連接超時
 
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM