Windows-classic-samples 示例解析:使用 GetAddrInfoEx 实现异步 DNS 名称解析
Windows-classic-samples 示例解析:使用 GetAddrInfoEx 实现异步 DNS 名称解析
导读
在桌面应用程序中,将主机名解析为 IP 地址是网络编程的常见起点,而 getaddrinfo 的阻塞特性会让 UI 线程或主流程长时间卡在 DNS 查询上。本篇文章以 Windows-classic-samples 仓库中的 DNSAsyncNetworkNameResolution 示例 为骨架,完整讲解如何借助 Winsock 2 的 GetAddrInfoExW 以异步方式解析网络名称,包括完成回调(completion routine)机制、OVERLAPPED 上下文管理、5 秒超时自动取消、结果遍历与资源释放。读完本文,你将掌握一套可直接复用的异步名称解析实现范式,并理解其底层调用链与容易踩坑的细节。
示例概览
该示例位于 Samples/DNSAsyncNetworkNameResolution,是一个极简的 C++ 控制台程序。它演示了如何通过 GetAddrInfoEx API 异步解析网络名称,并在查询完成后把解析到的 IP 地址逐一打印出来。与传统的阻塞式 getaddrinfo 不同,GetAddrInfoEx 允许调用方指定完成回调或 OVERLAPPED 事件,使解析过程不再阻塞当前线程。
程序的行为非常简洁:输入一个主机名(如 www.example.com),异步发起解析,收到结果后打印所有地址;若查询在 5 秒内未完成,则主动取消并安全退出。整个逻辑集中在单个源文件 ResolveName.cpp(约 236 行),非常适合作为理解异步 Winsock 编程的入门范本。
系统要求与运行环境
根据仓库 README 的说明,该示例面向以下环境:
| 项目 | 要求 |
|---|---|
| 客户端操作系统 | Windows 8.1 |
| 服务端操作系统 | Windows Server 2012 R2 |
| 开发工具 | Visual Studio 2013(任意 SKU)或更高版本 |
同时 README 也明确指出,示例虽然是基于 Windows 8.1 / Windows Server 2012 R2 与 Visual Studio 2013 创建的,但在多数情况下无需修改即可在更新的 Windows 与 Visual Studio 版本上运行。由于 GetAddrInfoEx 及其异步语义至今仍是 Winsock 2 的核心能力,该示例在现代 Windows 环境中依然具有直接的参考价值。
从工程配置(ResolveName.vcxproj)可以进一步确认编译前提:
- 目标平台为 Win32,提供 Debug / Release 两种配置;
- 字符集设置为 Unicode(对应预处理器宏
UNICODE;_UNICODE),因此源码使用wmain、wprintf和宽字符 APIGetAddrInfoExW; - 链接库中显式包含 Ws2_32.lib,这是使用 Winsock 2 函数(
WSAStartup、GetAddrInfoExW、WSAAddressToString等)的必备依赖; - 子系统为 Console(控制台应用程序),便于通过命令行直接观察输出。
构建与运行
构建步骤
按 README 的指引:
- 打开解决方案文件 ResolveName.sln(位于
Samples/DNSAsyncNetworkNameResolution/cpp/目录)。 - 在 Visual Studio 中加载完成后,按 F7(Visual Studio 2013 为 F6),或从顶部菜单执行 Build → Build Solution。
- 编译产物默认输出在工程的
Debug或Release目录下,可执行文件名为ResolveName.exe(由 ResolveName.vcxproj 中的TargetName指定)。
值得注意的是,工程开启了 Warning Level 4 并设置了 TreatWarningAsError(见 ResolveName.vcxproj),这意味着编译期警告会被视为错误——这有助于保证示例代码本身的整洁性,也让读者在参考该工程时更容易遵循严格的编译纪律。
运行步骤
编译完成后:
- 在 Visual Studio 中按 F5(带调试运行)或 Ctrl+F5(不带调试运行),也可以从 Debug 菜单中选择对应项。
- 程序是命令行程序,运行格式为:
ResolveName <QueryName>
其中 <QueryName> 是要解析的主机名。由于工程使用 Unicode 字符集,在控制台运行时建议配合支持宽字符输出的环境观察结果。
源码深度解析:异步名称解析的完整调用链
源码 ResolveName.cpp 虽然简短,却串起了异步 Winsock 名称解析的全流程。下面按执行顺序逐段拆解。
1. 入口与参数校验
程序使用宽字符入口 wmain(ResolveName.cpp),并在开头对命令行参数进行严格校验:
if (Argc != 2)
{
wprintf(L"Usage: ResolveName <QueryName>\n");
goto exit;
}
只接受恰好一个主机名参数,否则打印用法说明并直接退出。这种"先校验、后执行、统一收尾"的 goto exit 结构贯穿整个函数,保证无论哪一步失败都能走到统一的清理逻辑。
2. Winsock 初始化
所有 Winsock 函数使用前都必须调用 WSAStartup:
Error = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (Error != 0)
{
wprintf(L"WSAStartup failed with %d\n", Error);
goto exit;
}
IsWSAStartupCalled = TRUE;
MAKEWORD(2, 2) 请求 Winsock 2.2 版本。示例用一个 IsWSAStartupCalled 布尔标志记录初始化是否成功,以便在收尾阶段只有真正初始化成功时才调用 WSACleanup(),避免重复或无效清理。
3. 查询上下文结构
异步调用的关键在于把"本次查询的状态"封装成一个上下文结构:
typedef struct _QueryContext
{
OVERLAPPED QueryOverlapped; // 异步操作重叠结构
PADDRINFOEX QueryResults; // 解析结果链表头指针
HANDLE CompleteEvent; // 完成事件,用于主线程等待
} QUERY_CONTEXT, *PQUERY_CONTEXT;
QueryOverlapped用于承载异步操作;完成回调通过CONTAINING_RECORD宏从LPOVERLAPPED反向找回整个上下文(下文详述)。QueryResults保存GetAddrInfoExW输出的ADDRINFOEX结果链表。CompleteEvent是一个手动重置事件,回调完成后SetEvent,主线程据此得知查询结束。
代码注释明确指出:这是一个只等待/取消单个异步查询的简单示例,读者完全可以在此基础上扩展为同时管理多个异步查询的并发场景。
4. 发起异步查询:GetAddrInfoExW
在填充提示结构(Hints.ai_family = AF_UNSPEC,即同时接受 IPv4 与 IPv6)和创建完成事件之后,核心调用如下:
Error = GetAddrInfoExW(Argv[1],
NULL,
NS_DNS,
NULL,
&Hints,
&QueryContext.QueryResults,
NULL,
&QueryContext.QueryOverlapped,
QueryCompleteCallback,
&CancelHandle);
对照声明逐一说明各参数的作用:
| 参数 | 本例取值 | 含义 |
|---|---|---|
pName |
Argv[1] |
待解析的节点名称(主机名) |
pServiceName |
NULL |
服务名(如 http);本例只解析主机名,故传空 |
dwNameSpace |
NS_DNS |
使用 DNS 命名空间进行解析 |
lpNspId |
NULL |
命名空间提供程序标识符;传 NULL 表示使用系统默认提供程序 |
hints |
&Hints |
ADDRINFOEX 提示结构,ai_family 设为 AF_UNSPEC |
ppResult |
&QueryContext.QueryResults |
接收结果链表头指针的地址 |
timeout |
NULL |
超时时间(毫秒);传 NULL 表示使用系统默认超时 |
lpOverlapped |
&QueryContext.QueryOverlapped |
OVERLAPPED 结构,与完成回调配合 |
lpCompletionRoutine |
QueryCompleteCallback |
查询完成时由 Winsock 调用的回调函数 |
phHandle |
&CancelHandle |
接收可用于取消本次查询的句柄 |
说明:示例中使用的是宽字符版本
GetAddrInfoExW(对应 Unicode 工程设置);同一 API 还有GetAddrInfoEx(A 版本)与GetAddrInfoExA,宽字符版本在 Unicode 工程下是自然选择。
5. 处理 WSA_IO_PENDING:回调的两种触发路径
异步调用后的分支逻辑是整个程序正确性的关键(ResolveName.cpp):
if (Error != WSA_IO_PENDING)
{
QueryCompleteCallback(Error, 0, &QueryContext.QueryOverlapped);
goto exit;
}
- 如果
GetAddrInfoExW返回 WSA_IO_PENDING,说明操作已进入异步状态,Winsock 会在查询完成时调用完成回调; - 如果返回的是其他值(包括成功
0或各类错误码),Winsock 不会再调用回调,此时程序必须手动直接调用QueryCompleteCallback,把返回值、0 字节数与同一个OVERLAPPED指针传进去,保证回调逻辑在任何路径下都被执行一次。
这一设计让回调成为"唯一的结果汇聚点",无论查询是立即完成、异步完成还是出错,处理结果、释放内存、置起事件的逻辑都只写一份。
6. 完成回调:结果解析与释放
回调 QueryCompleteCallback(ResolveName.cpp)签名符合 Winsock 完成例程约定(DWORD Error、DWORD Bytes、LPOVERLAPPED Overlapped)。其内部关键点:
(1)找回上下文。回调拿到的只是 OVERLAPPED*,通过 CONTAINING_RECORD 宏反推得到整个 QUERY_CONTEXT:
QueryContext = CONTAINING_RECORD(Overlapped,
QUERY_CONTEXT,
QueryOverlapped);
这正是把 OVERLAPPED 作为结构体第一个成员(或作为已知偏移成员)放入上下文的经典用法——CONTAINING_RECORD 依据成员偏移量从子对象地址算出容器地址。
(2)错误检查与结果遍历。若 Error != ERROR_SUCCESS 则打印失败信息;否则遍历 ADDRINFOEX 单链表(ai_next 字段串联),对每个节点调用 WSAAddressToString 将 sockaddr 转换为可读的地址字符串并打印:
while (QueryResults)
{
AddressStringLength = MAX_ADDRESS_STRING_LENGTH;
WSAAddressToString(QueryResults->ai_addr,
(DWORD)QueryResults->ai_addrlen,
NULL,
AddrString,
&AddressStringLength);
wprintf(L"Ip Address: %s\n", AddrString);
QueryResults = QueryResults->ai_next;
}
由于 Hints.ai_family = AF_UNSPEC,同一个主机名可能同时返回 IPv4 与 IPv6 多个地址,while 循环会逐一打印,最终输出形如 ResolveName succeeded. Query Results: 后跟若干条 Ip Address: ...。
(3)释放与通知。遍历结束后调用 FreeAddrInfoEx 释放 GetAddrInfoExW 分配的结果链表,随后 SetEvent(QueryContext->CompleteEvent) 通知正在等待的主线程。注意 FreeAddrInfoEx 必须与 GetAddrInfoEx(W) 配对使用,而不是 freeaddrinfo,后者对应的是传统 getaddrinfo 系列。
7. 超时等待与查询取消
主线程在发起异步查询后进入等待逻辑(ResolveName.cpp):
if (WaitForSingleObject(QueryContext.CompleteEvent,
QueryTimeout) == WAIT_TIMEOUT )
{
wprintf(L"The query took longer than %d seconds to complete; "
L"cancelling the query...\n", QueryTimeout/1000);
GetAddrInfoExCancel(&CancelHandle);
WaitForSingleObject(QueryContext.CompleteEvent,
INFINITE);
}
QueryTimeout定义为5 * 1000(毫秒),即 5 秒;- 若 5 秒内事件未被置起(
WAIT_TIMEOUT),说明查询尚未完成,程序调用GetAddrInfoExCancel取消查询,然后以INFINITE再次等待完成事件。
为什么取消之后还要再等?源码注释给出了精确解释:GetAddrInfoExCancel 本身不会阻塞,取消是异步生效的,完成回调仍会被调用一次。如果取消后不等待回调执行完毕就直接 WSACleanup(),可能出现"异步查询尚在进行中而 Winsock 已被清理"的竞态,轻则资源泄漏,重则导致崩溃。这一"取消后必须等待完成回调"的纪律,是异步编程中极易被忽略却至关重要的细节。
8. 统一收尾
函数末端的 exit: 标签集中完成清理(ResolveName.cpp):
if (IsWSAStartupCalled)
{
WSACleanup();
}
if (QueryContext.CompleteEvent)
{
CloseHandle(QueryContext.CompleteEvent);
}
仅当 WSAStartup 确实成功时才调用 WSACleanup,随后关闭完成事件句柄,最后返回错误码作为进程退出码。整个程序对每个分配的资源(Winsock 会话、事件句柄、ADDRINFOEX 结果)都有对应的释放路径,可以作为资源管理范本。
异步完成的两种方式
源码注释(ResolveName.cpp)明确指出了 GetAddrInfoEx 支持的两条异步完成路径:
- 完成回调方式(本例采用):在
lpOverlapped中携带OVERLAPPED,并在lpCompletionRoutine传入回调函数。查询完成时 Winsock 调用回调,由回调负责处理结果与通知; - 事件方式:在
OVERLAPPED对象中设置hEvent字段,并把lpCompletionRoutine传NULL。此时查询完成时 Winsock 置起该事件,调用方通过WaitForSingleObject/GetOverlappedResult等机制在事件上等待。
两种方式分别适合不同场景:回调方式把"结果处理"封装进单一函数、便于与上下文结构绑定;事件方式则更容易融入基于事件循环或 OVERLAPPED 完成的既有 I/O 模型。示例选择回调方式并搭配一个手动重置事件来唤醒主线程,是理解两者关系的最简载体。
工程配置要点回顾
从 ResolveName.vcxproj 可以提炼出复刻该示例所需的工程设置:
| 配置项 | 取值 | 说明 |
|---|---|---|
| 配置类型 | Application | 可执行程序 |
| 平台工具集 | v110 | Visual Studio 2012 工具集(README 建议用 VS2013 或更高版本打开) |
| 字符集 | Unicode | 使用宽字符 API 与 wmain |
| 子系统 | Console | 控制台程序 |
| 附加依赖 | Ws2_32.lib | Winsock 2 链接库,必不可少 |
| 警告级别 | Level4 + TreatWarningAsError | 严格编译,警告即错误 |
| 优化 | Debug 禁用 / Release MaxSpeed | 常规配置 |
解决方案文件 ResolveName.sln 为 Visual Studio 11(2012)格式,仅包含 ResolveName 一个项目,提供 Debug|Win32 与 Release|Win32 两种配置。
扩展思路
源码注释提示,当前示例只管理单个异步查询。将 QUERY_CONTEXT 放入动态分配的结构数组或队列,即可自然扩展为:
- 并发批量解析:一次发起多个主机名的异步查询,统一由回调收集结果;
- 可配置超时:把
QueryTimeout改为命令行参数或配置项,并按查询维度分别管理; - 结果回调业务化:在回调中把解析结果投递到业务线程或消息队列,实现"解析不阻塞 UI"的桌面应用模式。
延伸阅读
对名称解析与 Winsock 编程感兴趣的读者,可以进一步查阅仓库中的相关样本:
- DNSAsyncQuery:同属 DNS 异步查询主题的另一个 C++ 示例;
- DNSAsyncNetworkNameResolution:本文所讲解示例的原始 README,包含官方构建与运行说明;
- WinsockBluetoothConnection:展示 Winsock 在蓝牙连接场景中的应用,可作为对比参考。
本示例以最短的代码完整呈现了 GetAddrInfoExW + 完成回调 + OVERLAPPED 上下文 + 超时取消的标准组合,是进入 Windows 异步网络编程的优质起点。