【使用LibEvent的DNS:高和低層功能】
Libevent
快速可移植非阻塞式網絡編程
修訂歷史
版本
日期
作者
備註
V1.0
2016-11-15
周勇
Libevent編程中文幫助文檔
文檔是2009-2012年由Nick-Mathewson基於Attribution-Noncommercial-Share Alike許可協議3.0創建,未來版本將會使用約束性更低的許可來創建.
此外,本文檔的源代碼示例也是基於BSD的"3條款"或"修改"條款.詳情請參考BSD文件全部條款.本文檔最新下載地址:
英文:http://libevent.org/
中文:http://blog.csdn.net/zhouyongku/article/details/53431750
請下載並運行"gitclonegit://github.com/nmathewson/libevent- book.git"獲取本文檔描述的最新版本源碼.
14.使用LibEvent的DNS:高和低層功能
LibEvent提供了少量的API來解決DNS名稱,以及用於實現簡單的DNS服務.
我們將由名稱查詢的高層機制開始介紹,然後介紹低層機制和服務機制.
注意LibEvent的當前DNS客戶端實現有限制,不支持TCP查詢,DNSSec或任意記錄類型,我們希望在將來版本LibEvent修復這些問題,但不是當前版本.
14.1正文前頁:可移植的阻塞式名稱解析
爲移植已經使用阻塞式名字解析的程序,libevent提供了標準getaddrinfo()接口的可移植實現.對於需要運行在沒有getaddrinfo()函數,或者getaddrinfo()不像我們的替代函數那樣遵循標準的平臺上的程序,這個替代實現很有用.
getaddrinfo()接口由RFC 3493的6.1節定義.關於libevent如何不滿足其一致性實現的概述,請看下面的"兼容性提示"節.
接口
struct evutil_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 evutil_addrinfo* ai_next;
};
#define EVUTIL_AI_PASSIVE / * ...* /
#define EVUTIL_AI_CANONNAME / * ...* /
#define EVUTIL_AI_NUMERICHOST / * ...* /
#define EVUTIL_AI_NUMERICSERV / * ...* /
#define EVUTIL_AI_V4MAPPED / * ...* /
#define EVUTIL_AI_ALL / * ...* /
#define EVUTIL_AI_ADDRCONFIG / * ...* /
int evutil_getaddrinfo(const char* nodename, const char * servname,
const struct evutil_addrinfo* hints, struct evutil_addrinfo ** res);
void evutil_freeaddrinfo(struct evutil_addrinfo* ai);
const char* evutil_gai_strerror(int err);
evutil_getaddrinfo()函數試圖根據hints給出的規則,解析指定的nodename和servname ,建立一個evutil_addrinfo結構體鏈表,將其存儲在*res中.成功時函數返回0,失敗時返回非零的錯誤碼.
必須至少提供 nodename和servname 中的一個.如果提供了nodename,則它是IPv4字面地址(如127.0.0.1)、IPv6字面地址(如::1) ,或者是DNS名字(如www.example.com) .如果提供了servname,則它是某網絡服務的符號名(如https) ,或者是一個包含十進制端口號的字符串(如443) .如果不指定servname,則*res中的端口號將是零.
如果不指定 nodename,則*res中的地址要麼是localhost(默認) ,要麼是"任意"(如果設置了EVUTIL_AI_PASSIVE) .
hints的ai_flags 字段指示evutil_getaddrinfo如何進行查詢,它可以包含0個或者多個以或運算連接的下述標誌:
-
EVUTIL_AI_PASSIVE:這個標誌指示將地址用於監聽,而不是連接.通常二者沒有差別,除非nodename爲空:對於連接,空的nodename表示localhost (127.0.0.1或者::1) ;而對於監聽,空的nodename表示任意(0.0.0.0或者::0) .
-
EVUTIL_AI_CANONNAME:如果設置了這個標誌,則函數試圖在ai_canonname字段中報告標準名稱.
-
EVUTIL_AI_NUMERICHOST:如果設置了這個標誌,函數僅僅解析數值類型的IPv4和IPv6地址;如果nodename要求名字查詢,函數返回EVUTIL_EAI_NONAME錯誤.
-
EVUTIL_AI_NUMERICSERV:如果設置了這個標誌,函數僅僅解析數值類型的服務名.如果servname不是空,也不是十進制整數,函數返回EVUTIL_EAI_NONAME錯誤.
-
EVUTIL_AI_V4MAPPED:這個標誌表示,如果ai_family是AF_INET6,但是找不到IPv6地址,則應該以v4映射(v4-mapped)型IPv6地址的形式返回結果中的IPv4地址.當前evutil_getaddrinfo()不支持這個標誌,除非操作系統支持它.
-
EVUTIL_AI_ALL:如果設置了這個標誌和EVUTIL_AI_V4MAPPED,則無論結果是否包含IPv6地址,IPv4地址都應該以v4映射型IPv6地址的形式返回.當前evutil_getaddrinfo()不支持這個標誌,除非操作系統支持它.
EVUTIL_AI_ADDRCONFIG:如果設置了這個標誌,則只有系統擁有非本地的IPv4地址時,結果才包含IPv4地址;只有系統擁有非本地的IPv6地址時,結果才包含IPv6地址.
hints的ai_famil y字段指示evutil_getaddrinfo()應該返回哪個地址.字段值可以是AF_INET ,表示只請求IPv4地址;也可以是AF_INET6,表示只請求IPv6地址;或者用AF_UNSPEC表示請求所有可用地址.
hints的ai_socktype和ai_protocol字段告知evutil_getaddrinfo()將如何使用返回的地址.這兩個字段值的意義與傳遞給socket()函數的socktype和protocol參數值相同.
成功時函數新建一個 evutil_addrinfo結構體鏈表,存儲在*res中,鏈表的每個元素通過ai_next指針指向下一個元素.因爲鏈表是在堆上分配的,所以需要調用evutil_freeaddrinfo()進行釋放.
如果失敗,函數返回數值型的錯誤碼:
-
EVUTIL_EAI_ADDRFAMILY:請求的地址族對nodename沒有意義.
-
EVUTIL_EAI_AGAIN:名字解析中發生可以恢復的錯誤,請稍後重試.
-
EVUTIL_EAI_FAIL:名字解析中發生不可恢復的錯誤:解析器或者DNS服務器可能已經崩潰.
-
EVUTIL_EAI_BADFLAGS:hints中的ai_flags字段無效.
-
EVUTIL_EAI_FAMILY:不支持hints中的ai_family字段.
-
EVUTIL_EAI_MEMORY:迴應請求的過程耗盡內存.
-
EVUTIL_EAI_NODATA:請求的主機不存在.
-
EVUTIL_EAI_SERVICE:請求的服務不存在.
-
EVUTIL_EAI_SOCKTYPE:不支持請求的套接字類型,或者套接字類型與ai_protocol不匹配.
-
EVUTIL_EAI_SYSTEM:名字解析中發生其他系統錯誤,更多信息請檢查errno.
-
EVUTIL_EAI_CANCEL:應用程序在解析完成前請求取消.evutil_getaddrinfo()函數從不產生這個錯誤,但是後面描述的evdns_getaddrinfo()可能產生這個錯誤.調用evutil_gai_strerror()可以將上述錯誤值轉化成描述性的字符串.
注意如果操作系統定義了 addrinfo結構體,則evutil_addrinfo僅僅是操作系統內置的addrinfo結構體的別名.類似地,如果操作系統定義了AI_*標誌,則相應的EVUTIL_AI_*標誌僅僅是本地標誌的別名;如果操作系統定義了EAI_*錯誤,則相應的EVUTIL_EAI_*只是本地錯誤碼的別名.
示例:解析主機名,建立阻塞的連接
#include <sys/socket.h>
#include <sys/types.h>
#include <stdio.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
evutil_socket_t get_tcp_socket_for_host(const char* hostname, ev_uint16_t port)
{
char port_buf[6];
struct evutil_addrinfo hints;
struct evutil_addrinfo* answer = NULL;
int err;
evutil_socket_t sock;
/* Convert the port to decimal.*/
evutil_snprintf(port_buf, sizeof(port_buf), "%d", (int)port);
/* Build the hints to tell getaddrinfo how to act.*/
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC; /* v4 or v6 is fine.*/
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP; /* We want a TCP socket*/
/* Only return addresses we can use.*/
hints.ai_flags = EVUTIL_AI_ADDRCONFIG;
/* Look up the hostname.*/
err = evutil_getaddrinfo(hostname, port_buf, &hints, &answer);
if (err != 0)
{
fprintf(stderr, "Error while resolving ’%s’: %s",
hostname, evutil_gai_strerror(err));
return -1;
}
/* If there was no error, we should have at least one answer.*/
assert(answer);
/* Just use the first answer.*/
sock = socket(answer->ai_family,
answer->ai_socktype,
answer->ai_protocol);
if (sock < 0)
return -1;
if (connect(sock, answer->ai_addr, answer->ai_addrlen))
{
/* Note that we’re doing a blocking connect in this function.*
If this were nonblocking, we’d need to treat some errors*
(like EINTR and EAGAIN) specially.*/
EVUTIL_CLOSESOCKET(sock);
return -1;
}
return sock;
}
上述函數和常量是2.0.3-alpha版本新增加的,聲明在event2/util.h中.
14.2使用 evdns_getaddrinfo()進行非阻塞名字解析
通常的 getaddrinfo(),以及上面的evutil_getaddrinfo()的問題是,它們是阻塞的:調用線程必須等待函數查詢DNS服務器,等待迴應.對於libevent,這可能不是期望的行爲.
對於非阻塞式應用,libevent提供了一組函數用於啓動DNS請求,讓libevent等待服務器迴應.
接口
typedef void ( * evdns_getaddrinfo_cb)(int result,
struct evutil_addrinfo* res,
void * arg);
struct evdns_getaddrinfo_request;
struct evdns_getaddrinfo_request* evdns_getaddrinfo(
struct evdns_base* dns_base,
const char* nodename,
const char * servname,
const struct evutil_addrinfo* hints_in,
evdns_getaddrinfo_cb cb,
void* arg);
void evdns_getaddrinfo_cancel(struct evdns_getaddrinfo_request* req);
除了不會阻塞在 DNS查詢上,而是使用libevent的底層DNS機制進行查詢外,evdns_getaddrinfo()和evutil_getaddrinfo()是一樣的.因爲函數不是總能立即返回結果,所以需要提供一個evdns_getaddrinfo_cb類型的回調函數,以及一個給回調函數的可選的用戶參數.
此外,調用evdns_getaddrinfo()還要求一個evdns_base指針.evdns_base結構體爲libevent的DNS解析器保持狀態和配置.關於如何獲取evdns_base指針,請看下一節.
如果失敗或者立即成功,函數返回NULL.否則,函數返回一個evdns_getaddrinfo_request指針.在解析完成之前可以隨時使用evdns_getaddrinfo_cancel()和這個指針來取消解析.
注意:不論evdns_getaddrinfo()是否返回NULL,是否調用了evdns_getaddrinfo_cancel() ,回調函數總是會被調用.
evdns_getaddrinfo()內部會複製nodename、servname和hints參數,所以查詢進行過程中不必保持這些參數有效.
示例:使用 evdns_getaddrinfo()的非阻塞查詢
#include <event2/dns.h>
#include <event2/util.h>
#include <event2/event.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
int n_pending_requests = 0;
struct event_base* base = NULL;
struct user_data
{
char* name; /*
the name we’re resolving*/
int idx; /* its position on the command line*/
};
void callback(int errcode, struct evutil_addrinfo* addr, void * ptr)
{
struct user_data* data = ptr;
const char* name = data->name;
if (errcode)
{
printf("%d. %s -> %s\n",
data->idx, name,
evutil_gai_strerror(errcode));
}
else
{
struct evutil_addrinfo* ai;
printf("%d. %s", data->idx, name);
if (addr->ai_canonname)
printf(" [%s]", addr->ai_canonname);
puts("");
for (ai = addr; ai; ai = ai->ai_next)
{
char buf[128];
const char* s = NULL;
if (ai->ai_family == AF_INET) {
struct sockaddr_in* sin = (struct sockaddr_in * )ai->ai_addr;
s = evutil_inet_ntop(AF_INET, &sin->sin_addr, buf, 128);
}
else if (ai->ai_family == AF_INET6)
{
struct sockaddr_in6* sin6 =(sockaddr_in6 * )ai->ai_addr;
s = evutil_inet_ntop(AF_INET6, &sin6->sin6_addr, buf, 128);
}
if (s)
printf(" -> %s\n", s);
}
evutil_freeaddrinfo(addr);
}
free(data->name);
free(data);
if (--n_pending_requests == 0)
event_base_loopexit(base, NULL);
}
/* Take a list of domain names from the
command line and resolve them in parallel.*/
int main(int argc, char** argv)
{
int i;
struct evdns_base* dnsbase;
if (argc == 1)
{
puts("No addresses given.");
return 0;
}
base = event_base_new();
if (!base)
return 1;
dnsbase = evdns_base_new(base, 1);
if (!dnsbase)
return 2;
for (i = 1; i < argc; ++i)
{
struct evutil_addrinfo hints;
struct evdns_getaddrinfo_request* req;
struct user_data* user_data;
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_flags = EVUTIL_AI_CANONNAME;
/* Unless we specify a socktype, we’ll get at least two entries for*
each address: one for TCP and one for UDP. That’s not what we*
want.*/
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
if (!(user_data = malloc(sizeof(struct user_data))))
{
perror("malloc");
exit(1);
}
if (!(user_data->name = strdup(argv[i])))
{
perror("strdup");
exit(1);
}
user_data->idx = i;
++n_pending_requests;
req = evdns_getaddrinfo(
dnsbase, argv[i], NULL /* no service name given*/,
&hints,
callback,
user_data);
if (req == NULL)
{
printf(" [request for %s returned immediately]\n", argv[i]);
/* No need to free user_data or decrement n_pending_requests; that*happened in the callback.*/
}
}
if (n_pending_requests)
event_base_dispatch(base);
evdns_base_free(dnsbase, 0);
event_base_free(base);
return 0;
}
這些函數是2.0.3-alpha版本新增加的,聲明在event2/dns.h中.
14.3創建和配置evdns_base
使用 evdns進行非阻塞DNS 查詢之前需要配置一個evdns_base.evdns_base存儲名字服務器列表和DNS配置選項,跟蹤活動的、進行中的DNS請求.
接口
struct evdns_base* evdns_base_new(struct event_base * event_base,int initialize);
void evdns_base_free(struct evdns_base* base, int fail_requests);
成功時 evdns_base_new()返回一個新建的evdns_base,失敗時返回NULL.如果initialize參數爲true,函數試圖根據操作系統的默認值配置evdns_base;否則,函數讓evdns_base爲空,不配置名字服務器和選項.
可以用 evdns_base_free()釋放不再使用的evdns_base.如果fail_request參數爲true,函數會在釋放evdns_base前讓所有進行中的請求使用取消錯誤碼調用其回調函數.
14.3.1使用系統配置初始化evdns
如果需要更多地控制 evdns_base如何初始化,可以爲evdns_base_new()的initialize參數傳遞0,然後調用下述函數.
接口
#define DNS_OPTION_SEARCH 1
#define DNS_OPTION_NAMESERVERS 2
#define DNS_OPTION_MISC 4
#define DNS_OPTION_HOSTSFILE 8
#define DNS_OPTIONS_ALL 15
int evdns_base_resolv_conf_parse(struct evdns_base
* base, int flags,
const char* filename);
#ifdef WIN32
int evdns_base_config_windows_nameservers(struct evdns_base* );
#define EVDNS_BASE_CONFIG_WINDOWS_NAMESERVERS_IMPLEMENTED
#endif
evdns_base_resolv_conf_parse()函數掃描resolv.conf格式的文件filename,從中讀取flags指示的選項(關於resolv.conf文件的更多信息,請看Unix手冊) .
-
DNS_OPTION_SEARCH:請求從resolv.conf文件讀取domain和search字段以及ndots選項,使用它們來確定使用哪個域(如果存在)來搜索不是全限定的主機名.
-
DNS_OPTION_NAMESERVERS:請求從resolv.conf中讀取名字服務器地址.
-
DNS_OPTION_MISC:請求從resolv.conf文件中讀取其他配置選項.
-
DNS_OPTION_HOSTSFILE:請求從/etc/hosts文件讀取主機列表.
-
DNS_OPTION_ALL:請求從resolv.conf文件獲取儘量多的信息.
Windows中 沒 有 可 以 告 知 名 字 服 務 器 在 哪 裏 的resolv.conf文 件,但 可 以 用
evdns_base_config_windows_nameservers()函數從註冊表(或者NetworkParams,或者
其他隱藏的地方)讀取名字服務器.
resolv.conf 文件格式
resolv.conf是一個文本文件,每一行要麼是空行,要麼包含以#開頭的註釋,要麼由一個跟隨零個或者多個參數的標記組成.可以識別的標記有:
-
nameserver:必須後隨一個名字服務器的IP地址.作爲一個擴展,libevent允許使用IP:Port或者[IPv6]:port語法爲名字服務器指定非標準端口.
-
domain:本地域名
-
search:解析本地主機名時要搜索的名字列表.如果不能正確解析任何含有少於"ndots"個點的本地名字,則在這些域名中進行搜索.比如說,如果"search"字段值爲example.com,"ndots"爲1,則用戶請求解析"www"時,函數認爲那是"www.example.com".
-
options:空格分隔的選項列表.選項要麼是空字符串,要麼具有格式option:value(如果有參數) .可識別的選項有:
-
dots:INTEGER:用於配置搜索,請參考上面的"search",默認值是1.
-
timeout:FLOAT:等待DNS服務器響應的時間,單位是秒.默認值爲5秒.
-
max-timeouts:INT:名字服務器響應超時幾次才認爲服務器當機?默認是3次.
-
max-inflight:INT:最多允許多少個未決的DNS請求?(如果試圖發出多於這麼多個請求,則過多的請求將被延
遲,直到某個請求被響應或者超時) .默認值是64.
-
attempts:INT:在放棄之前重新傳輸多少次DNS請求?默認值是3.
-
randomize-case:INT:如果非零,evdns會爲發出的DNS請求設置隨機的事務ID,並且確認迴應具有同樣的
隨機事務 ID值.這種稱作"0x20 hack"的機制可以在一定程度上阻止對DNS的簡單激活事件攻擊.這個
選項的默認值是1.
-
bind-to:ADDRESS:如果提供,則向名字服務器發送數據之前綁定到給出的地址.對於2.0.4-alpha版本,這
個設置僅應用於後面的名字服務器條目.
-
initial-probe-timeout:FLOAT:確定名字服務器當機後,libevent以指數級降低的頻率探測服務器以判斷服務器是否恢復.這個選項配置(探測時間間隔)序列中的第一個超時,單位是秒.默認值是10.
-
getaddrinfo-allow-skew:FLOAT:同時請求IPv4和IPv6地址時,
evdns_getaddrinfo()用單獨的DNS請求包分
別請求兩種地址 ,因爲有些服務器不能在一個包中同時處理兩種請求.服務器迴應一種地址類型後,函數等待一段時間確定另一種類型的地址是否到達.這個選項配置等待多長時間,單位是秒.默認值是3秒.不識別的字段和選項會被忽略.
-
14.3.2手動配置evdns
如果需要更精細地控制 evdns的行爲,可以使用下述函數:
接口
int evdns_base_nameserver_sockaddr_add(struct evdns_base* base,
const struct sockaddr* sa,
ev_socklen_t len,
unsigned flags);
int evdns_base_nameserver_ip_add(struct evdns_base* base,
const char* ip_as_string);
int evdns_base_load_hosts(struct evdns_base* base,
const char * hosts_fname);
void evdns_base_search_clear(struct evdns_base* base);
void evdns_base_search_add(struct evdns_base* base,
const char * domain);
void evdns_base_search_ndots_set(struct evdns_base* base, int ndots);
int evdns_base_set_option(struct evdns_base* base,
const char * option,
const char* val);
int evdns_base_count_nameservers(struct evdns_base* base);
evdns_base_nameserver_sockaddr_add()函數通過地址向evdns_base添加名字服務器 。當前忽略flags參數,爲向前兼容考慮,應該傳入0。成功時函數返回0,失敗時返回負值 。(這個函數在2.0.7-rc版本加入)
evdns_base_nameserver_ip_add()函數向evdns_base加入字符串表示的名字服務器,格式可以是IPv4地址、IPv6地址、帶端口號的IPv4地址(IPv4:Port),或者帶端口號的IPv6地址([IPv6]:Port) 。成功時函數返回0,失敗時返回負值。
evdns_base_load_hosts()函數從hosts_fname文件中載入主機文件(格式與/etc/hosts相同) 。成功時函數返回0,失敗時返回負值。
evdns_base_search_clear()函數從evdns_base中移除所有(通過search配置的)搜索後綴;evdns_base_search_add()則添加後綴。
evdns_base_set_option()函數設置evdns_base中某選項的值。選項和值都用字符串表示 。(2.0.3版本之前,選項名後面必須有一個冒號)解析一組配置文件後,可以使用evdns_base_count_nameservers()查看添加了多少個名字服務器。
14.3.3庫端配置
有一些爲 evdns模塊設置庫級別配置的函數:
接口
typedef void ( * evdns_debug_log_fn_type)(int is_warning, const char* msg);
void evdns_set_log_fn(evdns_debug_log_fn_type fn);
void evdns_set_transaction_id_fn(ev_uint16_t ( * fn)(void));
因爲歷史原因,evdns子系統有自己單獨的日誌。evdns_set_log_fn()可以設置一個回調函數,以便在丟棄日誌消息前做一些操作。
爲安全起見,evdns需要一個良好的隨機數發生源:使用0x20 hack的時候,evdns通過這個源來獲取難以猜(hard-to-guess)的事務ID以隨機化查詢(請參考“randomize-case”選項) 。然而,較老版本的libevent沒有自己的安全的RNG(隨機數發生器) 。此時可以通過調用evdns_set_transaction_id_fn(),傳入一個返回難以預測(hard-to-predict)的兩字節無符號整數的函數,來爲evdns設置一個更好的隨機數發生器。
2.0.4-alpha以 及 後 續 版 本 中 ,libevent有 自 己 內 置 的 安 全 的RNG,
evdns_set_transaction_id_fn()就沒有效果了。