VC++ IPv6的支持
最近根據項目需要,要在產品中添加對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結構
- 從SOCKADDR_IN到sockaddr只需要強制類型轉化即可
- 從addrinfo結構中只需要調用其成員即可
- 從SOCKADDR_STORAGE結構到sockaddr只需要強制轉化即可。
其實在使用上更常用的是將字符串的IP轉化為對應的數值,針對IPV4有我們常見的inet_addr
、inet_ntoa
函數,它們都是在ipv4中使用的,
針對v6一般使用inet_pton
,inet_ntop
來轉化,它們兩個分別對應於inet_addr
、inet_ntoa
。但是在WinSock中更常用的是WSAAddressToString
與 WSAStringToAddress
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的支持