Windows-classic-samples 实战:使用 DhcpV4FailoverAddScopeToRelationship 将 DHCP 作用域加入故障转移关系

原创2026-10-14 12:58:471,999 阅读
文章标签:示例工程

Windows-classic-samples 实战:使用 DhcpV4FailoverAddScopeToRelationship 将 DHCP 作用域加入故障转移关系

本篇文章以 Windows-classic-samples 仓库中的 Samples/DHCP/AddScopeToRelationship 示例为核心,系统讲解如何在 Windows Server 的 DHCP 服务器上,通过 DHCP Server Management API 将已有的作用域(Scope)动态加入到一个已建立的 DHCP 故障转移关系(Failover Relationship)中。读完本文,你将掌握 DhcpV4FailoverAddScopeToRelationship 的调用前置条件、完整调用流程、作用域状态切换的必要性,以及配套的内存管理与错误处理实践。

示例背景:DHCP 与故障转移关系

DHCP(Dynamic Host Configuration Protocol)是一种标准化协议,它使客户端能够被动态分配各种配置参数,例如 IP 地址、子网掩码、默认网关以及其他关键的网络配置信息。DHCP 服务器集中管理这类配置数据,由网络管理员根据给定的网络环境设置合适的配置;DHCP 服务器再通过 DHCP 消息与 DHCP 客户端通信(参见 示例 README)。

当一台 DHCP 服务器出现故障时,如果网络中只有一个 DHCP 服务器,客户端将无法续租,导致网络中断。Windows Server 2012 R2 引入的 DHCP 故障转移(Failover) 机制允许两台服务器(主服务器 Primary 与伙伴服务器 Secondary/Partner)为同一组作用域提供高可用服务。一个"故障转移关系"(Failover Relationship)把两台服务器、一组作用域以及运行参数(模式、安全期、最大客户端领先时间等)绑定在一起。

本示例解决的问题是:关系已存在,但需要把一个新的作用域追加到该关系中。对应 API 为 DhcpV4FailoverAddScopeToRelationship,完整演示代码位于 AddScopeToRelationship.cpp。

示例的既定前提(Assumptions)

示例源码头部明确写明了运行该示例前必须满足的两个前提(见 AddScopeToRelationship.cpp):

  1. 服务器上已经存在一个名为 "test" 的故障转移关系;
  2. 主服务器与伙伴服务器上都必须存在相同的作用域 172.16.0.0(源码中以十六进制 0xac100000 表示),并且该作用域当前不属于任何关系。

第一个前提意味着你应该先用 CreateRelationship 示例(或 DHCP 管理控制台)创建关系;第二个前提则要求在两台服务器上预先创建完全一致的 172.16.0.0 作用域。这两条是 API 调用成功的基础,缺一不可。

核心 API:DhcpV4FailoverAddScopeToRelationship

从源码调用方式可以确认,该 API 接收两个参数(AddScopeToRelationship.cpp):

dwError = DhcpV4FailoverAddScopeToRelationship(
                    pRelationship->SecondaryServerName,  // 目标服务器(伙伴服务器)
                    pRelationship);                       // 关系结构指针 LPDHCP_FAILOVER_RELATIONSHIP
  • 服务器名:要执行添加操作的那台 DHCP 服务器。示例中先对伙伴服务器调用一次,再对主服务器调用一次,保证两端关系一致;
  • 关系结构指针:DHCP_FAILOVER_RELATIONSHIP 结构。示例通过 DhcpV4FailoverGetRelationship 获取现有关系,然后将待添加的作用域数组挂到其 pScopes 字段上,再传给 API。

该 API 属于 DHCP Server Management API(dhcpsapi.h 头文件、dhcpsapi.lib 链接库)。从工程配置看,示例以控制台应用(SubSystem 为 Console)方式链接 dhcpsapi.lib,并启用了 Unicode 字符集,相关配置见 AddScopeToRelationship.vcxproj。

调用前置动作:把作用域从关系中"取出"或准备就绪

获取已有关系:DhcpV4FailoverGetRelationship

示例的第一步是读取名为 "test" 的现有关系(AddScopeToRelationship.cpp):

dwError = DhcpV4FailoverGetRelationship(
    pwszServer,           // Server IP Address(NULL 表示当前服务器)
    pwszRelationshipName, // L"test"
    &pRelationship        // 输出:LPDHCP_FAILOVER_RELATIONSHIP
    );
if(ERROR_SUCCESS != dwError)
{
    wprintf(L"DhcpV4FailoverGetRelationship failed with Error = %d\n", dwError);
    goto cleanup;
}

pwszServer 在示例中为 NULL,表示操作对象是运行该程序所在的当前服务器。这一步得到的 pRelationship 中已经包含关系名称、主/伙伴服务器名称、模式、共享密钥等字段,后续的添加操作会直接复用这个结构。

构造待添加的作用域数组:DHCP_IP_ARRAY

随后示例构造一个 DHCP_IP_ARRAY,声明要添加 1 个作用域,并填入 172.16.0.0(AddScopeToRelationship.cpp):

pIpAddressArray = (LPDHCP_IP_ARRAY)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(DHCP_IP_ARRAY));
...
pIpAddressArray->NumElements = 1;
pIpAddressArray->Elements = (LPDHCP_IP_ADDRESS)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(DHCP_IP_ADDRESS));
...
pIpAddressArray->Elements[0] = 0xac100000; // 172.16.0.0

DHCP_IP_ARRAY 由 NumElements(元素个数)与 Elements(DHCP_IP_ADDRESS 数组)组成,要添加多个作用域时按需扩展 NumElements 与 Elements 即可。接着示例释放关系结构中原有的 pScopes,再用这个新数组替换(AddScopeToRelationship.cpp):

if (NULL != pRelationship->pScopes)
{
    if(NULL != pRelationship->pScopes->Elements)
    {
        DhcpRpcFreeMemory(pRelationship->pScopes->Elements);
    }
    DhcpRpcFreeMemory(pRelationship->pScopes);
}
pRelationship->pScopes = pIpAddressArray;

注意这里对 API 返回的 RPC 内存使用 DhcpRpcFreeMemory 释放,而对程序自身用 HeapAlloc 分配的内存则使用 HeapFree 释放——两套内存管理语义在后续"内存管理"小节还会展开。

关键步骤:先停用作用域,再添加进关系

这是整个示例最核心的工程要点:把作用域加入故障转移关系之前,必须先在两台服务器上停用(Deactivate)该作用域。

示例封装了 ChangeScopeStates 辅助函数(AddScopeToRelationship.cpp),它对数组中的每个作用域依次执行:

  1. DhcpGetSubnetInfoVQ(pServer, scopeIp, &pSubnetInfoVQ) —— 读取该作用域的当前配置信息(DHCP_SUBNET_INFO_VQ);
  2. 将 pSubnetInfoVQ->SubnetState 设置为目标状态;
  3. DhcpSetSubnetInfoVQ(pServer, scopeIp, pSubnetInfoVQ) —— 写回服务器;
  4. 释放 pSubnetInfoVQ 并继续下一个作用域。

在此基础上派生了两个语义明确的封装(AddScopeToRelationship.cpp):

DWORD DeactivateScopes(_In_ LPWSTR pServer, _Inout_ LPDHCP_IP_ARRAY pArray)
{
    return ChangeScopeStates(pServer, pArray, DhcpSubnetDisabled);
}

DWORD ActivateScopes(_In_ LPWSTR pServer, _Inout_ LPDHCP_IP_ARRAY pArray)
{
    return ChangeScopeStates(pServer, pArray, DhcpSubnetEnabled);
}

DhcpSubnetDisabled / DhcpSubnetEnabled 属于 DHCP_SUBNET_STATE 枚举。执行顺序为:先在伙伴服务器上停用,再在主服务器上停用(AddScopeToRelationship.cpp):

// Deactivate scopes on partner server
dwError = DeactivateScopes(pRelationship->SecondaryServerName, pRelationship->pScopes);
if( ERROR_SUCCESS != dwError)
{   wprintf(L"Failed to deactivate scopes on partner server\n");
    goto cleanup;
}
// Deactivate scopes on current server
dwError = DeactivateScopes(pRelationship->PrimaryServerName, pRelationship->pScopes);
if( ERROR_SUCCESS != dwError)
{   wprintf(L"Failed to deactivate scopes on primary server\n");
    goto ActivateSecAndCleanup;
}

为什么必须停用?从故障转移机制的角度可以推断:故障转移要求两台服务器上的作用域处于可同步、一致的状态;处于启用状态且正在对外提供租约的作用域直接加入关系,会导致两端状态无法对齐、租约数据不一致。因此微软官方示例将"停用 → 加入关系 → 重新启用"作为标准操作序列。

核心调用:向伙伴服务器与主服务器依次添加作用域

第一次调用:伙伴服务器

示例先调用 DhcpV4FailoverAddScopeToRelationship,目标服务器取 pRelationship->SecondaryServerName(伙伴服务器,AddScopeToRelationship.cpp):

dwError = DhcpV4FailoverAddScopeToRelationship(
                    pRelationship->SecondaryServerName,
                    pRelationship);
if( ERROR_SUCCESS != dwError)
{
    wprintf(L"DhcpV4FailoverAddScopeToRelationship failed with Error = %d for partner server\n", dwError);
    goto ActivatePriAndCleanup;
}

第二次调用:主服务器

接着以 pwszServer(当前服务器)为目标再次调用(AddScopeToRelationship.cpp):

dwError = DhcpV4FailoverAddScopeToRelationship(
                    pwszServer,
                    pRelationship);
if( ERROR_SUCCESS != dwError)
{
    wprintf(L"DhcpV4FailoverAddScopeToRelationship failed with Error = %d for primary server\n", dwError);
}

注意两点:

  • 先伙伴后主:与 CreateRelationship 示例(createrelationship.cpp)中"先伙伴后主"的创建顺序一致,这是本系列示例约定俗成的顺序;
  • 失败路径处理:若主服务器调用失败,示例不会立即中止,而是落入 ActivatePriAndCleanup 标签,先把两台服务器上的作用域重新激活,避免作用域被永久停用(AddScopeToRelationship.cpp):
ActivatePriAndCleanup:
    //Activate scopes on primary server
    ActivateScopes(pRelationship->PrimaryServerName, pRelationship->pScopes);

ActivateSecAndCleanup:
    //Activate scopes on secondary server
    ActivateScopes(pRelationship->SecondaryServerName, pRelationship->pScopes);

内存管理实践:两套释放语义

示例在内存管理上刻意区分了两类内存,这是阅读 dhcpsapi 系列示例时必须掌握的要点:

内存来源 分配方式 释放方式 示例位置
API 返回的结构(RPC 内存) 由 DhcpV4FailoverGetRelationship 等 API 内部分配 DhcpRpcFreeMemory FreeRelationshipMemory
程序自行构造的数据 HeapAlloc / HeapFree HeapFree(GetProcessHeap()) cleanup 段

FreeRelationshipMemory 依次释放 RelationshipName、SharedSecret、PrimaryServerName、SecondaryServerName 以及关系结构本体;FreeSubnetInfoVQ 则释放作用域信息中的 NetBios 名、主机名、子网名与子网注释。cleanup 段先用 HeapFree 释放自己构造的 pIpAddressArray(其 Elements 已转交给 pRelationship->pScopes,故仅释放数组外壳即可),最后调用 FreeRelationshipMemory(pRelationship)。整个示例对每一步 HeapAlloc 都做了失败检查并输出 Not enough memory 提示。

构建与运行示例

环境要求

按 README 说明:

  • 客户端:Windows 8.1;
  • 服务器:Windows Server 2012 R2;
  • 开发环境:Visual Studio Express 2013 for Windows 8.1 或更高版本 Visual Studio(多数情况下无需改动即可在后续版本运行)。

构建步骤

  1. 打开解决方案文件 AddScopeToRelationship.sln;
  2. 按 F7(Visual Studio 2013 为 F6),或从菜单 Build → Build Solution 开始构建;
  3. 输出默认生成在 \Debug 或 Release 目录。

从 AddScopeToRelationship.vcxproj 可以看出,工程提供了 Debug|x64 与 Release|x64 两种配置,Release 开启 MaxSpeed 优化、FunctionLevelLinking 与 IntrinsicFunctions,并默认将警告视为错误(TreatWarningAsError)。

运行步骤

构建完成后,按 F5(启用调试运行)或 Ctrl-F5(不启用调试运行)即可启动程序(也可从 Debug 菜单选择对应选项)。

运行前务必确认前文"既定前提"中的两个条件已满足;程序成功执行后,172.16.0.0 作用域会同时出现在主、伙伴服务器上 "test" 关系的作用域列表中。

与同仓库其他 DHCP 故障转移示例的关系

Samples/DHCP 目录下提供了完整的故障转移关系生命周期示例,可对照阅读:

  • CreateRelationship:创建关系的完整流程,展示了 DHCP_FAILOVER_RELATIONSHIP 各字段的填充方式——模式 LoadBalance/HotStandby、ServerType(PrimaryServer/SecondaryServer)、Mclt(如 0xe10=3600 秒即 1 小时)、SafePeriod(如 0x258=600 秒即 10 分钟)、Percentage(如 0x32=50%)、SharedSecret 等,是理解本示例 pScopes 来源的入口;
  • GetRelationship:读取关系,本示例的第一步即调用 DhcpV4FailoverGetRelationship;
  • SetRelationship:通过 MODE | SAFEPERIOD | MCLT | PERCENTAGE 等标志位修改关系参数;
  • EnumRelationship:以 resumeHandle 分页枚举服务器上的全部关系;
  • DeleteScopeFromRelationship:与本文流程完全镜像——同样先停用作用域,再调用 DhcpV4FailoverDeleteScopeFromRelationship 把作用域从关系中移除。

把这几个示例串起来,即可掌握"创建关系 → 添加作用域 → 修改参数 → 移除作用域 → 删除关系"的完整运维闭环。

常见问题与注意事项

  • 关系不存在:DhcpV4FailoverGetRelationship 返回非 ERROR_SUCCESS,程序直接进入 cleanup。请先用 CreateRelationship 示例或 DHCP 控制台创建名为 "test" 的关系;
  • 作用域未在两端同时存在:API 要求主、伙伴服务器上有相同的作用域,否则添加会失败。这是本示例 README 与源码注释反复强调的前提;
  • 作用域已属于其他关系:前提要求待添加作用域"不属于任何关系",如已归属需先通过 DeleteScopeFromRelationship 解绑;
  • 失败后的恢复:示例通过标签跳转保证即使添加失败,两台服务器上的作用域也会被重新激活(DhcpSubnetEnabled),不会留下停用状态的残留;
  • 内存泄漏:所有 API 返回结构必须用 DhcpRpcFreeMemory 释放,自建数组用 HeapFree 释放,混用将导致不可预期的行为。

综上,AddScopeToRelationship 示例用不到 200 行代码完整演示了 Windows DHCP Server Management API 中最具代表性的"动态扩展故障转移关系"场景。把它与同目录下的关系管理示例结合阅读,即可全面掌握 Windows Server DHCP 故障转移的编程式运维方法。

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