1、IPV6-Only支持是啥?html
首先IPV6,是对IPV4地址空间的扩充。目前当咱们用iOS设备链接上Wifi、4G、3G等网络时,设备被分配的地址均是IPV4地址,可是随着运营商和企业逐渐部署IPV6 DNS64/NAT64网络以后,设备被分配的地址会变成IPV6的地址,而这些网络就是所谓的IPV6-Only网络,而且仍然能够经过此网络去获取IPV4地址提供的内容。客户端向服务器端请求域名解析,首先经过DNS64 Server查询IPv6的地址,若是查询不到,再向DNS Server查询IPv4地址,经过DNS64 Server合成一个IPV6的地址,最终将一个IPV6的地址返回给客户端。如图所示:ios
在Mac OS 10.11+的双网卡的Mac机器(以太网口+无线网卡),咱们能够经过模拟构建这么一个local IPv6 DNS64/NAT64 的网络环境去测试应用是否支持IPV6-Only网络,大概原理以下:git
参考资料: https://developer.apple.com/library/mac/documentation/NetworkingInternetWeb/Conceptual/NetworkingOverview/UnderstandingandPreparingfortheIPv6Transition/UnderstandingandPreparingfortheIPv6Transition.html#//apple_ref/doc/uid/TP40010220-CH213-SW1
2、Apple如何审核支持IPV6-Only?github
首先第一点:这里说的支持IPV6-Only网络,其实就是说让应用在 IPv6 DNS64/NAT64 网络环境下仍然可以正常运行。可是考虑到咱们目前的实际网络环境仍然是IPV4网络,因此应用须要可以同时保证IPV4和IPV6环境下的可用性。从这点来讲,苹果不会去扫描IPV4的专有API来拒绝审核经过,由于IPV4的API和IPV6的API调用都会同时存在于代码中(不过为了减少审核被拒风险,建议将IPV4专有API经过IPV6的兼容API来替换)。服务器
其次第二点:Apple官方声明iOS9开始向IPV6支持过渡,在iOS9.2+支持经过getaddrInfo方法将IPV4地址合成IPV6地址(The ability to synthesize IPv6 addresses was added to getaddrinfo in iOS 9.2 and OS X 10.11.2)。其提供的Reachability库在iOS8系统下,当从IPV4切换到IPV6网络,或者从IPV6网络切换到IPV4,是没法监控到网络状态的变化。也有一些开发者针对这些Bug询问Apple的审核部门,给予的答复是只须要在苹果最新的系统上保证IPV6的兼容性便可。网络
最后第三点:只要应用的主流程支持IPV6,经过苹果审核便可。对于不支持IPV6的模块,考虑到咱们现实IPV6网络的部署还须要一段时间,短期内不会影响咱们用户的使用。但随着4G网络IPV6的部署,这部分模块仍是须要逐渐安排人力进行支持。数据结构
追加第四点:若是应用一直直接使用IPV4地址经过NSURLConenction或者NSURLSession进行网络请求(通常须要服务器容许,且客户端须要在header中假装host);经测试,IPV6网络环境下,直接使用IPV4地址在iOS9及以上的系统仍然可以正常访问;在iOS8.4及如下不能正常访问;这一点苹果的解释和建议是这样的:app
Note: In iOS 9 and OS X 10.11 and later, NSURLSession and CFNetwork automatically synthesize IPv6 addresses from IPv4 literals locally on devices operating on DNS64/NAT64 networks. However, you should still work to rid your code of IP address literals.socket
3、应用如何支持IPV6-Only?xss
对于如何支持IPV6-Only,官方给出了以下几点标准:(这里就不对其进行解释了,你们看上面的参考连接便可)
1. Use High-Level Networking Frameworks;
2. Don’t Use IP Address Literals;
3. Check Source Code for IPv6 DNS64/NAT64 Incompatibilities;
4. Use System APIs to Synthesize IPv6 Addresses;
3.1 NSURLConnection是否支持IPV6?
官方的这句话让咱们疑惑顿生: using high-level networking APIs such as NSURLSession and the CFNetwork frameworks and you connect by name, you should not need to change anything for your app to work with IPv6 addresses
只说了NSURLSession和CFNetwork的API不须要改变,可是并无说起到NSURLConnection。 从上文的参考资料中,咱们看到NSURLSession、NSURLConnection同属于Cocoa的url loading system,能够猜想出NSURLConnection在ios9上是支持IPV6的。
应用里面的API网络请求,你们通常都会选择AFNetworking进行请求发送,因为历史缘由,应用的代码基本上都深度引用了AFHTTPRequestOperation类,因此目前API网络请求均须要经过NSURLConnection发送出去,因此必须确认NSURLConnection是否支持IPV6. 通过测试,NSURLConnection在最新的iOS9系统上是支持IPV6的。
3.2 Cocoa的URL Loading System从iOS哪一个版本开始支持IPV6?
目前咱们的应用最低版本还须要支持iOS7,虽然苹果只要求最新版本支持IPV6-Only,可是出于对用户负责的态度,咱们仍然须要搞清楚在低版本上URL Loading System的API是否支持IPV6.
(to fix me, make some experiments)待续~~~
3.3 Reachability是否须要修改支持IPV6?
咱们能够查到应用中大量使用了Reachability进行网络状态判断,可是在里面却使用了IPV4的专用API。
在Pods:Reachability中
AF_INET Files:Reachability.m
struct sockaddr_in Files:Reachability.h , Reachability.m
那Reachability应该如何支持IPV6呢? (1)目前Github的开源库Reachability的最新版本是3.2,苹果也出了一个Support IPV6 的Reachability的官方样例,咱们比较了一下源码,跟Github上的Reachability没有什么差别。 (2)咱们一般都是经过一个0.0.0.0 (ZeroAddress)去开启网络状态监控,通过咱们测试,在iOS9以上的系统上IPV4和IPV6网络环境均可以正常使用;可是在iOS8上IPV4和IPV6相互切换的时候没法监控到网络状态的变化,多是由于苹果在iOS8上还并无对IPV6进行相关支持相关。(可是这仍然知足苹果要求在最新系统版本上支持IPV6的网络)。 (3)当你们都在要求Reachability添加对于IPV6的支持,其实苹果在iOS9以上对Zero Address进行了特别处理,官方发言是这样的:
reachabilityForInternetConnection: This monitors the address 0.0.0.0, which reachability treats as a special token that causes it to actually monitor the general routing status of the device, both IPv4 and IPv6.
+ (instancetype)reachabilityForInternetConnection {
struct sockaddr_in zeroAddress;
bzero(&zeroAddress, sizeof(zeroAddress));
zeroAddress.sin_len = sizeof(zeroAddress);
zeroAddress.sin_family = AF_INET;
return [self reachabilityWithAddress: (const struct sockaddr *) &zeroAddress];
}
综上所述,Reachability不须要作任何修改,在iOS9上就能够支持IPV6和IPV4,可是在iOS9如下会存在bug,可是苹果审核并不关心。
4、底层的socket API如何同时支持IPV4和IPV6?
因为在应用中使用了网络诊断的组件,大量使用了底层的 socket API,因此对于IPV6支持,这块是个重头戏。若是你的应用中使用了长链接,其必然会使用底层socket API,这一块也是须要支持IPV6的。 对于Socket如何同时支持IPV4和IPV6,能够参考谷歌的开源库CocoaAsyncSocket.
下面我针对咱们的开源 网络诊断组件, 说一下是如何同时支持IPV4和IPV6的。 开源地址:https://github.com/Lede-Inc/LDNetDiagnoService_IOS.git 这个网络诊断组件的主要功能以下:
本地网络环境的监测(本机IP+本地网关+本地DNS+域名解析);
经过TCP Connect监测到域名的连通性;
经过Ping 监测到目标主机的连通耗时;
经过traceRoute监测设备到目标主机中间每个路由器节点的ICMP耗时;
4.1 IP地址从二进制到符号的转化
以前咱们都是经过inet_ntoa()进行二进制到符号,这个API只能转化IPV4地址。而inet_ntop()可以兼容转化IPV4和IPV6地址。 写了一个公用的in6_addr的转化方法以下:
//for IPV6
+(NSString *)formatIPV6Address:(struct in6_addr)ipv6Addr{
NSString *address = nil;
char dstStr[INET6_ADDRSTRLEN];
char srcStr[INET6_ADDRSTRLEN];
memcpy(srcStr, &ipv6Addr, sizeof(struct in6_addr));
if(inet_ntop(AF_INET6, srcStr, dstStr, INET6_ADDRSTRLEN) != NULL){
address = [NSString stringWithUTF8String:dstStr];
}
return address;
}
//for IPV4
+(NSString *)formatIPV4Address:(struct in_addr)ipv4Addr{
NSString *address = nil;
char dstStr[INET_ADDRSTRLEN];
char srcStr[INET_ADDRSTRLEN];
memcpy(srcStr, &ipv4Addr, sizeof(struct in_addr));
if(inet_ntop(AF_INET, srcStr, dstStr, INET_ADDRSTRLEN) != NULL){
address = [NSString stringWithUTF8String:dstStr];
}
return address;
}
4.2 本机IP获取支持IPV6
至关于咱们在终端中输入ifconfig命令获取字符串,而后对ifconfig结果字符串进行解析,获取其中en0(Wifi)、pdp_ip0(移动网络)的ip地址。
注意: (1)在模拟器和真机上都会出现以FE80开头的IPV6单播地址影响咱们判断,因此在这里进行特殊的处理(当第一次遇到不是单播地址的IP地址即为本机IP地址)。 (2)在IPV6环境下,真机测试的时候,第一个出现的是一个IPV4地址,因此在IPV4条件下第一次遇到单播地址不退出。
+ (NSString *)deviceIPAdress
{
while (temp_addr != NULL) {
NSLog(@"ifa_name===%@",[NSString stringWithUTF8String:temp_addr->ifa_name]);
// Check if interface is en0 which is the wifi connection on the iPhone
if ([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"] || [[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"pdp_ip0"])
{
//若是是IPV4地址,直接转化
if (temp_addr->ifa_addr->sa_family == AF_INET){
// Get NSString from C String
address = [self formatIPV4Address:((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr];
}
//若是是IPV6地址
else if (temp_addr->ifa_addr->sa_family == AF_INET6){
address = [self formatIPV6Address:((struct sockaddr_in6 *)temp_addr->ifa_addr)->sin6_addr];
if (address && ![address isEqualToString:@""] && ![address.uppercaseString hasPrefix:@"FE80"]) break;
}
}
temp_addr = temp_addr->ifa_next;
}
}
}
4.3 设备网关地址获取获取支持IPV6
实际上是在IPV4获取网关地址的源码的基础上进行了修改,初开把AF_INET->AF_INET6, sockaddr -> sockaddr_in6以外,还须要注意以下修改,就是拷贝的地址字节数。去掉了ROUNDUP的处理。 (解析出来的地址总是少了4个字节,结果是偏移量搞错了,纠结了半天),具体参考源码库。
/* net.route.0.inet.flags.gateway */
int mib[] = {CTL_NET, PF_ROUTE, 0, AF_INET6, NET_RT_FLAGS, RTF_GATEWAY};
if (sysctl(mib, sizeof(mib) / sizeof(int), buf, &l, 0, 0) < 0 xss=removed xss=removed>rtm_addrs & (1 << i xss=removed xss=removed>sa_len));
} else {
sa_tab[i] = NULL;
}
}
//for IPV6
for (i = 0; i < RTAX>rtm_addrs & (1 << i xss=removed xss=removed>sin6_len);
} else {
sa_tab[i] = NULL;
}
}
4.4 设备DNS地址获取支持IPV6
IPV4时只须要经过res_ninit进行初始化就能够获取,可是在IPV6环境下须要经过res_getservers()接口才能获取。
+(NSArray *)outPutDNSServers{
res_state res = malloc(sizeof(struct __res_state));
int result = res_ninit(res);
NSMutableArray *servers = [[NSMutableArray alloc] init];
if (result == 0) {
union res_9_sockaddr_union *addr_union = malloc(res->nscount * sizeof(union res_9_sockaddr_union));
res_getservers(res, addr_union, res->nscount);
for (int i = 0; i < res>nscount; i++) {
if (addr_union[i].sin.sin_family == AF_INET) {
char ip[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(addr_union[i].sin.sin_addr), ip, INET_ADDRSTRLEN);
NSString *dnsIP = [NSString stringWithUTF8String:ip];
[servers addObject:dnsIP];
NSLog(@"IPv4 DNS IP: %@", dnsIP);
} else if (addr_union[i].sin6.sin6_family == AF_INET6) {
char ip[INET6_ADDRSTRLEN];
inet_ntop(AF_INET6, &(addr_union[i].sin6.sin6_addr), ip, INET6_ADDRSTRLEN);
NSString *dnsIP = [NSString stringWithUTF8String:ip];
[servers addObject:dnsIP];
NSLog(@"IPv6 DNS IP: %@", dnsIP);
} else {
NSLog(@"Undefined family.");
}
}
}
res_nclose(res);
free(res);
return [NSArray arrayWithArray:servers];
}
4.4 域名DNS地址获取支持IPV6
在IPV4网络下咱们经过gethostname获取,而在IPV6环境下,经过新的gethostbyname2函数获取。
//ipv4
phot = gethostbyname(hostN);
//ipv6
phot = gethostbyname2(hostN, AF_INET6);
4.5 ping方案支持IPV6
Apple的官方提供了最新的支持IPV6的ping方案,参考地址以下: https://developer.apple.com/library/mac/samplecode/SimplePing/Introduction/Intro.html
只是须要注意的是: (1)返回的packet去掉了IPHeader部分,IPV6的header部分也不返回TTL(Time to Live)字段; (2)IPV6的ICMP报文不进行checkSum的处理;
4.6 traceRoute方案支持IPV6
实际上是经过建立socket套接字模拟ICMP报文的发送,以计算耗时; 两个关键的地方须要注意: (1)IPV6中去掉IP_TTL字段,改用跳数IPV6_UNICAST_HOPS来表示; (2)sendto方法能够兼容支持IPV4和IPV6,可是须要最后一个参数,制定目标IP地址的大小;由于前一个参数只是指明了IP地址的开始地址。千万不要用统一的sizeof(struct sockaddr), 由于sockaddr_in 和 sockaddr都是16个字节,二者能够通用,可是sockaddr_in6的数据结构是28个字节,若是不显式指定,sendto方法就会一直返回-1,erroNo报22 Invalid argument的错误。
关键代码以下:(完整代码参考开源组件)
//构造通用的IP地址结构stuck sockaddr
NSString *ipAddr0 = [serverDNSs objectAtIndex:0];
//设置server主机的套接口地址
NSData *addrData = nil;
BOOL isIPV6 = NO;
if ([ipAddr0 rangeOfString:@":"].location == NSNotFound) {
isIPV6 = NO;
struct sockaddr_in nativeAddr4;
memset(&nativeAddr4, 0, sizeof(nativeAddr4));
nativeAddr4.sin_len = sizeof(nativeAddr4);
nativeAddr4.sin_family = AF_INET;
nativeAddr4.sin_port = htons(udpPort);
inet_pton(AF_INET, ipAddr0.UTF8String, &nativeAddr4.sin_addr.s_addr);
addrData = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)];
} else {
isIPV6 = YES;
struct sockaddr_in6 nativeAddr6;
memset(&nativeAddr6, 0, sizeof(nativeAddr6));
nativeAddr6.sin6_len = sizeof(nativeAddr6);
nativeAddr6.sin6_family = AF_INET6;
nativeAddr6.sin6_port = htons(udpPort);
inet_pton(AF_INET6, ipAddr0.UTF8String, &nativeAddr6.sin6_addr);
addrData = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)];
}
struct sockaddr *destination;
destination = (struct sockaddr *)[addrData bytes];
//建立socket
if ((recv_sock = socket(destination->sa_family, SOCK_DGRAM, isIPV6?IPPROTO_ICMPV6:IPPROTO_ICMP)) < 0 xss=removed>sa_family, SOCK_DGRAM, 0)) < 0)
//设置sender 套接字的ttl
if ((isIPV6?
setsockopt(send_sock,IPPROTO_IPV6, IPV6_UNICAST_HOPS, &ttl, sizeof(ttl)):
setsockopt(send_sock, IPPROTO_IP, IP_TTL, &ttl, sizeof(ttl))) < 0)
//发送成功返回值等于发送消息的长度ssize_t sentLen = sendto(send_sock, cmsg, sizeof(cmsg), 0, (struct sockaddr *)destination, isIPV6?sizeof(struct sockaddr_in6):sizeof(struct sockaddr_in));