IPv6 Winsock 應用程式的函數調用

新的函式已引入 Windows Sockets 介面,專為讓 Windows Sockets 程式設計更容易而設計。 這些新 Windows 套接字函式的優點之一是整合支援 IPv6 和 IPv4。

這些新的 Windows Sockets 函式包括下列各項:

此外,已新增支援 IPv6 和 IPv4 的新 IP 協助程式函式,以簡化 Windows 套接字程式設計。 這些新的IP協助程式函式包括下列各項:

將 IPv6 支援新增至應用程式時,應該使用下列指導方針:

  • 使用 WSAConnectByName 建立與指定主機名和埠之端點的連線。 WSAConnectByName 函式可在 Windows Vista 和更新版本上使用。
  • 使用 WSAConnectByList 建立與一組目的地位址(主機名和埠)所代表之可能端點集合的連線。 WSAConnectByList 函式可在 Windows Vista 和更新版本上使用。
  • 將 gethostbyname 函式呼叫取代為其中一個新的 getaddrinfo Windows Sockets 函式的呼叫。 支援 IPv6 通訊協定的 getaddrinfo 函式可在 Windows XP 和更新版本上使用。 安裝適用於 Windows 2000 的 IPv6 技術預覽版時,Windows 2000 也支援 IPv6 通訊協定。
  • 將 gethostbyaddr 函式呼叫取代為對其中一個新的 getnameinfo Windows Sockets 函式的呼叫。 支援 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 通訊,請使用下列方法:

  • setockopt 函式必須在為AF_INET6位址系列建立的套接字上呼叫,才能在呼叫 WSAConnectByName 之前停用IPV6_V6ONLY套接字選項。 這可藉由呼叫套接字上的 setsockopt 函式,並將 level 參數設定為 IPPROTO_IPV6 (請參閱 IPPROTO_IPV6 Socket Options)、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

WSAConnectByList 函式會建立與一組可能主機的連線(由一組目的地 IP 位址和埠表示)。 函式會採用端點和所有本機計算機 IP 位址的所有 IP 位址和埠,並使用所有可能的位址組合嘗試連線。

WSAConnectByList 與 WSAConnectByName 函式相關。 WSAConnectByList 不會採用單一主機名,而是接受主機清單(目的地位址和埠組),並連線到所提供清單中的其中一個位址和埠。 此函式的設計訴求是支援應用程式需要從潛在主機清單中連線到任何可用主機的案例。

與 WSAConnectByName 類似,WSAConnectByList 函式可大幅減少建立、系結及連接套接字所需的原始程式碼。 此函式可讓您更輕鬆地實作與IP版本無關的應用程式。 此函式所接受主機的位址清單可能是IPv6或IPv4位址。

若要讓 IPv6 和 IPv4 位址都傳入函式所接受的單一位址清單中,必須先執行下列步驟,才能呼叫 函式:

  • setockopt 函式必須在為AF_INET6位址系列建立的套接字上呼叫,才能在呼叫 WSAConnectByList 之前停用IPV6_V6ONLY套接字選項。 這可藉由呼叫套接字上的 setsockopt 函式,並將 level 參數設定為 IPPROTO_IPV6 (請參閱 IPPROTO_IPV6 Socket Options)、optname 參數設定為 IPV6_V6ONLY,並將 optvalue 參數值設定為零。
  • 任何 IPv4 位址都必須以 IPv4 對應 IPv6 位址格式來表示,這可讓只有 IPv6 的應用程式與 IPv4 節點通訊。 IPv4 對應 IPv6 位址格式可讓 IPv4 節點的 IPv4 位址表示為 IPv6 位址。 IPv4 位址會編碼為 IPv6 位址的低序 32 位,而高階 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 Sockets 函式,才能建立、開啟,然後將位址系結至套接字。 使用 getaddrinfo 函式時,執行這類工作所需的原始程式碼行可能會大幅減少。 下列兩個範例說明使用 和 不使用 getaddrinfo 函式來執行這些工作所需的原始程式碼。

使用 getaddrinfo 執行 Open、Connect 和 Bind

#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;
}

執行 Open、Connect 和 Bind 而不使用 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 位址和位址系列。 然後,您可以呼叫套接字函式,以開啟 getaddrinfo 所傳回之位址系列的套接字。 這是 Windows Sockets 應用程式撰寫方式的重要變更,因為許多 IPv4 應用程式通常會使用不同的函數調用順序。

大部分的 IPv4 應用程式會先為AF_INET位址系列建立套接字,然後執行名稱解析,然後使用套接字聯機到已解析的 IP 位址。 啟用這類應用程式 IPv6 時, 必須延遲套接字 函式呼叫,直到確定位址系列時,名稱解析之後才會延遲。 請注意,如果名稱解析同時傳回 IPv4 和 IPv6 位址,則必須使用個別的 IPv4 和 IPv6 套接字來連線到這些目的地位址。 您可以使用 Windows Vista 和更新版本的 WSAConnectByName 函式來避免所有這些複雜度,因此鼓勵應用程式開發人員使用此新函式。

下列程式代碼範例顯示第一次執行名稱解析的適當順序(在下列原始程式碼範例的第四行中執行),然後開啟套接字(在下列程式代碼範例的第 19 行中執行)。 此範例是附錄 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 協助程式函式

最後,使用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 的一部分。 舊版 的 Checkv4.exe 工具也隨附於 Windows 2000 Microsoft IPv6 Technical Preview 的一部分。
  2. 針對您的程式代碼執行Checkv4.exe公用程式。 請參閱 使用 Checkv4.exe 公用程式 來瞭解如何對檔案執行版本公用程式。
  3. 公用程式會警示您使用 gethostbyname、gethostbyaddr 和其他僅限 IPv4 的函式,並提供如何以 IPv6 兼容函式取代它們的建議,例如 getaddrinfo 和 getnameinfo。
  4. 使用 getaddrinfo 函式取代 gethostbyname 函式的任何實例,並適當地取代相關聯的程式代碼。 在 Windows Vista 上,適當時使用 WSAConnectByName 或 WSAConnectByList 函式。
  5. 使用 getnameinfo 函式取代 gethostbyaddr 函式的任何實例,並適當地取代相關聯的程式代碼。

或者,您可以搜尋您的程式代碼基底以取得 gethostbyname 和 gethostbyaddr 函式的實例,並將所有這些用法(以及適當的其他相關聯程式代碼)變更為 getaddrinfo 和 getnameinfo 函式。

Windows 套接字應用程式的 IPv6 指南

變更 IPv6 Winsock 應用程式的數據結構

IPv6 Winsock 應用程式的雙堆疊套接字

使用硬式編碼的IPv4位址

IPv6 Winsock 應用程式的使用者介面問題

IPv6 Winsock 應用程式的基礎通訊協定