IPv6是英文“Internet Protocol Version 6”(互聯網協議第6版)的縮寫,是互聯網工程任務組(IETF)設計的用於替代IPv4的下一代IP協議,其地址數量號稱可以爲全世界的每一粒沙子編上一個地址。
推動ipv6的一個重要目的是爲了解決ipv4地址不足的問題。
目前蘋果已經要求新上架的app必須支持ipv6 only,國內各大運營商也對ipv6進行了一定程度的支持。 國家也對ipv6的發展有許多的推動政策(詳見https://www.china-ipv6.cn/#/index),實際上,國內目前對於ipv6的支持與國外相比還有較大的差距。
由於ipv6在設計的時候沒有做到與ipv4兼容,是導致ipv4升級到ipv6存在困難的主要原因,大量的硬件設備、軟件產品如果要支持ipv6都要進行大量的修改,這無疑是非常困難的。 不過ipv6標準已經制定了許多年,操作系統層面的支持已經非常完善,目前主流的windows7, linux對於ipv6的支持都非常好。
目前更多的支持就需要在應用程序設計層面進行了。那麼爲了支持ipv6我們的應用程序需要做哪些改變呢,尤其是c++這種偏底層的開發。下面我們就來講解如何對程序進行升級來支持ipv6。
一、ipv6地址
IPv6地址語法:
IPv4地址表示爲點分十進制格式,32位的地址分成4個8位分組,每個8位寫成十進制,中間用點號分隔。
IPv6地址表示爲冒號分十六進制格式,128位地址以16位爲一分組,每個16位分組寫成4個十六進制數,中間用冒號分隔。
特殊
::/128即0:0:0:0:0:0:0:0,只能作爲尚未獲得正式地址的主機的源地址,不能作爲目的地址,不能分配給真實的網絡接口。
::1/128即0:0:0:0:0:0:0:1,環回地址,相當於IPv4中的localhost(127.0.0.1)
ipv6地址後面跟着的/64,/48,/32指的是ipv6地址的前綴長度(前綴,即前64或48或32位長度的地址相同)。
由於ipv6地址是128位長度(使用的是16進制),但協議規定了後64位爲網絡接口ID(可理解爲設備在網絡上的唯一ID),所以一般家用ipv6分發是分配/64前綴的(64位前綴+64位接口ID)。
ipv6地址和端口號連寫的方式:(ipv6地址中包含“:”,容易與端口號的冒號衝突)
例如 [::0]:80 用“[]”將IPv6地址包含起來,端口號還採用“:”標識。
二、具體要更改的socket函數
在 IPv4 網絡下,網絡編程主要依靠的是 socket 連接。在客戶端,其基本步驟如下,創建一個 socket,使用 socket 連接服務器,最後通過 TCP 或者 UDP 協議進行數據讀寫。如果把這套方法移植到 IPv6 網絡下,就需要在原來的基礎上引入新的協議族、新的數據結構以及新的地址域名轉換函數等。具體的一些差異如圖 1所示:
圖 1. IPv4 與 IPv6 區別
在這裏要稍微介紹下 getaddrinfo()函數,它提供獨立於協議的名稱解析。函數的前兩個參數分別是節點名和服務名。節點名可以是主機名,也可以是地址串 (IPv4 的點分十進制數表示或 IPv6 的十六進制數字串 )。服務名可以是十進制的端口號,也可以是已定義的服務名稱,如 ftp、http 等。函數的第三個參數 hints 是 addrinfo 結構的指針,由調用者填寫關於它所想返回的信息類型的線索。函數的返回值是一個指向 addrinfo 結構的鏈表指針 res。詳見圖 2。
圖 2. getaddrinfo 函數說明
調用getaddrinfo 函數之前通常需要對以下 6 個參數進行以下設置:nodename、servname、hints 的 ai_flags、ai_family、ai_socktype、ai_protocol。在 6 項參數中,對函數影響最大的是nodename(host),sername(port) 和 hints.ai_flag。而 ai_family 只是有地址爲 v4 地址或 v6 地址的區別。而 ai_protocol 一般是爲 0 不作改動。其中 ai_flags、ai_family、ai_socktype。說明如圖 3所示:
圖 3. getaddrinfo 參數說明
getaddrinfo 函數在 IPv6 和 IPv4 網絡下都能實現獨立於協議的名稱解析,而且它返回的指向 addrinfo 結構的鏈表中會存放所有由輸入參數 nodename 解析出的所有對應的 IP 信息,包括 IP 地址,協議族信息等。所以只要對 const struct addrinfo* hints 進行一些配置,就可以利用這個函數來識別連接目標的網絡協議屬性,進而根據其網絡協議族而進行準確的連接操作。這樣就解決了我們提出的第一個問題。
AF_UNSPEC、AF_INET和AF_INET6之間的關係
ai_family參數指定調用者期待返回的套接口地址結構的類型。它的值包括三種:AF_INET,AF_INET6和AF_UNSPEC。如果指定AF_INET,那麼函數九不能返回任何IPV6相關的地址信息;如果僅指定了AF_INET6,則就不能返回任何IPV4地址信息。AF_UNSPEC則意味着函數返回的是適用於指定主機名和服務名且適合任何協議族的地址。如果某個主機既有AAAA記錄(IPV6)地址,同時又有A記錄(IPV4)地址,那麼AAAA記錄將作爲sockaddr_in6結構返回,而A記錄則作爲sockaddr_in結構返回
三、ipv4到ipv6的移植與兼容
提示:用IPV6建立服務器端的話,即使客戶端仍用IPV4的socket連接也可以正常通訊,IPV4的地址會被轉換成這種地址 ::ffff:192.168.27.25。
1.服務器端從IPV4移植到IPV6要做些什麼?
可以調用getaddrinfo得到AF_INET6的通配地址,也可以直接將sockaddr_in結構體更改爲sockaddr_in6結構體並相應初始化即可
是不是有點複雜,具體請看代碼。
2.客戶器端如何同時兼容IPV4和IPV6?
按如上方法就可同時兼容IPV4和IPV6的連接請求。
3.客戶器端從IPV4移植到IPV6要做些什麼?
如果服務器端具有IPV4地址則基本不需改動,因爲IPV6的服務器對IPV4客戶端是兼容的,如果服務端用的是域名且是IPV6的地址則必須調用getaddrinfo得到addrinfo的可用IP地址。
4.客戶器端如何同時兼容IPV4和IPV6?
指定getaddrinfo的family爲AF_UNSPEC,會返回可用的IPV4和IPV6 IP地址鏈表。用返回的family、socktype、protocol建立socket,用返回的IP地址進行 connect連接請求。
四、具體代碼
如果看上面的文字你沒懂,那麼不要緊,看了下面的代碼我想很多人再結合上面的文字就很容易理解了。
代碼參考:《ipv4、ipv6兼容編程》https://blog.csdn.net/ligt0610/article/details/18667595
原文代碼編譯時會有找不到頭文件的問題,這裏進行了補充。
編譯環境 centos-7.6 , gcc-4.8.5
server.cpp
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <errno.h>
#include <time.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
using namespace std;
int tcp_listen(const char *host, const char *service, const int listen_num = 5)
{
int listenfd, ret;
const int on = 1;
struct addrinfo hints, *res, *ressave;
bzero(&hints, sizeof(hints));
hints.ai_flags = AI_PASSIVE;
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_IP;
if (0 != (ret = getaddrinfo(host, service, &hints, &res)))
{
cout << "getaddrinfo error: " << gai_strerror(ret) << endl;
return -1;
}
ressave = res;
while(NULL != res)
{
if (-1 == (listenfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol)))
{
cout << "create socket error: " << strerror(errno) << endl;
res = res->ai_next;
continue;
}
if (-1 == setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)))
{
cout << "setsockopt error: " << strerror(errno) << endl;
close(listenfd);
res = res->ai_next;
continue;
}
if (-1 == bind(listenfd, res->ai_addr, res->ai_addrlen))
{
cout << "bind error: " << strerror(errno) << endl;
close(listenfd);
res = res->ai_next;
continue;
}
if (-1 == listen(listenfd, listen_num))
{
cout << "listen error: " << strerror(errno) << endl;
close(listenfd);
res = res->ai_next;
continue;
}
break;
}
freeaddrinfo(ressave);
if (NULL == res)
return -1;
return listenfd;
}
int get_addrinfo(const struct sockaddr *addr, string &ip, in_port_t &port)
{
void *numeric_addr = NULL;
char addr_buff[INET6_ADDRSTRLEN];
if (AF_INET == addr->sa_family)
{
numeric_addr = &((struct sockaddr_in*)addr)->sin_addr;
port = ntohs(((struct sockaddr_in*)addr)->sin_port);
}
else if (AF_INET6 == addr->sa_family)
{
numeric_addr = &((struct sockaddr_in6*)addr)->sin6_addr;
port = ntohs(((struct sockaddr_in6*)addr)->sin6_port);
}
else
{
return -1;
}
if (NULL != inet_ntop(addr->sa_family, numeric_addr, addr_buff, sizeof(addr_buff)))
ip = addr_buff;
else
return -1;
return 0;
}
int main(int argc, char *argv[])
{
int listenfd, connfd;
struct sockaddr_storage cliaddr;
socklen_t len = sizeof(cliaddr);
time_t now;
char buff[128];
if (2 == argc) //指定端口
listenfd = tcp_listen(NULL, argv[1]);
else if (3 == argc) //指定本地IP和端口
listenfd = tcp_listen(argv[1], argv[2]);
else
{
cout << "usage: " << argv[0] << " [<hostname/ipaddress>] <service/port>" << endl;
return -1;
}
if (listenfd < 0)
{
cout << "call tcp_listen error" << endl;
return -1;
}
while (true)
{
connfd = accept(listenfd, (struct sockaddr*)&cliaddr, &len);
string ip = "";
in_port_t port = 0;
get_addrinfo((struct sockaddr*)&cliaddr, ip, port);
cout << "client " << ip << "|" << port << " login" << endl;
now = time(NULL);
snprintf(buff, sizeof(buff) - 1, "%.24s", ctime(&now));
write(connfd, buff, strlen(buff));
close(connfd);
}
close(listenfd);
return 0;
}
client代碼
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <errno.h>
#include <time.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
using namespace std;
int tcp_connect(const char *host, const char *service)
{
int sockfd, ret;
struct addrinfo hints, *res, *ressave;
bzero(&hints, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_IP;
if (0 != (ret = getaddrinfo(host, service, &hints, &res)))
{
cout << "getaddrinfo error: " << gai_strerror(ret) << endl;
return -1;
}
ressave = res;
while (NULL != res)
{
if (-1 == (sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol)))
{
cout << "create socket error: " << strerror(errno) << endl;
res = res->ai_next;
continue;
}
if (-1 == connect(sockfd, res->ai_addr, res->ai_addrlen))
{
cout << "connect error: " << strerror(errno) << endl;
close(sockfd);
res = res->ai_next;
continue;
}
break;
}
freeaddrinfo(ressave);
if (NULL == res)
return -1;
return sockfd;
}
int main(int argc, char *argv[])
{
int sockfd, n;
char buff[128];
struct sockaddr_storage cliaddr;
if (3 != argc)
{
cout << "usage: " << argv[0] << " <hostname/ipaddress> <service/port>" << endl;
return -1;
}
sockfd = tcp_connect(argv[1], argv[2]);
if (sockfd < 0)
{
cout << "call tcp_connect error" << endl;
return -1;
}
bzero(buff, sizeof(buff));
while ((n = read(sockfd, buff, sizeof(buff) - 1) > 0))
{
cout << buff << endl;
bzero(buff, sizeof(buff));
}
close(sockfd);
return 0;
}
編譯命令:
g++ server.cpp -o server
g++ client.cpp -o client
不同的命令有不同的行爲
./server ::0 9527 #服務器監聽ipv6地址, ipv4也可以訪問
./server 0.0.0.0 9527 #只能用ipv4地址訪問
./client ::0 9527 #用ipv6地址訪問server
./client 127.0.0.1 9527 #用ipv4地址訪問server
#客戶端地址在服務端顯示爲 ::ffff:127.0.0.1