LibEvent中文幫助文檔--第14章【使用LibEvent的DNS:高和低層功能】

LiEvent中文幫助文檔--第14章

【使用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.使用LibEventDNS:高和低層功能

LibEvent提供了少量的API來解決DNS名稱,以及用於實現簡單的DNS服務.

 

我們將由名稱查詢的高層機制開始介紹,然後介紹低層機制和服務機制.

 

注意LibEvent的當前DNS客戶端實現有限制,不支持TCP查詢,DNSSec或任意記錄類型,我們希望在將來版本LibEvent修復這些問題,但不是當前版本.

 

14.1正文前頁:可移植的阻塞式名稱解析

爲移植已經使用阻塞式名字解析的程序,libevent提供了標準getaddrinfo()接口的可移植實現.對於需要運行在沒有getaddrinfo()函數,或者getaddrinfo()不像我們的替代函數那樣遵循標準的平臺上的程序,這個替代實現很有用.

 

getaddrinfo()接口由RFC 34936.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給出的規則,解析指定的nodenameservname ,建立一個evutil_addrinfo結構體鏈表,將其存儲在*res.成功時函數返回0,失敗時返回非零的錯誤碼.

 

必須至少提供 nodenameservname 中的一個.如果提供了nodename,則它是IPv4字面地址(127.0.0.1)IPv6字面地址(::1) ,或者是DNS名字(www.example.com) .如果提供了servname,則它是某網絡服務的符號名(https) ,或者是一個包含十進制端口號的字符串(443) .如果不指定servname,*res中的端口號將是零.

 

如果不指定 nodename,*res中的地址要麼是localhost(默認) ,要麼是"任意"(如果設置了EVUTIL_AI_PASSIVE) .

 

hintsai_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:如果設置了這個標誌,函數僅僅解析數值類型的IPv4IPv6地址;如果nodename要求名字查詢,函數返回EVUTIL_EAI_NONAME錯誤.

  • EVUTIL_AI_NUMERICSERV:如果設置了這個標誌,函數僅僅解析數值類型的服務名.如果servname不是空,也不是十進制整數,函數返回EVUTIL_EAI_NONAME錯誤.

  • EVUTIL_AI_V4MAPPED:這個標誌表示,如果ai_familyAF_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地址.

hintsai_famil y字段指示evutil_getaddrinfo()應該返回哪個地址.字段值可以是AF_INET ,表示只請求IPv4地址;也可以是AF_INET6,表示只請求IPv6地址;或者用AF_UNSPEC表示請求所有可用地址.

 

hintsai_socktypeai_protocol字段告知evutil_getaddrinfo()將如何使用返回的地址.這兩個字段值的意義與傳遞給socket()函數的socktypeprotocol參數值相同.

 

成功時函數新建一個 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結構體爲libeventDNS解析器保持狀態和配置.關於如何獲取evdns_base指針,請看下一節.

 

如果失敗或者立即成功,函數返回NULL.否則,函數返回一個evdns_getaddrinfo_request指針.在解析完成之前可以隨時使用evdns_getaddrinfo_cancel()和這個指針來取消解析.

 

注意:不論evdns_getaddrinfo()是否返回NULL,是否調用了evdns_getaddrinfo_cancel() ,回調函數總是會被調用.

 

evdns_getaddrinfo()內部會複製nodenameservnamehints參數,所以查詢進行過程中不必保持這些參數有效.

 

示例:使用  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文件讀取domainsearch字段以及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:同時請求IPv4IPv6地址時, 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()就沒有效果了。



<<下一章>>



發佈了81 篇原創文章 · 獲贊 118 · 訪問量 43萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章