Windows-classic-samples 示例解析:使用 GetAddrInfoEx 实现异步 DNS 名称解析

原创2026-10-15 17:35:161,991 阅读
文章标签:示例工程

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 和宽字符 API GetAddrInfoExW;
  • 链接库中显式包含 Ws2_32.lib,这是使用 Winsock 2 函数(WSAStartup、GetAddrInfoExW、WSAAddressToString 等)的必备依赖;
  • 子系统为 Console(控制台应用程序),便于通过命令行直接观察输出。

构建与运行

构建步骤

按 README 的指引:

  1. 打开解决方案文件 ResolveName.sln(位于 Samples/DNSAsyncNetworkNameResolution/cpp/ 目录)。
  2. 在 Visual Studio 中加载完成后,按 F7(Visual Studio 2013 为 F6),或从顶部菜单执行 Build → Build Solution。
  3. 编译产物默认输出在工程的 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 支持的两条异步完成路径:

  1. 完成回调方式(本例采用):在 lpOverlapped 中携带 OVERLAPPED,并在 lpCompletionRoutine 传入回调函数。查询完成时 Winsock 调用回调,由回调负责处理结果与通知;
  2. 事件方式:在 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 编程感兴趣的读者,可以进一步查阅仓库中的相关样本:

本示例以最短的代码完整呈现了 GetAddrInfoExW + 完成回调 + OVERLAPPED 上下文 + 超时取消的标准组合,是进入 Windows 异步网络编程的优质起点。

登录后查看全文
Windows-classic-samples