標籤:
原文串連 果然是蘋果打個哈欠,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調用都會同時存在於代碼中(
不過為了減小審核被拒風險,建議將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及以下不能正常訪問;這一點蘋果的解釋和建議是這樣的:
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.
三、應用如何支援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.mstruct 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,可以參考Google的開源庫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地址擷取支援IPV6IPV4時只需要通過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函數擷取。
//ipv4phot = gethostbyname(hostN);//ipv6 phot = gethostbyname2(hostN, AF_INET6);
4.5 ping方案支援IPV6Apple的官方提供了最新的支援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];//建立socketif ((recv_sock = socket(destination->sa_family, SOCK_DGRAM, isIPV6?IPPROTO_ICMPV6:IPPROTO_ICMP)) < 0 xss=removed>sa_family, SOCK_DGRAM, 0)) < 0)//設定sender 通訊端的ttlif ((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));
iOS應用支援IPV6,就那點事兒