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地址语法:
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地址包含起来,端口号还采用“:”标识。
在 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结构返回
提示:用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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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] << " [] " << 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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
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] << " " << 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