自從5月初Apple明文規定所有開發者在6月1號以後提交新版本需要支持IPV6-Only的網絡,大家便開始熱火朝天的研究如何支持IPV6,以及應用中哪些模塊目前不支持IPV6。
首先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網絡,大概原理如下:
首先第一點:這裡說的支持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,官方給出了如下幾點標准:(這裡就不對其進行解釋了,大家看上面的參考鏈接即可)
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;
官方的這句話讓我們疑惑頓生:
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的。
目前我們的應用最低版本還需要支持iOS7,雖然蘋果只要求最新版本支持IPV6-Only,但是出於對用戶負責的態度,我們仍然需要搞清楚在低版本上URL Loading System的API是否支持IPV6.
(to fix me, make some experiments)待續~~~
我們可以查到應用中大量使用了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,所以對於IPV6支持,這塊是個重頭戲。如果你的應用中使用了長連接,其必然會使用底層socket API,這一塊也是需要支持IPV6的。 對於Socket如何同時支持IPV4和IPV6,可以參考谷歌的開源庫CocoaAsyncSocket.
下面我針對我們的開源 IOS.git" target="_blank"> 網絡診斷組件, 說一下是如何同時支持IPV4和IPV6的。
開源地址:https://github.com/Lede-Inc/LDNetDiagnoService_IOS.git
這個網絡診斷組件的主要功能如下:
之前我們都是通過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; }
相當於我們在終端中輸入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; } } }
其實是在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; } }
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]; }
在IPV4網絡下我們通過gethostname獲取,而在IPV6環境下,通過新的gethostbyname2函數獲取。
//ipv4 phot = gethostbyname(hostN); //ipv6 phot = gethostbyname2(hostN, AF_INET6);
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的處理;
其實是通過創建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));
IPv6的簡介
IPv4 和 IPv6的區別就是 IP 地址前者是 .(dot)分割,後者是以 :(冒號)分割的(更多詳細信息自行搜索)。
PS:在使用 IPv6 的熱點時候,記得手機開 飛行模式 哦,保證手機只在 Wi-Fi 下上網,以免手機在連接不到網絡時候,會默認跳轉到使用 蜂窩移動網絡(即2G、3G、4G流量) 上網。
本地 Mac 搭建 IPv6 測試環境
想要測試你的 APP 是否在 IPv6 環境下運轉是否正常,你所需要的就是一台用非Wi-Fi方式上網的Mac電腦。如果你用的是 Mac 一體機網絡用的有線,那麼你什麼也不用准備,如果你用的 Mac 本,甭管 Air 還是 Pro,只要用無線上網,你就需要一個 RJ-45 轉 USB 的轉換工具(因為 Mac 本沒有直接插有線的接口),去某狗、某貓上淘個吧,不貴也就不到100來大洋。
搭建 IPv6 測試環境說白了就是用 Mac 做一個熱點,然後用 iPhone 連接這個 Wi-Fi,聽起來很容易,下面跟著我的步伐走吧。
和正常的開啟 Mac 熱點的方式的區別是這次我們產生的是一個本地的 IPv6 DNS64/NAT64 網絡,這項功能是 OS X 10.11 新加的功能(如果你的 Mac 系統版本不是的話必須要升級哦,才能產生 IPv6 的熱點吶 )。
和我們以前開啟熱點方式不一樣的地方在於,我們在 “系統偏好設置(System Preferences)” 界面選中 “共享(Sharing)” 的同時,要按住 “Option” 鍵。見圖:
步奏1
之後在 “共享” 界面中,我們會看到和之前不一樣的地方,就是紅框所標的地方,多了一個叫 “創建 NAT64 網絡 ” 的選框,選中它。
步奏2
接下來在 共享 窗口中,依次按圖中所示的標號來,如圖所示
步奏3
隨後請點擊 共享以下來源的連接 的下拉列表,選擇我們想要共享出去的網絡接口。我當前是想要共享的是 USB 10/100/1000 LAN ,(因為的我用的是 有線的 RJ-45 接頭轉 USB 輸出的網絡轉換工具 )。
PS:如果你的 Mac 是用有線撥號上網的話,請選擇 PPOE 選項作為共享源。如果你的 Mac 是用有線上網(不用撥號的)的話,請選擇 Thunderbolt 以太網有線網 選項作為共享源。
標號1
標號2,用以下端口共享給電腦 選項此處選擇 Wi-Fi
標號3,點擊 Wi-Fi選項... 選項,個性化自己的熱點的哦
最後一步
大功告成
出現一下變化證明你已經成功產生了一個 IPv6 的熱點
Wi-Fi圖標變樣
看手機的連接共享 Wi-Fi 的變化
普通熱點共享
IPv6 熱點共享
對比2張圖中 DNS 的地址看到區別了吧,一個 . 分割,一個 : 分割。
接下來,用 IPv6 的熱點測試幾個常用的 APP,如圖:
微信
提示無法連接服務器。不過 QQ 是可以的。
提示網絡連接不可用。可能環信老版本的Demo也會有這種情況。解決辦法就去官網查閱 SDK 文檔,此處只是給出檢測 IPv6 環境下APP的連通性。