以下轉自: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函數原型
函數的前兩個參數分別是節點名和服務名。節點名可以是主機名,也可以是地址串(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(¶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)<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(¶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)<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函數。但是由於移植開源庫工程量大、占用資源、耗費時間,因此沒有考慮。
水平有限,不足之處,敬請指正。
- 1. 超時機制處理
- 2. Android超時機制的處理
- 3. 淺談java異常處理機制
- 4. 淺談Android消息處理機制
- 5. 淺談異常處理機制
- 6. android 淺談消息處理機制
- 7. 淺談字符串處理函數(二)
- 8. 淺談字符串處理函數(三)
- 9. 淺談Java代理機制
- 10. 談服務超時問題的處理
- 更多相關文章...
-
每一個你不滿意的現在,都有一個你沒有努力的曾經。