Windows-classic-samples 中的 DNSAsyncQuery 示例:用 DnsQueryEx 实现异步 DNS 查询与结果解析
Windows-classic-samples 中的 DNSAsyncQuery 示例:用 DnsQueryEx 实现异步 DNS 查询与结果解析
本指南围绕 Windows-classic-samples 仓库中的 DNSAsyncQuery 示例展开,完整讲解如何用 Win32 的 DnsQueryEx API 以异步方式发起 DNS 查询、设置超时与取消机制、通过完成回调解析 A/AAAA/SOA/SRV 等各类资源记录,并支持指定特定 DNS 服务器。读完本文后,你能掌握 DNS_QUERY_REQUEST 的完整字段用法、异步上下文生命周期管理(引用计数 + 事件同步)的编程模型,以及命令行查询工具(DnsQuery -q <name> [-t type] [-o options] [-s server])的全部参数细节,可直接将该套异步 DNS 查询模式移植到自己的 Windows 桌面程序中。
示例概述与运行环境
该示例的定位是"演示如何异步查询 DNS 记录并解析查询结果"(原文见 Samples/DNSAsyncQuery/README.md)。它由三个源文件构成一个控制台命令行工具,编译产物名为 DnsQuery.exe(尽管主源文件叫 DnsQueryEx.cpp):
| 文件 | 职责 |
|---|---|
| DnsQueryEx.cpp | 主程序:组装异步查询请求、引用计数管理、超时取消、完成回调 |
| ParseArgs.cpp | 命令行参数解析:-q / -t / -o / -s 及帮助输出 |
| PrintDnsRecord.cpp | 按记录类型分发并打印各类 DNS 资源记录内容 |
环境要求(来自 README):
- 客户端:Windows 8.1;服务器:Windows Server 2012 R2。文档同时说明,虽然示例最初基于 Visual Studio 2013 针对 Windows 8.1 / Server 2012 R2 创建,但在多数情况下可在更新的版本上不做修改直接运行。
- 从工程文件 DnsQueryEx.vcxproj 看,工程使用
v110平台工具集(对应 VS2013)、Unicode 字符集、Level4警告级别且TreatWarningAsError开启;链接依赖包含dnsapi.lib(DnsQueryEx所在库)、ws2_32.lib(WSAStringToAddress用于解析服务器 IP)、ntdll.lib(RtlIpv4AddressToString/RtlIpv6AddressToString用于地址字符串化),子系统为Console。
命令行接口:参数、查询类型与查询选项
工具的使用形式定义在 ParseArgs.cpp 的 PrintHelp() 中:
Usage: DnsQuery -q <QueryName> [-t QueryType] [-s DnsServerIP] [-o QueryOptions]
<QueryName> Input query Name
<QueryType> Input query type: A, PTR, NS, AAAA, TXT....
<DnsServerIP> DNS Server IP address
<QueryOptions> Input query flags (use combination of following numerics or
one of the below option string):
0x0 DNS_QUERY_STANDARD
0x1 DNS_QUERY_ACCEPT_TRUNCATED_RESPONSE
0x2 DNS_QUERY_USE_TCP_ONLY
0x4 DNS_QUERY_NO_RECURSION
0x8 DNS_QUERY_BYPASS_CACHE
0x10 DNS_QUERY_NO_WIRE_QUERY
0x20 DNS_QUERY_NO_LOCAL_NAME
0x40 DNS_QUERY_NO_HOSTS_FILE
0x100 DNS_QUERY_WIRE_ONLY
0x400 DNS_QUERY_MULTICAST_ONLY
0x800 DNS_QUERY_NO_MULTICAST
0x1000 DNS_QUERY_TREAT_AS_FQDN
0x200000 DNS_QUERY_DISABLE_IDN_ENCODING
几个值得注意的解析细节(对应 ParseArguments(),位于 ParseArgs.cpp#L182-L300):
- 默认值:
-t缺省为DNS_TYPE_A,-o缺省为0(即DNS_QUERY_STANDARD),-q为必填项,缺少时打印帮助并返回ERROR_INVALID_PARAMETER。 - 查询类型双模式解析:
ExtractUserFriendlyQueryType()(ParseArgs.cpp#L109-L131)先把输入与一张包含 50 余种条目的符号表比对(ZERO、A、NS、CNAME、SOA、PTR、MX、TXT、AAAA、SRV、SIG、KEY、DS、RRSIG、NSEC、NSEC3、NSEC3PARAM等,见TypeTable),映射到对应的DNS_TYPE_*常量;匹配失败则回退为按数值解析(wcstoul(..., 0)),因此可以传十进制/十六进制记录类型号。 - 查询选项双模式解析:
ExtractUserFriendlyQueryOptions()采用同样的策略,可传DNS_QUERY_BYPASS_CACHE这类符号名,也可传0x8这类位值,便于组合多个选项位。
异步查询核心流程:DnsQueryRequest、引用计数与超时取消
主程序 wmain()(DnsQueryEx.cpp#L84-L208)完整演示了一次异步 DNS 查询的生命周期,可以按以下五步理解:
第 1 步:分配带引用计数的查询上下文。 QUERY_CONTEXT 结构(DnsQueryEx.cpp#L28-L37)聚合了整次异步查询所需的全部状态:
typedef struct _QueryContextStruct
{
ULONG RefCount;
WCHAR QueryName[DNS_MAX_NAME_BUFFER_LENGTH];
WORD QueryType;
ULONG QueryOptions;
DNS_QUERY_RESULT QueryResults;
DNS_QUERY_CANCEL QueryCancelContext;
HANDLE QueryCompletedEvent;
}QUERY_CONTEXT, *PQUERY_CONTEXT;
AllocateQueryContext()(DnsQueryEx.cpp#L239-L271)从进程堆分配该结构(HEAP_ZERO_MEMORY),把 QueryResults.Version 初始化为 DNS_QUERY_RESULTS_VERSION1,并创建一个自动重置事件 QueryCompletedEvent 用于等待查询完成。引用计数的意义在于:DnsQueryEx 的文档要求 QueryResults 与 QueryCancelContext 指针在查询完成前必须保持有效(源码注释中明确写到 "QueryResults and QueryCancelContext should be valid till query completes"),因此主线程先持有一份引用,在发起查询前再 AddReferenceQueryContext() 追加一份(DnsQueryEx.cpp#L155-L163),交给完成回调在结束时释放;两份引用都归零后 DeReferenceQueryContext() 才会关闭事件句柄并释放内存,避免了回调与主线程的释放竞争。
第 2 步:填充 DNS_QUERY_REQUEST。 关键字段包括:
ZeroMemory(&DnsQueryRequest, sizeof(DnsQueryRequest));
DnsQueryRequest.Version = DNS_QUERY_REQUEST_VERSION1;
DnsQueryRequest.QueryName = QueryContext->QueryName;
DnsQueryRequest.QueryType = QueryContext->QueryType;
DnsQueryRequest.QueryOptions = (ULONG64)QueryContext->QueryOptions;
DnsQueryRequest.pQueryContext = QueryContext;
DnsQueryRequest.pQueryCompletionCallback = QueryCompleteCallback;
即版本标识为 DNS_QUERY_REQUEST_VERSION1,把解析好的查询名、类型、选项(QueryOptions 是 ULONG64,这里从 ULONG 提升)、用户上下文和完成回调一并挂入请求结构。
第 3 步:发起异步查询并区分两种返回。 调用 DnsQueryEx(&DnsQueryRequest, &QueryContext->QueryResults, &QueryContext->QueryCancelContext) 后(DnsQueryEx.cpp#L161-L175):
- 返回
DNS_REQUEST_PENDING:查询被接受,真正在后台进行,之后系统会调用QueryCompleteCallback; - 返回其他值(例如查询直接命中本地缓存而同步完成):完成例程不会被调用,示例代码此时手动调用一次
QueryCompleteCallback(QueryContext, &QueryContext->QueryResults)走统一的完成路径。
第 4 步:超时等待与取消。 主线程在 QueryCompletedEvent 上等待,超时时间为 5 秒(QueryTimeout = 5 * 1000,DnsQueryEx.cpp#L92):
if (WaitForSingleObject(QueryContext->QueryCompletedEvent,
QueryTimeout) == WAIT_TIMEOUT)
{
wprintf(L"The query took longer than %d seconds to complete; "
L"cancelling the query...\n", QueryTimeout/1000);
DnsCancelQuery(&QueryContext->QueryCancelContext);
WaitForSingleObject(QueryContext->QueryCompletedEvent, INFINITE);
}
这里体现了一个重要的 API 语义:DnsCancelQuery 只是一个"取消请求",它会加速流程结束,但并不保证回调立即返回——所以取消之后仍要以 INFINITE 继续等待完成回调(源码注释:"Cancel is just a request which will speed the process. It should still wait for the completion callback")。
第 5 步:完成回调统一收尾。 QueryCompleteCallback()(DnsQueryEx.cpp#L337-L363)无论成功失败都执行固定的收尾序列:
- 若
QueryResults->QueryStatus == ERROR_SUCCESS,打印成功信息并调用PrintDnsRecordList(QueryResults->pQueryRecords)输出全部记录;否则打印错误码; - 若存在记录链表,用
DnsRecordListFree(QueryResults->pQueryRecords, DnsFreeRecordList)释放记录及关联字符串; SetEvent(QueryContext->QueryCompletedEvent)通知主线程;DeReferenceQueryContext(&QueryContext)释放回调持有的那一份引用。
至此主线程 exit 标签处再 DeReferenceQueryContext 释放自己那份引用,整个上下文生命周期闭环。
DNS 结果解析:按类型分发的记录打印
PrintDnsRecord.cpp 展示了如何消费 DNS_QUERY_RESULT 中的记录链表。每条 PDNS_RECORD 都是链表节点(pNext),PrintRecord()(PrintDnsRecord.cpp#L281-L314)先打印通用头部字段——Owner(所有者域名)、Type、Flags(含 Section / Delete / CharSet 位域)、TTL、DataLength 等——再按 wType 索引到函数指针表 PrintDataTable 调类型专属打印函数(PrintDnsRecord.cpp#L225-L279)。
示例实现了以下类型的专属解析:
| 记录类型 | 打印函数 | 提取的字段 |
|---|---|---|
| A | ARecordPrint |
把 Data.A.IpAddress 经 RtlIpv4AddressToString 转成点分十进制 |
| AAAA | AaaaRecordPrint |
Data.AAAA.Ip6Address 经 RtlIpv6AddressToString 转成 IPv6 字符串 |
| PTR / NS / CNAME 等 | PtrRecordPrint |
统一取 Data.PTR.pNameHost(主机名字段) |
| SOA | SoaRecordPrint |
主服务器、管理员邮箱、序列号、Refresh / Retry / Expire / 默认 TTL |
| MX / AFSDB / RT | MxRecordPrint |
wPreference(优先级)与 pNameExchange(交换/目标主机) |
| TXT / HINFO / X25 等 | TxtRecordPrint |
遍历 Data.TXT.pStringArray 数组逐条输出字符串 |
| SRV | SrvRecordPrint |
wPriority、wWeight、wPort、pNameTarget |
| SIG / RRSIG | SigRecordPrint |
覆盖类型、算法、标签数、原 TTL、签名起止时间、Key Tag、签名者名 |
| KEY / DNSKEY | KeyRecordPrint |
标志位、协议、算法 |
| DS | DsRecordPrint |
Key Tag、算法、摘要类型 |
| NSEC | NsecRecordPrint |
下一域名 |
| NSEC3 / NSEC3PARAM | Nsec3RecordPrint 等 |
哈希算法、标志、迭代次数 |
表外或无实现(表中为 NULL)的类型只打印通用头部,不会崩溃——PrintRecord 里有 DnsRecord->wType < ARRAYSIZE(PrintDataTable) 的越界保护(PrintDnsRecord.cpp#L309-L313)。PrintDnsRecordList() 沿 pNext 遍历整条链,逐条调用 PrintRecord 输出(PrintDnsRecord.cpp#L316-L326)。
指定 DNS 服务器:构造 DNS_ADDR_ARRAY
-s 参数用于把查询定向到指定的 DNS 服务器 IP,而非系统的默认解析器。实现位于 CreateDnsServerList()(DnsQueryEx.cpp#L277-L330):
- 先
WSAStartup(MAKEWORD(2, 2), ...)初始化 Winsock; - 用
WSAStringToAddress把 IP 字符串解析进SOCKADDR_STORAGE,先按AF_INET(IPv4)尝试,失败再以AF_INET6(IPv6)重试; - 成功后把地址拷贝进
DNS_ADDR_ARRAY的第一个槽位(MaxCount = 1、AddrCount = 1,拷贝长度为DNS_ADDR_MAX_SOCKADDR_LENGTH),最后WSACleanup()。
只有当用户显式指定了服务器(ServerIp<a href="https://link.gitcode.com/i/1e594920d6010950cf644b77afab7c62" target="_blank">0] != L'\0')时,才会把该数组指针挂到 DnsQueryRequest.pDnsServerList([DnsQueryEx.cpp#L142-L153);不指定时请求结构保持 ZeroMemory 后的默认状态,走系统默认 DNS 配置。
构建与运行
README 给出的操作步骤(对应 README 的 "Build the sample" / "Run the sample" 章节):
- 启动 Visual Studio,选择 File > Open > Project/Solution;
- 进入示例目录
Samples/DNSAsyncQuery/cpp,打开解决方案文件 DnsQueryEx.sln; - 按 F7(Visual Studio 2013 中为 F6)或使用 Build > Build Solution 编译,产物输出到默认的 Debug/Release 目录,可执行文件名
DnsQuery.exe(由 DnsQueryEx.vcxproj 中<TargetName>DnsQuery</TargetName>决定); - 调试运行按 F5 或 Debug > Start Debugging;不带调试运行按 Ctrl+F5 或 Debug > Start Without Debugging。
运行后即可用命令行发起查询,例如:
DnsQuery -q example.com :: 默认 A 记录、标准选项
DnsQuery -q example.com -t AAAA :: 按符号名查 IPv6 记录
DnsQuery -q example.com -t SOA :: 查 SOA 记录
DnsQuery -q example.com -o DNS_QUERY_BYPASS_CACHE -s 8.8.8.8
:: 绕过缓存,并定向到指定 DNS 服务器
关键要点小结
- API 选择:同步场景用
DnsQuery,异步场景用DnsQueryEx;两者共用同一套记录解析与释放逻辑(DnsRecordListFree+DnsFreeRecordList)。 - 生命周期约束:
DNS_QUERY_RESULT和DNS_QUERY_CANCEL指针必须存活到完成回调返回,示例用引用计数的QUERY_CONTEXT保证这一点;跨线程传递查询状态时这是必须遵守的约束。 - 双路径完成:
DNS_REQUEST_PENDING表示回调稍后触发;否则查询已同步完成,回调不会被调用,需要调用方自己补齐完成逻辑——这是很多异步 DNS 调用容易漏掉的分支。 - 取消不是同步终止:
DnsCancelQuery发出后仍须等待完成回调,主流程不能直接释放上下文。 - 服务器定向:通过
pDnsServerList传入DNS_ADDR_ARRAY即可覆盖默认解析器,地址可用WSAStringToAddress从字符串构造,同时兼容 IPv4/IPv6。
相关 API 背景可参考 Windows SDK 头文件 <windns.h> 中的 DNS_QUERY_REQUEST / DNS_QUERY_RESULT / DNS_QUERY_CANCEL 定义;仓库内还有一个姊妹示例 DNSAsyncNetworkNameResolution,演示的是基于 Winsock GetAddrInfoEx 的异步主机名解析,与本示例的 DnsQueryEx 记录级查询形成互补。