1. 程式人生 > >VC++ IPv6的支持

VC++ IPv6的支持

大小 cat eat 還需 const erro bin sap 不知道

最近根據項目需要,要在產品中添加對IpV6的支持,因此研究了一下IPV6的相關內容,Ipv6 與原來最直觀的改變就是地址結構的改變,IP地址由原來的32位擴展為128,這樣原來的地址結構肯定就不夠用了,根據微軟的官方文檔,只需要對原來的代碼做稍許改變就可以適應ipv6。

修改地址結構

Windows Socket2 針對Ipv6的官方描述

根據微軟官方的說法,要做到支持Ipv6首先要做的就是將原來的SOCKADDR_IN等地址結構替換為SOCKADDR_STORAGE 該結構的定義如下:

typedef struct sockaddr_storage {
    short ss_family;  
    char __ss_pad1[_SS_PAD1SIZE];  
    __int64 __ss_align;  
    char __ss_pad2[_SS_PAD2SIZE];
} SOCKADDR_STORAGE,  *PSOCKADDR_STORAGE;
  • ss_family:代表的是地址家族,IP協議一般是AF_INET, 但是如果是IPV6的地址這個參數需要設置為 AF_INET6

後面的成員都是作為保留字段,或者說作為填充結構大小的字段,這個結構兼容了IPV6與IPV4的地址結構,跟以前的SOCKADDR_IN結構不同,我們現在不能直接從SOCKADDR_STORAGE結構中獲取IP地址了。也沒有辦法直接往結構中填寫IP地址。

使用兼容函數

除了地址結構的改變,還需要改變某些函數,有的函數是只支持Ipv4的,我們需要將這些函數改為即兼容的函數,根據官方的介紹,這些兼容函數主要是下面幾個:

  • WSAConnectByName : 可以直接通過主機名建立一個連接
  • WSAConnectByList: 從一組主機名中建立一個連接
  • getaddrinfo: 類似於gethostbyname, 但是gethostbyname只支持IPV4所以一般用這個函數來代替
  • GetAdaptersAddresses: 這個函數用來代替原來的GetAdaptersInfo

WSAConnectByName函數:

函數原型如下:

BOOL PASCAL WSAConnectByName(
  __in     SOCKET s,
  __in     LPSTR nodename,
  __in     LPSTR servicename,
  __inout  LPDWORD LocalAddressLength,
  __out    LPSOCKADDR LocalAddress,
  __inout  LPDWORD RemoteAddressLength,
  __out    LPSOCKADDR RemoteAddress,
  __in     const struct timeval* timeout,
           LPWSAOVERLAPPED Reserved
);
  • s: 該參數為一個新創建的未綁定,未與其他主機建立連接的SOCKET,後續會采用這個socket來進行收發包的操作
  • nodename: 主機名,或者主機的IP地址的字符串
  • servicename: 服務名稱,也可以是對應的端口號的字符串,傳入服務名時需要傳入那些知名的服務,比如HTTP、FTP等等, 其實這個字段本身就是需要傳入端口的,傳入服務名,最後函數會根據服務名稱轉化為這些服務的默認端口
  • LocalAddressLength, LocalAddress, 返回當前地址結構,與長度
  • RemoteAddressLength, RemoteAddress,返回遠程主機的地址結構,與長度
  • timeout: 超時值
  • Reserved: 重疊IO結構

為了使函數能夠支持Ipv6,需要在調用前使用setsockopt函數對socket做相關設置,設置的代碼如下:

iResult = setsockopt(ConnSocket, IPPROTO_IPV6, IPV6_V6ONLY, (char*)&ipv6only, sizeof(ipv6only) );

調用函數的例子如下(該實例為微軟官方的例子):

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

該函數從傳入的一組hostname中選取一個建立連接,函數內部會調用WSAConnectByName,它的原型,使用方式與WSAConnectByName類似,這裏就不再給出具體的原型以及調用方法了。

getaddrinfo

該函數的作用與gethostbyname類似,但是它可以同時支持獲取V4、V6的地址結構,函數原型如下:

int getaddrinfo(
  const char FAR* nodename,
  const char FAR* servname,
  const struct addrinfo FAR* hints,
  struct addrinfo FAR* FAR* res
);
  • nodename: 主機名或者IP地址的字符串
  • servname: 知名服務的名稱或者端口的字符串
  • hints:一個地址結構,該結構規定了應該如何進行地址轉化。
  • res:與gethostbyname類似,它也是返回一個地址結構的鏈表。後續只需要遍歷這個鏈表即可。

使用的實例如下:

char szServer[] = "www.baidu.com";
char szPort[] = "80";
addrinfo hints = {0};
struct addrinfo* ai = NULL;
getaddrinfo(szServer, szPort, NULL, &ai);
while (NULL != ai)
{
  SOCKET sConnect = socket(ai->ai_family, SOCK_STREAM, ai->ai_protocol);
  connect(sConnect, ai->ai_addr, ai->ai_addrlen);
  shutdown(sConnect, SD_BOTH);
  closesocket(sConnect);
  ai = ai->ai_next;
}

freeaddrinfo(ai); //最後別忘了釋放鏈表

針對硬編碼的情況

針對這種情況一般是修改硬編碼,如果希望你的應用程序即支持IPV6也支持IPV4,那麽就需要去掉這些硬編碼的部分。微軟提供了一個工具叫"Checkv4.exe" 這個工具一般是放到VS的安裝目錄中,作為工具一起安裝到本機了,如果沒有可以去官網下載。
工具的使用也非常簡單

checkv4.exe 對應的.h或者.cpp 文件

這樣它會給出哪些代碼需要進行修改,甚至會給出修改意見,我們只要根據它的提示修改代碼即可。

幾個例子

因為IPV6 不能再像V4那樣直接往地址結構中填寫IP了,因此在IPV6的場合需要大量使用getaddrinfo函數,來根據具體的IP字符串或者根據主機名來自動獲取地址信息,然後根據地址信息直接調用connect即可,下面是微軟的例子

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

    int iResult = 0;

    ADDRINFO *AddrInfo = NULL;
    ADDRINFO *AI = NULL;
    ADDRINFO Hints;
    
    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;
    }
    
    if(NULL != AddrInfo)
    {
        SOCKET sConnect = socket(AddrInfo->ai_family, SOCK_STREAM, AddrInfo->ai_protocol);
        connect(sConnect, AddrInfo->ai_addr, AddrInfo->ai_addrlen);
        
        shutdown(sConnect, SD_BOTH);
        closesocket(sConnect);
    }
    freeaddrinfo(AddrInfo);
    return 0;
}

這個例子需要傳入額外的family參數來規定它使用何種地址結構,但是如果我只有一個主機名,而且事先並不知道需要使用何種IP協議來進行通信,這種情況下又該如何呢?
針對服務端,不存在這個問題,服務端是我們自己的代碼,具體使用IPV6還是IPV4這個實現是可以確定的,因此可以采用跟上面類似的寫法:

BOOL Create(int af_family)
{
    //這裏不建議使用IPPROTO_IP 或者IPPROTO_IPV6,使用TCP或者UDP可能會好點,因為它們是建立在IP協議之上的
    //當然,具體情況具體分析
    s = socket(af_family, SOCK_STREAM, IPPROTO_TCP);
}

BOOL Bind(int family, UINT nPort)
{
    addrinfo hins = {0};
    hins.ai_family = family;
    hins.ai_flags = AI_PASSIVE; /* For wildcard IP address */
    hins.ai_protocol = IPPROTO_TCP;
    hins.ai_socktype = SOCK_STREAM;

    addrinfo *lpAddr = NULL;
    CString csPort = "";
    csPort.Format("%u", nPort);
    if (0 != getaddrinfo(NULL, csPort, &hins, &lpAddr))
    {
        closesocket(s);
        return FALSE;
    }

    int nRes = bind(s, lpAddr->ai_addr, lpAddr->ai_addrlen);
    freeaddrinfo(lpAddr);

    if(nRes == 0)
        return TRUE;

    return FALSE;
}

//監聽,以及後面的收發包並沒有區分V4和V6,因此這裏不再給出跟他們相關的代碼

針對服務端,我們自然沒辦法事先知道它使用的IP協議的版本,因此傳入af_family參數在這裏不再適用,我們可以利用getaddrinfo函數根據服務端的主機名或者端口號來提前獲取它的地址信息,這裏我們可以封裝一個函數

int GetAF_FamilyByHost(LPCTSTR lpHost, int nPort, int SocketType)
{
    addrinfo hins = {0};
    addrinfo *lpAddr = NULL;
    hins.ai_family = AF_UNSPEC;
    hins.ai_socktype = SOCK_STREAM;
    hins.ai_protocol = IPPROTO_TCP;
    CString csPort = "";
    csPort.Format("%u", nPort);
    int af = AF_UNSPEC;
    char host[MAX_HOSTNAME_LEN] = "";
    if (lpHost == NULL)
    {
        gethostname(host, MAX_HOSTNAME_LEN);// 如果為NULL 則獲取本機的IP地址信息
    }else
    {
        strcpy_s(host, MAX_HOSTNAME_LEN, lpHost);
    }

    if(0 != getaddrinfo(host, csPort, &hins, &lpAddr))
    {
        return af;
    }

    af = lpAddr->ai_family;
    freeaddrinfo(lpAddr);
    return af;
}

有了地址家族信息,後面的代碼即可以根據地址家族信息來分別處理IP協議的不同版本,也可以使用上述服務端的思路,直接使用getaddrinfo函數得到的addrinfo結構中地址信息,下面給出第二種思路的部分代碼:

if(0 != getaddrinfo(host, csPort, &hins, &lpAddr))
{
    connect(s, lpAddr->ai_addr, lpAddr->ai_addrlen);
}

當然,也可以使用前面提到的 WSAConnectByName 函數,不過它需要針對IPV6來進行特殊的處理,需要事先知道服務端的IP協議的版本。

VC中各種地址結構

在學習網絡編程中,一個重要的概念就是IP地址,而巴克利套接字中提供了好幾種結構體來表示地址結構,微軟針對WinSock2 又提供了一些新的結構體,有的時候眾多的結構體讓人眼花繚亂,在這我根據自己的理解簡單的回顧一下這些常見的結構

SOCKADD_IN 與sockaddr_in結構

在Winsock2 中這二者是等價的, 它們的定義如下:

struct sockaddr_in{
   short sin_family;
   unsigned short sin_port;
   struct in_addr sin_addr;
   char sin_zero[8];
};
  • sin_family: 地址協議家族
  • sin_port:端口號
  • sin_addr: 表示ip地址的結構
  • sin_zero: 用於與sockaddr 結構體的大小對齊,這個數組裏面為全0

in_addr 結構如下:

struct in_addr { 
    union { 
        struct{ 
            unsigned char s_b1, 
            s_b2, 
            s_b3, 
            s_b4; 
        } S_un_b; 
        
        struct {
            unsigned short s_w1, s_w2; 
        } S_un_w; 
        
        unsigned long S_addr; 
    } S_un; 
}; 

這個結構是一個公用體,占4個字節,從本質上將IP地址僅僅是一個占4個字節的無符號整型數據,為了方便讀寫才會采用點分十進制的方式。
仔細觀察這個結構會發現,它其實定義了IP地址的幾種表現形式,我們可以將IP地址以一個字節一個字節的方式拆開來看,也可以以兩個字型數據的形式拆開,也可以簡單的看做一個無符號長整型。
當然在寫入的時候按照這幾種方式寫入,為了方便寫入IP地址,微軟定義了一個宏:

#define s_addr  S_un.S_addr

因此在填入IP地址的時候可以簡單的使用這個宏來給S_addr這個共用體成員賦值

一般像bind、connect等函數需要傳入地址結構,它們需要的結構為sockaddr,但是為了方便都會傳入SOCKADDR_IN結構

sockaddr SOCKADDR結構

這兩個結構也是等價的,它們的定義如下
struct sockaddr {
unsigned short sa_family;
char sa_data[14];
};

從結構上看它占16個字節與 SOCKADDR_IN大小相同,而且第一個成員都是地址家族的相關信息,後面都是存儲的具體的IPV4的地址,因此它們是可以轉化的,
為了方便一般是使用SOCKADDR_IN來保存IP地址,然後在需要填入SOCKADDR的時候強制轉化即可。

sockaddr_in6

該結構類似於sockaddr_in,只不過它表示的是IPV6的地址信息,在使用上,由於IPV6是128的地址占16個字節,而sockaddr_in 中表示地址的部分只有4個字節,
所以它與之前的兩個是不能轉化的,在使用IPV6的時候需要特殊的處理,一般不直接填寫IP而是直接根據IP的字符串或者主機名來連接。

sockaddr_storage

這是一個通用的地址結構,既可以用來存儲IPV4地址也可以存儲IPV6的地址,這個地址結構在前面已經說過了,這裏就不再詳細解釋了。

各種地址之間的轉化

一般我們只使用從SOCKADDR_IN到sockaddr結構的轉化,而且仔細觀察socket函數族發現只需要從其他結構中得到sockaddr結構,而並不需要從sockaddr轉化為其他結構,因此這裏重點放在如何轉化為sockaddr結構

  1. 從SOCKADDR_IN到sockaddr只需要強制類型轉化即可
  2. 從addrinfo結構中只需要調用其成員即可
  3. 從SOCKADDR_STORAGE結構到sockaddr只需要強制轉化即可。

其實在使用上更常用的是將字符串的IP轉化為對應的數值,針對IPV4有我們常見的inet_addrinet_ntoa 函數,它們都是在ipv4中使用的,
針對v6一般使用inet_pton,inet_ntop來轉化,它們兩個分別對應於inet_addrinet_ntoa。但是在WinSock中更常用的是WSAAddressToStringWSAStringToAddress

INT WSAAddressToString(
  LPSOCKADDR lpsaAddress,
  DWORD dwAddressLength,
  LPWSAPROTOCOL_INFO lpProtocolInfo,
  OUT LPTSTR lpszAddressString,
  IN OUT LPDWORD lpdwAddressStringLength
);
  • lpsaAddress: ip地址
  • dwAddressLength: 地址結構的長度
  • lpProtocolInfo: 協議信息的結構體,這個結構一般給NULL
  • lpszAddressString:目標字符串的緩沖
  • lpdwAddressStringLength:字符串的長度

而WSAStringToAddress定義與使用與它類似,這裏就不再說明了。


VC++ IPv6的支持