Winsock 协议与名称空间 Provider 目录

Winsock 协议与名称空间 Provider 目录

文章所述功能来自KswordARK,项目开源地址 https://github.com/KSwordDEV/KSword

要取得当前进程可见的 Winsock 目录,分为四步

本篇要得到当前进程可见的 Winsock 协议目录和名称空间目录,并读出每个条目的身份、类型和活动状态。这个过程只读取目录,不安装、删除或调整提供程序。要完成这项读取,分为四步:

  1. 认识目录中的对象,并初始化 Winsock
  2. 测量缓冲区并读取 Protocol Catalog
  3. 测量缓冲区并读取 Name Space Catalog
  4. 解读条目、记录查询范围,并归还 Winsock 引用

Windows Sockets(Winsock)是 Windows 程序访问网络的公共接口。应用程序调用 socketconnectsendrecv 或名称查询函数时,调用不会直接面对网卡驱动。Winsock 根据目录中的提供程序信息选择实现,再由协议栈、网络设备和远端系统完成通信。目录是系统网络配置的一部分,读取目录能够说明当前进程看见哪些提供程序,不能单独证明某个程序已经使用某一项。

Winsock 有两类目录。协议目录(Protocol Catalog)回答"某种地址族、套接字类型和协议如何收发数据"。名称空间目录(Name Space Catalog)回答"某类名称如何解析为地址或服务"。两类条目都带 GUID 和显示名,却处在不同调用链。排查网络异常时先分清这两个角色,再检查条目的位数、目录 ID 与返回错误。

创建套接字:应用参数 -> Protocol Catalog -> 协议 Provider -> 网络栈

名称查询:查询参数 -> Name Space Catalog -> 名称空间 Provider -> 地址结果

只读检查:WSAStartup -> 测量所需字节数 -> 分配缓冲区 -> 枚举条目 -> WSAcCleanup

一、第一步:认识目录中的对象,并初始化 Winsock

协议目录条目用 WSAPROTOCOL_INFOW 描述。条目中的 iAddressFamily 表示地址族,例如 AF_INETAF_INET6iSocketType 表示 SOCK_STREAMSOCK_DGRAM 等套接字语义。iProtocolIPPROTO_TCPIPPROTO_UDP 等协议号。GUID(Globally Unique Identifier,全局唯一标识符)是 Windows 用 128 位值标识对象的格式,ProviderId 用它标识提供程序。dwCatalogEntryId 是当前目录中的条目身份。链式提供程序还会由多个目录项共同组成。

名称空间目录条目用 WSANAMESPACE_INFOW 描述。dwNameSpace 表示名称空间类别,例如 NS_DNS 代表 DNS,NS_NLA 与网络位置感知有关。NSProviderId 表示提供程序身份,fActive 表示条目当前活动状态,lpszIdentifier 是显示文本。名称空间条目参与主机名、服务名或服务发现查询,它不承担 TCP 字节发送。

调用 socket:地址族、套接字类型和协议 -> Protocol Catalog -> 协议 Provider -> 连接、收发和套接字选项

调用 getaddrinfo:名称查询 -> Name Space Catalog -> DNS、NLA 或其它 Provider -> 候选地址

同名条目不能仅凭显示文本合并。目录 ID 与 GUID 才能帮助识别条目。地址族、进程位数和目录种类决定其语义。一个网络程序可以只使用协议提供程序,也可以先经过名称解析后再创建套接字。

初始化是当前进程的前置条件

在调用大多数 Winsock API 前,当前进程要先调用 WSAStartup。函数的返回值为 0 表示成功,非零值直接就是 Winsock 错误码。成功调用一次,就要在同一进程中调用一次 WSACleanup 归还初始化计数。WSADATA 由调用方提供存储空间,函数在其中写入协商后的版本和实现信息。

cpp 复制代码
// 作用:初始化当前进程的 Winsock 使用计数并协商 API 版本。
// 返回:0 成功。非零值是 Winsock 错误码,不依赖 WSAGetLastError。
int WSAStartup(
    WORD wVersionRequired, // 请求版本,例如 MAKEWORD(2, 2)
    LPWSADATA lpWSAData    // 输出:版本、描述和实现信息。调用方拥有此结构
);

// 作用:与一次成功的 WSAStartup 配对,减少当前进程的初始化计数。
// 返回:0 成功。SOCKET_ERROR 失败,失败时可调用 WSAGetLastError。
int WSACleanup(void);

WSADATA data{};
int result = WSAStartup(MAKEWORD(2, 2), &data);
if (result == 0) {
    // 在这里读取目录。完成后必须调用 WSACleanup()。
}

初始化只影响当前进程,不会修复、安装或重排系统目录。WSASYSNOTREADY 通常表示网络子系统未准备好。版本协商失败说明请求版本不被实现支持。访问限制也应和"目录为空"分开记录。

完成第一步后,当前进程已经取得可用的 Winsock 引用,也能区分协议目录和名称空间目录的用途。接下来读取 Protocol Catalog,才能把"支持哪些网络通信组合"变成可核对的条目。

二、第二步:测量缓冲区并读取 Protocol Catalog

WSCEnumProtocols 读取协议目录。该 SPI 函数的结构类型固定为 WSAPROTOCOL_INFOW,因此函数名称本身没有 W 后缀。首次调用将 lpBuffer 设为 nullptr。典型结果是 SOCKET_ERROR,同时 lpErrno 写入 WSAENOBUFSlpdwBufferLength 写入所需容量。容量的单位是字节。分配该容量后重复调用,函数返回的非负数才是有效 WSAPROTOCOL_INFOW 条目数。

cpp 复制代码
// 作用:枚举当前进程可见的协议目录条目。
// 返回:非负数为条目数。SOCKET_ERROR 失败,lpErrno 接收扩展错误。
INT WSAAPI WSCEnumProtocols(
    LPINT lpiProtocols,              // 可选协议号过滤数组。nullptr 表示全部
    LPWSAPROTOCOL_INFOW lpBuffer,    // 输出:连续条目缓冲区。首次测量可为 nullptr
    LPDWORD lpdwBufferLength,        // 输入容量、输出所需或实际容量,单位为字节
    LPINT lpErrno                     // 输出:WSAENOBUFS 等扩展错误码
);

DWORD bytes = 0;
int error = 0;
int count = WSCEnumProtocols(nullptr, nullptr, &bytes, &error);
if (count == SOCKET_ERROR && error == WSAENOBUFS && bytes != 0) {
    std::vector<BYTE> raw(bytes);
    DWORD capacity = bytes;
    count = WSCEnumProtocols(nullptr,
        reinterpret_cast<LPWSAPROTOCOL_INFOW>(raw.data()),
        &capacity, &error);
    // count 才是元素数量,capacity 仍然是字节数。
}

目录可能在两次调用间被安装程序或网络维护操作改写。第二次仍返回 WSAENOBUFS 时,应采用有限次数的重新分配和重试。容量没有增长、错误码为非 WSAENOBUFS 值、或超出合理上限时停止。把 bytes / sizeof(WSAPROTOCOL_INFOW) 当成条目数是错误的,因为 bytes 是容量,返回结构中也可能带有不同版本字段。

错误示例:

cpp 复制代码
DWORD bytes = 4096;
std::vector<BYTE> raw(bytes);
int assumed = static_cast<int>(bytes / sizeof(WSAPROTOCOL_INFOW));
// 错误:assumed 只是可能容纳的结构数量,并非 API 实际写入的条目数。

枚举结果应保存条目数、目录 ID、Provider GUID、地址族、套接字类型、协议号、链长度和显示名。iProtocol 是数字协议标识,不能当作 DLL 文件路径。Provider GUID 是身份标识,DLL 定位属于更深一层安装信息。

完成第二步后,已经得到当前进程可用于建连和收发数据的协议条目。名称解析仍由另一套目录负责,接下来读取 Name Space Catalog,才能知道名称会交给哪些解析实现处理。

三、第三步:测量缓冲区并读取 Name Space Catalog

WSCEnumNameSpaceProvidersW 提供当前进程可见的名称空间条目。它的 lpdwBufferLength 同样以字节计量,成功返回条目数量,失败返回 SOCKET_ERROR,错误通过 WSAGetLastError 取得。首次测量后再调用时也可能遇到目录变化。

cpp 复制代码
// 作用:枚举名称空间 Provider 目录。
// 返回:非负数为条目数。SOCKET_ERROR 失败,使用 WSAGetLastError 获取错误。
INT WSAAPI WSCEnumNameSpaceProvidersW(
    LPDWORD lpdwBufferLength,       // 输入容量、输出所需容量,单位为字节
    LPWSANAMESPACE_INFOW lpnspBuffer // 输出:连续条目。测量阶段可为 nullptr
);

DWORD bytes = 0;
int count = WSCEnumNameSpaceProvidersW(&bytes, nullptr);
if (count == SOCKET_ERROR && bytes != 0) {
    std::vector<BYTE> raw(bytes);
    DWORD capacity = bytes;
    count = WSCEnumNameSpaceProvidersW(&capacity,
        reinterpret_cast<LPWSANAMESPACE_INFOW>(raw.data()));
}

lpszIdentifier 可能为空,未知 dwNameSpace 数值也可能在新系统或第三方提供程序上出现。读取工具应保留原始数字和 GUID,而非因显示文本为空自动删除条目。名称空间不存在、条目不活动、缓冲区不足、网络子系统未准备好和访问遭拒是不同的诊断结论。

完成第三步后,两个目录都已读入内存。接下来要结合进程位数、条目字段和错误状态解释这些记录,再在读取结束时归还 Winsock 初始化引用。

四、第四步:解读条目、记录查询范围,并归还 Winsock 引用

32 位与 64 位目录视图

Winsock 目录按进程位数区分。64 位进程查询到的目录可与 32 位进程不同。安装器、兼容层和提供程序架构都会影响可见条目。记录结果时至少包含操作系统架构、查询进程位数、Protocol Catalog 或 Name Space Catalog 类别、目录 ID、Provider GUID 和查询时的错误码。

目录安装、删除和顺序调整影响系统范围内的网络程序。遇到异常时先进行只读枚举并保留快照,再使用受支持的网络维护接口或供应商安装程序处理。直接删除注册表字符串可能留下引用、顺序和跨位数目录不一致的问题。

从进程初始化到目录记录的完整流程

这一流程的输入是当前进程请求的 Winsock 版本,输出是该进程可见的协议目录和名称空间目录快照。Winsock DLL 维护每个进程的初始化计数。WSAStartup 成功后创建一次可用引用,WSACleanup 归还一次引用。WSADATA 是调用方在栈或其他可用存储中分配的结构,Winsock 在初始化成功时写入协商版本。目录缓冲区由调用方分配,API 只在调用期间借用它,缓冲区销毁后其中的结构指针随之失效。

text 复制代码
请求 Winsock 2.2 -> WSAStartup 写入 WSADATA
        ->
空缓冲区测量 Protocol Catalog 的字节数
        ->
分配字节缓冲区 -> 重复查询 -> 用返回条目数遍历
        ->
空缓冲区测量 Name Space Catalog -> 同样读取
        ->
记录目录 ID、GUID、类型、进程位数和错误状态
        ->
WSACleanup 归还初始化引用

WSAGetLastError 只适用于返回 SOCKET_ERROR 且文档要求通过 Winsock 错误槽取得原因的 API。WSCEnumProtocols 把扩展错误写入其 lpErrno 参数,读取该输出参数。名称空间枚举没有该参数,才在失败后立即调用 WSAGetLastError。错误码要在日志格式化或 GUID 转换前保存,因为后续 API 可能覆盖线程错误状态。

cpp 复制代码
// 作用:返回当前线程最近一次失败的 Winsock API 错误码。
// 返回:WSA* 错误整数。没有参数、没有资源所有权和长度单位。
int WSAGetLastError(void);

下面的正确示范完整枚举协议目录。它先处理"首次调用预期容量不足"的状态,再处理第二次调用期间目录变动导致的重复不足。count 是可访问结构数,capacity 是字节数,二者不能混用。

cpp 复制代码
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <winsock2.h>
#include <ws2spi.h>
#include <vector>

int EnumerateProtocolCatalog() {
    WSADATA startupData{};
    int startup = WSAStartup(MAKEWORD(2, 2), &startupData);
    if (startup != 0) return startup; // 返回值直接是错误码。尚未产生清理责任

    DWORD requiredBytes = 0;
    int catalogError = 0;
    int count = WSCEnumProtocols(
        nullptr,          // 输入:nullptr 表示不按协议号过滤
        nullptr,          // 输出:测量阶段没有接收缓冲区
        &requiredBytes,   // 输出:需要的容量,单位为字节
        &catalogError);   // 输出:函数失败时写入 WSAENOBUFS 等错误
    if (count != SOCKET_ERROR || catalogError != WSAENOBUFS || requiredBytes == 0) {
        WSACleanup();
        return catalogError;
    }

    for (int retry = 0; retry != 3; ++retry) {
        std::vector<BYTE> bytes(requiredBytes); // vector 拥有缓冲区并在循环最后释放
        DWORD capacity = requiredBytes;
        count = WSCEnumProtocols(nullptr,
            reinterpret_cast<LPWSAPROTOCOL_INFOW>(bytes.data()), // 输出:连续结构起始地址
            &capacity, &catalogError);
        if (count != SOCKET_ERROR) {
            const auto* entries = reinterpret_cast<const WSAPROTOCOL_INFOW*>(bytes.data());
            for (int index = 0; index < count; ++index) {
                const WSAPROTOCOL_INFOW& item = entries[index];
                // item 只在 bytes 存活期间可读。这里记录 item.dwCatalogEntryId 和 item.ProviderId。
            }
            WSACleanup();
            return 0;
        }
        if (catalogError != WSAENOBUFS || capacity <= requiredBytes) break;
        requiredBytes = capacity; // 目录在两次调用间增大,使用新字节容量重试
    }
    WSACleanup();
    return catalogError;
}

名称空间目录的标准流程相同,只是错误传递方式不同。lpnspBuffer 接收 WSANAMESPACE_INFOW 数组,条目中的 lpszIdentifier 是 API 管理的指针,仅在调用方提供的缓冲区仍然存活时使用。保存结果时复制文本和 GUID。

cpp 复制代码
DWORD bytes = 0;
int count = WSCEnumNameSpaceProvidersW(
    &bytes,    // 输出:测量阶段返回所需容量,单位为字节,不能为 nullptr
    nullptr);  // 输出:测量阶段不接收结构
if (count == SOCKET_ERROR && bytes != 0) {
    const int measureError = WSAGetLastError(); // 立即读取失败原因
    std::vector<BYTE> raw(bytes);
    DWORD capacity = bytes;
    count = WSCEnumNameSpaceProvidersW(
        &capacity, // 输入可用字节数,输出实际/所需字节数
        reinterpret_cast<LPWSANAMESPACE_INFOW>(raw.data()));
    if (count == SOCKET_ERROR) {
        const int readError = WSAGetLastError();
        // readError == WSAENOBUFS 时可在有限次数内扩容并重试。
    }
}

错误做法是在没有调用 WSAStartup 时直接读取目录,或在 WSAStartup 成功后遗漏 WSACleanup。前者可能得到 WSASYSNOTREADY 或其他初始化失败状态,后者使进程内引用计数不能按预期归还。另一个错误是保留 WSAPROTOCOL_INFOW*lpszIdentifier 指针到 std::vector<BYTE> 离开作用域之后。那时底层缓冲区已释放,指针不再有效。

完整可运行程序在附件

https://wangweicm.lanzouu.com/icGmw3yz8e5a

相关推荐
Bobolink_2 小时前
跨境业务的网络稳定性,拆开看是三个不同的问题
网络·抖动·延迟·丢包·网络稳定性
拳里剑气3 小时前
C++算法:BFS解决FloodFill算法
c++·算法·bfs·宽度优先
爱写代码的小朋友3 小时前
从零开始学 Win32 API:C++ 窗口编程实战(VS Code + MinGW-w64 命令行详解)
开发语言·c++
yangshicong3 小时前
第19章:AI安全防护与AI安全
人工智能·python·安全·prompt·ai编程
果汁华3 小时前
Function Calling 与 Python 实战完整指南
开发语言·网络·python
Web4Browser3 小时前
指纹浏览器 API 自动化怎么接:启动 Profile、获取 CDP 端点并连接自动化框架
前端·网络·typescript·自动化
小小晓.3 小时前
C++:语句和作用域
开发语言·c++
fqbqrr3 小时前
2607C++,soui,xplayer视频播放器
c++·soui
zh路西法5 小时前
【10天速通ROS2-PX4无人机】(四) 关掉GPS和气压计,纯激光定位还能飞吗
c++·无人机·px4·ros2·卡尔曼滤波·fastlio2