IPv6 Winsock 应用程序的函数调用

新函数已引入 Windows 套接字接口,专门用于简化 Windows 套接字编程。 这些新的 Windows 套接字函数的优点之一是集成了对 IPv6 和 IPv4 的支持。

这些新的 Windows 套接字函数包括以下内容:

此外,添加了支持 IPv6 和 IPv4 的新 IP 帮助程序函数,以简化 Windows 套接字编程。 这些新的 IP 帮助程序函数包括以下内容:

向应用程序添加 IPv6 支持时,应使用以下准则:

  • 使用“WSAConnectByName与给定主机名和端口的终结点建立连接。 “WSAConnectByName”函数在 Windows Vista 及更高版本上可用
  • 使用“WSAConnectByList与由一组目标地址(主机名和端口)表示的可能终结点集合中的其中一个建立连接。 “WSAConnectByList”函数在 Windows Vista 及更高版本上可用。
  • 将“gethostbyname函数调用替换为对新的“getaddrinfoWindows 套接字函数其中之一的调用。 支持 IPv6 协议的“getaddrinfo”函数在 Windows XP 及更高版本上可用。 安装适用于 Windows 2000 的 IPv6 技术预览版时,Windows 2000 也支持 IPv6 协议。
  • 将“gethostbyaddr函数调用替换为对新的“getnameinfoWindows 套接字函数其中之一的调用。 支持 IPv6 协议的“getnameinfo”函数在 Windows XP 及更高版本上可用。 安装适用于 Windows 2000 的 IPv6 技术预览版时,Windows 2000 也支持 IPv6 协议。

WSAConnectByName

WSAConnectByName函数简化了在给定目标主机名或 IP 地址(IPv4 或 IPv6)的情况下使用基于流的套接字连接到终结点的过程。 此函数减少了创建与所使用 IP 协议版本无关的 IP 应用程序所需的源代码。 “WSAConnectByName”将典型 TCP 应用程序中的以下步骤替换为单个函数调用:

  • 将主机名解析为一组 IP 地址。
  • 对于每个 IP 地址:
    • 创建相应地址系列的套接字。
    • 尝试连接到远程 IP 地址。 如果连接成功,则会返回;否则会尝试主机的下一个远程 IP 地址。

WSAConnectByName函数不仅仅是解析名称,然后尝试连接。 该函数使用名称解析返回的所有远程地址以及本地计算机的所有源 IP 地址。 它首先使用成功率最高的地址对尝试进行连接。 因此,“WSAConnectByName”不仅可确保在可能的情况下建立连接,而且还可以最大程度地缩短建立连接的时间。

要启用 IPv6 和 IPv4 通信,请使用以下方法:

  • 在调用“WSAConnectByName”之前,必须在为 AF_INET6 地址系列创建的套接字上调用“setsockopt”函数,以禁用“IPV6_V6ONLY”套接字选项。 通过以下方法可实现此操作,即在套接字上调用“setsockopt”函数,并将“level”参数设置为“IPPROTO_IPV6”(请参阅IPPROTO_IPV6 套接字选项)、将“optname”参数设置为“IPV6_V6ONLY”,并将“optvalue”参数值设置为零。

如果应用程序需要绑定到特定的本地地址或端口,则不能使用“WSAConnectByName,因为“WSAConnectByName”的套接字参数必须是未绑定的套接字。

下面的代码示例仅显示了使用此函数实现与 IP 版本无关的应用程序所需的几行代码。

使用“WSAConnectByName建立连接

#ifndef UNICODE
#define UNICODE
#endif

#define WIN32_LEAN_AND_MEAN

#include <winsock2.h>
#include <Ws2tcpip.h>
#include <stdio.h>

// Link with ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")

SOCKET OpenAndConnect(LPWSTR NodeName, LPWSTR PortName) 
{
    SOCKET ConnSocket;
    DWORD ipv6only = 0;
    int iResult;
    BOOL bSuccess;
    SOCKADDR_STORAGE LocalAddr = {0};
    SOCKADDR_STORAGE RemoteAddr = {0};
    DWORD dwLocalAddr = sizeof(LocalAddr);
    DWORD dwRemoteAddr = sizeof(RemoteAddr);
  
    ConnSocket = socket(AF_INET6, SOCK_STREAM, 0);
    if (ConnSocket == INVALID_SOCKET){
        return INVALID_SOCKET;
    }

    iResult = setsockopt(ConnSocket, IPPROTO_IPV6,
        IPV6_V6ONLY, (char*)&ipv6only, sizeof(ipv6only) );
    if (iResult == SOCKET_ERROR){
        closesocket(ConnSocket);
        return INVALID_SOCKET;       
    }

    bSuccess = WSAConnectByName(ConnSocket, NodeName, 
            PortName, &dwLocalAddr,
            (SOCKADDR*)&LocalAddr,
            &dwRemoteAddr,
            (SOCKADDR*)&RemoteAddr,
            NULL,
            NULL);
    if (bSuccess){
        return ConnSocket;
    } else {
        return INVALID_SOCKET;
    }
}

WSAConnectByList

在给定一组可能的主机(由一组目标 IP 地址和端口表示)的情况下,“WSAConnectByList函数可建立与主机的连接。 该函数可接受该终结点的所有 IP 地址与端口,以及所有本地计算机的 IP 地址,并使用所有可能的地址组合尝试建立连接。

WSAConnectByList与“WSAConnectByName函数相关。 “WSAConnectByList”不会接受单个主机名,而是接受主机列表(目标地址和端口对),并连接到所提供列表中地址和端口之一。 此函数旨在支持应用程序需要连接到可能的主机列表中任何可用主机的方案。

与“WSAConnectByName类似,“WSAConnectByList函数显著减少了创建、绑定和连接套接字所需的源代码。 此函数简化了与 IP 版本无关的应用程序实现。 此函数接受的主机的地址列表可以是 IPv6 或 IPv4 地址。

要启用 IPv6 和 IPv4 地址以在函数接受的单个地址列表中传递,必须在调用该函数之前执行以下步骤:

  • 在调用“WSAConnectByList”之前,必须在为 AF_INET6 地址系列创建的套接字上调用“setsockopt”函数,以禁用“IPV6_V6ONLY”套接字选项。 通过以下方法可实现此操作,即在套接字上调用“setsockopt”函数,并将“level”参数设置为“IPPROTO_IPV6”(请参阅 IPPROTO_IPV6 套接字选项)、将“optname”参数设置为“IPV6_V6ONLY”,并将“optvalue”参数值设置为零。
  • 任何 IPv4 地址必须以 IPv4 映射的 IPv6 地址格式表示,以支持仅使用 IPv6 的应用程序与 IPv4 节点通信。 IPv4 映射的 IPv6 地址格式支持将 IPv4 节点的 IPv4 地址表示为 IPv6 地址。 IPv4 地址可编码为低序 32 位 IPv6 地址,并且高序 96 位将保留固定前缀 0:0:0:0:0:FFFF。 IPv4 映射的 IPv6 地址格式是在 RFC 4291 中指定的。 有关详细信息,请参阅 www.ietf.org/rfc/rfc4291.txt。 “Mstcpip.h”中的 IN6ADDR_SETV4MAPPED 宏可用于将 IPv4 地址转换为所需的 IPv4 映射的 IPv6 地址格式。

使用“WSAConnectByList建立连接

#ifndef UNICODE
#define UNICODE
#endif

#define WIN32_LEAN_AND_MEAN

#include <winsock2.h>
#include <Ws2tcpip.h>
#include <stdio.h>

// Link with ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")

SOCKET OpenAndConnect(SOCKET_ADDRESS_LIST *AddressList) 
{
    SOCKET ConnSocket;
    DWORD ipv6only = 0;
    int iResult;
    BOOL bSuccess;
    SOCKADDR_STORAGE LocalAddr = {0};
    SOCKADDR_STORAGE RemoteAddr = {0};
    DWORD dwLocalAddr = sizeof(LocalAddr);
    DWORD dwRemoteAddr = sizeof(RemoteAddr);

    ConnSocket = socket(AF_INET6, SOCK_STREAM, 0);
    if (ConnSocket == INVALID_SOCKET){
        return INVALID_SOCKET;
    }

    iResult = setsockopt(ConnSocket, IPPROTO_IPV6,
        IPV6_V6ONLY, (char*)&ipv6only, sizeof(ipv6only) );
    if (iResult == SOCKET_ERROR){
        closesocket(ConnSocket);
        return INVALID_SOCKET;       
    }

    // AddressList may contain IPv6 and/or IPv4Mapped addresses
    bSuccess = WSAConnectByList(ConnSocket,
            AddressList,
            &dwLocalAddr,
            (SOCKADDR*)&LocalAddr,
            &dwRemoteAddr,
            (SOCKADDR*)&RemoteAddr,
            NULL,
            NULL);
    if (bSuccess){
        return ConnSocket;
    } else {
        return INVALID_SOCKET;
    }
}

getaddrinfo

“getaddrinfo”函数还可以执行许多函数的处理工作。 以前,要创建、打开,然后将地址绑定到套接字需要调用多个 Windows 套接字函数。 使用“getaddrinfo”函数,可以显著减少执行此类工作所需的源代码行。 以下两个示例演示了在使用和不使用“getaddrinfo”函数的情况下执行这些任务所需的源代码。

在使用“getaddrinfo”的情况下执行打开、连接和绑定

#ifndef UNICODE
#define UNICODE
#endif

#define WIN32_LEAN_AND_MEAN

#include <winsock2.h>
#include <Ws2tcpip.h>
#include <stdio.h>

// Link with ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")

SOCKET OpenAndConnect(char *ServerName, char *PortName, int SocketType)
{
    SOCKET ConnSocket;
    ADDRINFO *AI;

    if (getaddrinfo(ServerName, PortName, NULL, &AI) != 0) {
        return INVALID_SOCKET;
    }

    ConnSocket = socket(AI->ai_family, SocketType, 0);
    if (ConnSocket == INVALID_SOCKET) {
        freeaddrinfo(AI);
        return INVALID_SOCKET;
    }

    if (connect(ConnSocket, AI->ai_addr, (int) AI->ai_addrlen) == SOCKET_ERROR) {
        closesocket(ConnSocket);
        freeaddrinfo(AI);
        return INVALID_SOCKET;
    }

    return ConnSocket;
}

在不使用“getaddrinfo”的情况下执行打开、连接和绑定

#ifndef UNICODE
#define UNICODE
#endif

#define WIN32_LEAN_AND_MEAN

#include <winsock2.h>
#include <Ws2tcpip.h>
#include <stdio.h>

// Link with ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")

SOCKET OpenAndConnect(char *ServerName, unsigned short Port, int SocketType) 
{
    SOCKET ConnSocket;
    LPHOSTENT hp;
    SOCKADDR_IN ServerAddr;
    
    ConnSocket = socket(AF_INET, SocketType, 0); /* Open a socket */
    if (ConnSocket < 0 ) {
        return INVALID_SOCKET;
    }

    memset(&ServerAddr, 0, sizeof(ServerAddr));
    ServerAddr.sin_family = AF_INET;
    ServerAddr.sin_port = htons(Port);

    if (isalpha(ServerName[0])) {   /* server address is a name */
        hp = gethostbyname(ServerName);
        if (hp == NULL) {
            return INVALID_SOCKET;
        }
        ServerAddr.sin_addr.s_addr = (ULONG) hp->h_addr;
    } else { /* Convert nnn.nnn address to a usable one */
        ServerAddr.sin_addr.s_addr = inet_addr(ServerName);
    } 

    if (connect(ConnSocket, (LPSOCKADDR)&ServerAddr, 
        sizeof(ServerAddr)) == SOCKET_ERROR)
    {
        closesocket(ConnSocket);
        return INVALID_SOCKET;
    }

    return ConnSocket;
}

请注意,两个源代码示例执行相同的任务,但使用“getaddrinfo”函数的第一个示例需要的源代码行数更少,并且可以处理 IPv6 或 IPv4 地址。 使用“getaddrinfo”函数消除的源代码行数各不相同。

注意

在生产源代码中,应用程序将循环访问“gethostbyname或“getaddrinfo函数返回的地址集。 为简单起见,这些示例省略了该步骤。

 

修改现有 IPv4 应用程序以支持 IPv6 时必须解决的另一个问题与调用函数的顺序相关联。 “getaddrinfo和“gethostbyname都要求按特定顺序进行系列函数调用。

在同时使用 IPv4 和 IPv6 的平台上,远程主机名的地址系列事先是未知的。 因此,必须先使用“getaddrinfo函数执行地址解析,以确定远程主机的 IP 地址和地址系列。 然后,可以调用“socket”数以打开“getaddrinfo”返回的地址系列的套接字。 这是 Windows 套接字应用程序写入方式的重要更改,因为许多 IPv4 应用程序倾向于使用不同的函数调用顺序。

大多数 IPv4 应用程序会首先为 AF_INET 地址系列创建套接字,再执行名称解析,然后使用套接字连接到解析的 IP 地址。 在使这样的应用支持 IPv6 时,对“socket函数的调用必须延迟到已确定地址系列并解析名称之后。 请注意,如果名称解析同时返回 IPv4 和 IPv6 地址,则必须单独使用 IPv4 和 IPv6 套接字连接到这些目标地址。 在 Windows Vista 及更高版本中使用“WSAConnectByName函数可以避免所有这些复杂情况,因此鼓励应用程序开发人员使用此新函数。

下面的代码示例演示了首先执行名称解析(在以下源代码示例中的第四行执行),然后打开套接字(在以下代码示例中的第 19th 行中执行)的正确顺序。 此示例是附录 B 已启用 IPv6 的客户端代码中 Client.c 文件的摘录。以下代码示例中调用的 PrintError 函数列出在 Client.c 示例中。

#ifndef UNICODE
#define UNICODE
#endif

#define WIN32_LEAN_AND_MEAN

#include <winsock2.h>
#include <Ws2tcpip.h>
#include <stdio.h>

// Link with ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")

SOCKET OpenAndConnect(char *Server, char *PortName, int Family, int SocketType)
{

    int iResult = 0;
    SOCKET ConnSocket = INVALID_SOCKET;

    ADDRINFO *AddrInfo = NULL;
    ADDRINFO *AI = NULL;
    ADDRINFO Hints;

    char *AddrName = NULL;

    memset(&Hints, 0, sizeof (Hints));
    Hints.ai_family = Family;
    Hints.ai_socktype = SocketType;

    iResult = getaddrinfo(Server, PortName, &Hints, &AddrInfo);
    if (iResult != 0) {
        printf("Cannot resolve address [%s] and port [%s], error %d: %s\n",
               Server, PortName, WSAGetLastError(), gai_strerror(iResult));
        return INVALID_SOCKET;
    }
    //
    // Try each address getaddrinfo returned, until we find one to which
    // we can successfully connect.
    //
    for (AI = AddrInfo; AI != NULL; AI = AI->ai_next) {

        // Open a socket with the correct address family for this address.
        ConnSocket = socket(AI->ai_family, AI->ai_socktype, AI->ai_protocol);
        if (ConnSocket == INVALID_SOCKET) {
            printf("Error Opening socket, error %d\n", WSAGetLastError());
            continue;
        }
        //
        // Notice that nothing in this code is specific to whether we 
        // are using UDP or TCP.
        //
        // When connect() is called on a datagram socket, it does not 
        // actually establish the connection as a stream (TCP) socket
        // would. Instead, TCP/IP establishes the remote half of the
        // (LocalIPAddress, LocalPort, RemoteIP, RemotePort) mapping.
        // This enables us to use send() and recv() on datagram sockets,
        // instead of recvfrom() and sendto().
        //

        printf("Attempting to connect to: %s\n", Server ? Server : "localhost");
        if (connect(ConnSocket, AI->ai_addr, (int) AI->ai_addrlen) != SOCKET_ERROR)
            break;

        if (getnameinfo(AI->ai_addr, (socklen_t) AI->ai_addrlen, AddrName,
                        sizeof (AddrName), NULL, 0, NI_NUMERICHOST) != 0) {
            strcpy_s(AddrName, sizeof (AddrName), "<unknown>");
            printf("connect() to %s failed with error %d\n", AddrName, WSAGetLastError());
            closesocket(ConnSocket);
            ConnSocket = INVALID_SOCKET;
        }    
    }
    return ConnSocket;
}

IP Helper 函数

最后,使用 IP 帮助程序函数“GetAdaptersInfo的应用程序及其关联结构“IP_ADAPTER_INFO必须确认此函数和结构仅限于 IPv4 地址。 此函数和结构的已启用 IPv6 的替换项为“GetAdaptersAddresses函数和“IP_ADAPTER_ADDRESSES”结构。 使用 IP 帮助程序 API 的已启用 IPv6 的应用程序应使用“GetAdaptersAddresses”函数和相应的启用了 IPv6 的“IP_ADAPTER_ADDRESSES”结构,两者都是 Microsoft Windows 软件开发工具包 (SDK) 中定义的。

建议

确保应用程序使用 IPv6 兼容函数调用的最佳方法是使用“getaddrinfo函数来获取主机到地址转换。 从 Windows XP 开始,“getaddrinfo”函数已使“gethostbyname”函数不再是必须使用的函数,因此应用程序应改用“getaddrinfo”函数,以适应将来的编程项目。 虽然 Microsoft 将继续支持“gethostbyname”,但不会扩展此函数以支持 IPv6。 为了实现对获取 IPv6 和 IPv4 主机信息的透明支持,必须使用“getaddrinfo”

以下示例演示了如何以最佳方式使用“getaddrinfo”函数。 请注意,该函数在按此示例所示正确使用时,不仅可以正确处理 IPv6 和 IPv4 主机到地址转换,而且可以获取有关主机的其他有用信息,例如支持的套接字类型。 此示例摘录自附录 B 中的 Client.c 示例。

#ifndef UNICODE
#define UNICODE
#endif

#define WIN32_LEAN_AND_MEAN

#include <winsock2.h>
#include <Ws2tcpip.h>
#include <stdio.h>

// Link with ws2_32.lib
#pragma comment(lib, "Ws2_32.lib")

int ResolveName(char *Server, char *PortName, int Family, int SocketType)
{

    int iResult = 0;

    ADDRINFO *AddrInfo = NULL;
    ADDRINFO *AI = NULL;
    ADDRINFO Hints;

   //
    // By not setting the AI_PASSIVE flag in the hints to getaddrinfo, we're
    // indicating that we intend to use the resulting address(es) to connect
    // to a service.  This means that when the Server parameter is NULL,
    // getaddrinfo will return one entry per allowed protocol family
    // containing the loopback address for that family.
    //
    
    memset(&Hints, 0, sizeof(Hints));
    Hints.ai_family = Family;
    Hints.ai_socktype = SocketType;
    iResult = getaddrinfo(Server, PortName, &Hints, &AddrInfo);
    if (iResult != 0) {
        printf("Cannot resolve address [%s] and port [%s], error %d: %s\n",
               Server, PortName, WSAGetLastError(), gai_strerror(iResult));
        return SOCKET_ERROR;
    }
     return 0;
}

注意

支持 IPv6 的“getaddrinfo函数的版本是 Windows XP 版本 Windows 的新增功能。

 

要避免的代码

传统上,主机地址转换是使用“gethostbyname函数实现的。 从 Windows XP 开始:

  • “getaddrinfo”函数淘汰了“gethostbyname”函数。
  • 应用程序应使用“getaddrinfo”函数,而不是“gethostbyname”函数

编码任务

修改现有 IPv4 应用程序以添加对 IPv6 的支持

  1. 获取 Checkv4.exe 实用工具。 此实用工具是作为 Windows SDK 的一部分安装的。 Windows SDK 通过 MSDN 订阅提供,也可以从 Microsoft 网站(https://msdn.microsoft.com)下载。 “Checkv4.exe”工具的早期版本也包含在适用于 Windows 2000 的 Microsoft IPv6 技术预览版中。
  2. 针对代码运行“Checkv4.exe”实用工具。 请参阅使用 Checkv4.exe 实用工具,以了解如何针对文件运行该版本实用工具。
  3. 该实用工具会提醒你使用“gethostbyname、“gethostbyaddr,以及其他仅限 IPv4 的函数,并提供有关如何将它们替换为 IPv6 兼容函数(如“getaddrinfo和“getnameinfo)的建议。
  4. 将“gethostbyname”函数的任何实例以及相应的关联代码替换为“getaddrinfo”函数。 在 Windows Vista 上,请在适当情况下使用“WSAConnectByName或“WSAConnectByList函数。
  5. 将“gethostbyaddr函数的任何实例以及相应的关联代码替换为“getnameinfo”函数

或者,可以搜索代码库以获取“gethostbyname”和“gethostbyaddr函数的实例,并将所有这些用法(和相应的其他关联代码)更改为“getaddrinfo”和“getnameinfo函数。

Windows 套接字应用程序的 IPv6 指南

更改 IPv6 Winsock 应用程序的数据结构

IPv6 Winsock 应用程序的双堆栈套接字

使用硬编码的 IPv4 地址

IPv6 Winsock 应用程序的用户界面问题

面向 IPv6 Winsock 应用程序的基础协议