针对苹果最新审核要求为应用兼容IPv6-备用

在WWDC2015上苹果宣布iOS9将支持纯IPv6的网络服务。2016年初开始所有提交到App Store的应用必须支持IPv6。为确保现有的应用是兼容的,我们需要注意下面几点。

 

不建议使用底层的网络API

 

下图展示的蓝色部分的这些API都是不存在兼容性问题的,而我们平时自己用的包括那些第三方的网络库大部分都是用的这些API。

 

Networking frameworks and API layers

 

大部分情况下,我们用高级的API完全能够实现我们的需求,而且高级API封装的很便于使用,很多底层的像适配IPv6的工作都已经帮我们做好了。而用底层API会有大量的工作要我们自己来做,更容易产生bug。但你如果确实需要用底层的POSIX socket API, 请参照这个RFC4038: Application Aspects of IPv6 Transition的指导。

 

不要用IP地址

 

比如下面这个API,nodename这个参数不要传IP地址,而应该用域名

 

 

SCNetworkReachabilityCreateWithName

 

这个方法在著名的Reachability中是用到的,我们常用的网络库AFNetworking就用了这个。所以用到的同学得好好查一下了,另外这个项目的作者几天前刚刚就这个问题有一个新的提交,不过最新的release版本中还没有加进去,可以点下面链接先去看看他都改了哪些地方。

 

Added support for IPv6 to Reachability #3174

https://github.com/AFNetworking/AFNetworking/pull/3174/files

 

检查不兼容IPv6的代码

 

搜一下工程里有没有下面的这些API,这些都是只针对IPv4做处理的,有的话就删了。

 

inet_addr()

 

inet_aton()

 

inet_lnaof()

 

inet_makeaddr()

 

inet_netof()

 

inet_network()

 

inet_ntoa()

 

inet_ntoa_r()

 

bindresvport()

 

getipv4sourcefilter()

 

setipv4sourcefilter()

 

如果用到了下面左边的这些IPv4的类型,那么它们相应的IPv6类型也需要做处理

 

IPv4-IPv6

 

本地搭建IPv6测试环境

 

最后我们来搭一个IPv6的测试环境吧,你所需要的就是一台用非Wi-Fi的方式上网的Mac电脑。

 

我们的要做的其实就是用Mac做一个热点,然后用iPhone连接这个Wi-Fi,听起来很容易,我相信大家在公司就是这么干的吧。

 

区别是这次我们产生的是一个本地的IPv6 DNS64/NAT64网络,这项功能是OS X 10.11新加的。和我们以前开启热点方式不一样的地方在于,我们在“System Preferences”界面选中“Sharing”的同时,要按住“Option”键。

 

System Preferences

 

之后在“Sharing”界面中,我们会看到和之前不一样的地方,就是红框所标的地方,多了一个叫“Create NAT64 Network”的选框,选中它。

 

Sharing

 

之后就是按照正常的创建热点的流程走完就行了。

 

现在我们用iPhone连接上这个刚创建好的热点就可以测试了,注意此时要把iPhone设成飞行模式,以保证只用Wi-Fi上网。

 

说法三:

最近很多人都在关注支持 IPv6 的事情吧?我们公司也是。也有不少同行使用了我们的 YTKNetwork 网络库,问我们什么时候迁移到 AFNetworking 3.0。正在这个时候,我发现了本文。

在本文中,作者经过测试发现,NSURLConnection 是支持 IPv6 的,因此基于 NSURLConnection 的 AFNetworking 2.x 也同样应该是支持 IPv6,所以大家不用担心。只要网络请求是基于 AFNetworking 的,都不用做什么额外的事情。我向本文的作者 philon 申请了全文的转载授权,分享给大家。

作者介绍:philon, iOS 高级开发工程师,现在网易乐得彩票项目组负责技术研发工作~

以下是文章正文,本文的所有打赏归 philon 所有。


果然是苹果打个哈欠,iOS 行业内就得起一次风暴呀。自从 5 月初 Apple 明文规定所有开发者在 6 月 1 号以后提交新版本需要支持 IPv6-Only 的网络,大家便开始热火朝天的研究如何支持 IPv6,以及应用中哪些模块目前不支持 IPv6。

一、IPv6-Only 支持是啥?

首先 IPv6,是对 IPv4 地址空间的扩充。目前当我们用 iOS 设备连接上 Wifi、4G、3G 等网络时,设备被分配的地址均是 IPv4 地址,但是随着运营商和企业逐渐部署 IPv6 DNS64/NAT64 网络之后,设备被分配的地址会变成 IPv6 的地址,而这些网络就是所谓的 IPv6-Only 网络,并且仍然可以通过此网络去获取 IPv4 地址提供的内容。客户端向服务器端请求域名解析,首先通过 DNS64 Server 查询 IPv6 的地址,如果查询不到,再向 DNS Server 查询 IPv4 地址,通过 DNS64 Server 合成一个 IPv6 的地址,最终将一个 IPv6 的地址返回给客户端。如图所示:

在 Mac OS 10.11+的双网卡的 Mac 机器(以太网口+无线网卡),我们可以通过模拟构建这么一个 local IPv6 DNS64/NAT64 的网络环境去测试应用是否支持 IPv6-Only 网络,大概原理如下:

  • 参考资料:

  • https://developer.apple.com/library/mac/documentation/NetworkingInternetWeb/Conceptual/NetworkingOverview/UnderstandingandPreparingfortheIPv6Transition/UnderstandingandPreparingfortheIPv6Transition.html#//apple_ref/doc/uid/TP40010220-CH213-SW1

二、Apple 如何审核支持 IPv6-Only?

首先第一点: 这里说的支持 IPv6-Only 网络,其实就是说让应用在 IPv6 DNS64/NAT64 网络环境下仍然能够正常运行。但是考虑到我们目前的实际网络环境仍然是 IPv4 网络,所以应用需要能够同时保证 IPv4 和 IPv6 环境下的可用性。从这点来说,苹果不会去扫描 IPv4 的专有 API 来拒绝审核通过,因为 IPv4 的 API 和 IPv6 的 API 调用都会同时存在于代码中。

其次第二点:Apple 官方声明 iOS9 开始向 IPv6 支持过渡,在 iOS9.2+ 支持 IPv4 地址合成 IPv6 地址。其提供的 Reachability 库在 iOS8 系统下,当从 IPv4 切换到 IPv6 网络,或者从 IPv6 网络切换到 IPv4,是无法监控到网络状态的变化。也有一些开发者针对这些 Bug 询问 Apple 的审核部门, 给予的答复是只需要在苹果最新的系统上保证 IPv6 的兼容性即可

最后第三点: 只要应用的主流程支持 IPv6,通过苹果审核即可。对于不支持 IPv6 的模块,考虑到我们现实 IPv6 网络的部署还需要一段时间,短时间内不会影响我们用户的使用。但随着 4G 网络 IPv6 的部署,这部分模块还是需要逐渐安排人力进行支持。

三、应用如何支持 IPv6-Only?

对于如何支持 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,但是苹果审核并不关心。

四、底层的 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 的转化方法如下:

+(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;
}
4.2 本机 IP 获取支持 IPv6

相当于我们在终端中输入 ifconfig 命令获取字符串,然后对 ifconfig 结果字符串进行解析,获取其中 en0(模拟器)、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 = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)];
//                    if (address && ![address isEqualToString:@""] && ![address.uppercaseString hasPrefix:@"FE80"]) break;
}

// 如果是 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) {
address = @"192.168.0.1";
}

....

//for IPv4
for (i = 0; i < RTAX_MAX; i++) {
if (rt->rtm_addrs & (1 << i)) {
sa_tab[i] = sa;
sa = (struct sockaddr *)((char *)sa + ROUNDUP(sa->sa_len));
} else {
sa_tab[i] = NULL;
}
}

//for IPv6
for (i = 0; i < RTAX_MAX; i++) {
if (rt->rtm_addrs & (1 << i)) {
sa_tab[i] = sa;
sa = (struct sockaddr_in6 *)((char *)sa + sa->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);
nativeAddr4.sin_addr.s_addr = inet_addr([ipAddr0 UTF8String]);
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)
if ((send_sock = socket(destination->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));


说法4:

果然是苹果打个哈欠,iOS行业内就得起一次风暴呀。自从5月初Apple明文规定所有开发者在6月1号以后提交新版本需要支持IPV6-Only的网络,大家便开始热火朝天的研究如何支持IPV6,以及应用中哪些模块目前不支持IPV6。

一、IPV6-Only支持是啥?

首先IPV6,是对IPV4地址空间的扩充。目前当我们用iOS设备连接上Wifi、4G、3G等网络时,设备被分配的地址均是IPV4地址,但是随着运营商和企业逐渐部署IPV6 DNS64/NAT64网络之后,设备被分配的地址会变成IPV6的地址,而这些网络就是所谓的IPV6-Only网络,并且仍然可以通过此网络去获取IPV4地址提供的内容。客户端向服务器端请求域名解析,首先通过DNS64 Server查询IPv6的地址,如果查询不到,再向DNS Server查询IPv4地址,通过DNS64 Server合成一个IPV6的地址,最终将一个IPV6的地址返回给客户端。如图所示:


NAT64-DNS64-ResolutionOfIPv4_2x.png

在Mac OS 10.11+的双网卡的Mac机器(以太网口+无线网卡),我们可以通过模拟构建这么一个local IPv6 DNS64/NAT64 的网络环境去测试应用是否支持IPV6-Only网络,大概原理如下:


local_ipv6_dns64_nat64_network_2x.png

二、Apple如何审核支持IPV6-Only?

首先第一点:这里说的支持IPV6-Only网络,其实就是说让应用在 IPv6 DNS64/NAT64 网络环境下仍然能够正常运行。但是考虑到我们目前的实际网络环境仍然是IPV4网络,所以应用需要能够同时保证IPV4和IPV6环境下的可用性。从这点来说,苹果不会去扫描IPV4的专有API来拒绝审核通过,因为IPV4的API和IPV6的API调用都会同时存在于代码中。

其次第二点:Apple官方声明iOS9开始向IPV6支持过渡,在iOS9.2+支持IPV4地址合成IPV6地址。其提供的Reachability库在iOS8系统下,当从IPV4切换到IPV6网络,或者从IPV6网络切换到IPV4,是无法监控到网络状态的变化。也有一些开发者针对这些Bug询问Apple的审核部门,给予的答复是只需要在苹果最新的系统上保证IPV6的兼容性即可

最后第三点:只要应用的主流程支持IPV6,通过苹果审核即可。对于不支持IPV6的模块,考虑到我们现实IPV6网络的部署还需要一段时间,短时间内不会影响我们用户的使用。但随着4G网络IPV6的部署,这部分模块还是需要逐渐安排人力进行支持。

三、应用如何支持IPV6-Only?

对于如何支持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,但是苹果审核并不关心。

四、底层的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(模拟器)、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) { address = @"192.168.0.1"; } .... //for IPV4 for (i = 0; i < RTAX_MAX; i++) { if (rt->rtm_addrs & (1 << i)) { sa_tab[i] = sa; sa = (struct sockaddr *)((char *)sa + ROUNDUP(sa->sa_len)); } else { sa_tab[i] = NULL; } } //for IPV6 for (i = 0; i < RTAX_MAX; i++) { if (rt->rtm_addrs & (1 << i)) { sa_tab[i] = sa; sa = (struct sockaddr_in6 *)((char *)sa + sa->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) if ((send_sock = socket(destination->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));


文/philon(简书作者)
原文链接:http://www.jianshu.com/p/a6bab07c4062#rd
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。
 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值