内容简介:最近根据项目需要,要在产品中添加对IpV6的支持,因此研究了一下IPV6的相关内容,Ipv6 与原来最直观的改变就是地址结构的改变,IP地址由原来的32位扩展为128,这样原来的地址结构肯定就不够用了,根据微软的官方文档,只需要对原来的代码做稍许改变就可以适应ipv6。根据微软官方的说法,要做到支持Ipv6首先要做的就是将原来的
最近根据项目需要,要在产品中添加对IpV6的支持,因此研究了一下IPV6的相关内容,Ipv6 与原来最直观的改变就是地址结构的改变,IP地址由原来的32位扩展为128,这样原来的地址结构肯定就不够用了,根据微软的官方文档,只需要对原来的代码做稍许改变就可以适应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定义与使用与它类似,这里就不再说明了。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 支持向量机(一):支持向量机的分类思想
- Vardump 更新,支持打印各种 Java 数据结构,支持嵌套
- Vardump 更新,支持打印各种 Java 数据结构,支持嵌套
- NutzBoot v2.1.5 添加单元测试支持及 ssdb 支持
- iView 3.1.0 版本:支持 TypeScript,支持 Vue CLI 3
- Mesalink v1.0.0 发布,正式支持 TLS 1.3 和 IPv6,支持CMake编译,支持Windows,实现生产环境可用
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。