背景#
最近開發水印相機,遇到了用戶網絡正常,但是上傳超時、上傳失敗的問題。通過聽雲後台看到接口錯誤記錄中,用戶的 localDNS 為空,於是就有了接入 HTTPDNS 的需求。
實踐#
由於項目中網絡請求使用的 AFNetworking 框架,接入第三方 HTTPDNS 後,需要修改 AFNetworking 中的內容,才能讓請求走 IP。
大致流程是接入 SDK——> 註冊 SDK——> 獲取 IP——> 存儲 ——> 使用。這裡可依據個人情況,在啟動時進行 SDK 註冊,獲取 IP 有兩種方式,一是只在 APP 啟動時獲取一次,然後存儲起來,APP 使用過程中不需要更新。二是在 每次某個接口使用時都獲取。
下面詳細來看看接入的過程
阿里 HTTPDNS#
- 按照快速入門中的步驟進行配置
- 添加域名,注意阿里的添加域名,可以添加全匹配和二級域名的方式
- 參考iOS SDK 接入進行接入
- 使用 CocoaPods 接入
這裡到要罵人的地方了,按照阿里自己的官方文檔上面寫的 CocoaPod 安裝的 SDK 不是最新的source 'https://github.com/CocoaPods/Specs.git' source 'https://github.com/aliyun/aliyun-specs.git' pod 'AlicloudHTTPDNS' # 注意,不能按照官方文檔上的寫法,後面不要加指定版本
- 項目中使用
- (void)registerAliDNS{ HttpDnsService *httpdns = [[HttpDnsService alloc]initWithAccountID:@"Your Account ID" secretKey:@"Your Secret Key"]; [httpdns setCachedIPEnabled:NO]; // [httpdns cleanHostCache:nil]; [httpdns setHTTPSRequestEnabled:YES]; [httpdns setPreResolveHosts:@[ @"baidu.com", ]]; [httpdns setLogEnabled:YES]; [httpdns setPreResolveAfterNetworkChanged:YES]; // [httpdns setExpiredIPEnabled:YES]; // [httpdns setDelegateForDegradationFilter:self]; self.httpdns = httpdns; NSUInteger delaySeconds = 1; dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delaySeconds * NSEC_PER_SEC)); dispatch_after(when, dispatch_get_main_queue(), ^{ [self getUpFileHostIp]; }); } - (void)getUpFileHostIp { NSString *originalUrl = @"https://www.baidu.com"; NSURL *url = [NSURL URLWithString:originalUrl]; NSArray *ipsArray = [self.httpdns getIpsByHostAsync:url.host]; if (!IsNilArray(ipsArray)) { self.hostIpStr = ipsArray.firstObject; } }
- 使用 CocoaPods 接入
騰訊雲 HTTPDNS#
- 按照入門指南中的步驟進行配置
- 註冊 / 登錄賬號
- 開通服務
- 在開發配置中申請應用
- 在域名管理中添加域名,注意添加騰訊設置域名,只能添加 xxx.com,不能添加 xxx.yyy.com 這種
- 參考iOS SDK 文檔進行接入
- 使用 CocoaPods 接入
pod 'MSDKDns'
- 項目中使用
#import <MSDKDns/MSDKDns.h> // 騰訊 HTTP DNS - (void)registerMSDKDns { // 注意下面的使用方法,採用的是字典的方法來初始化,因為使用類的方法編譯不通過。。。。 [[MSDKDns sharedInstance] initConfigWithDictionary:@{ @"dnsIp": @"119.29.29.98", @"dnsId": @"Your AppID", @"dnsKey": @"Your SecretKey", // 注意不同加密方式的 SecretKey 不同 @"encryptType": @0, // 加密方式 @"debug": @1, // @"routeIp": @"", }]; NSString *hostStr = @"baidu.com"; [[MSDKDns sharedInstance] WGGetHostByNameAsync:hostStr returnIps:^(NSArray *ipsArray) { NSLog(@"解析 IP:%@", ipsArray); if (ipsArray) { self.hostIpStr = ipsArray.firstObject; } }]; }
- 使用 CocoaPods 接入
使用#
檢測本地是否使用 HTTP、HTTPS 的代理,如果有代理,建議不要使用 HTTPDNS —— iOS SDK 文檔
// 檢測是否有 http 代理
- (BOOL)isUseHTTPProxy {
CFDictionaryRef dicRef = CFNetworkCopySystemProxySettings();
const CFStringRef proxyCFstr = (const CFStringRef)CFDictionaryGetValue(dicRef, (const void*)kCFNetworkProxiesHTTPProxy);
NSString *proxy = (__bridge NSString *)proxyCFstr;
if (proxy) {
return YES;
} else {
return NO;
}
}
// 檢測是否有 https 代理
- (BOOL)isUseHTTPSProxy {
CFDictionaryRef dicRef = CFNetworkCopySystemProxySettings();
const CFStringRef proxyCFstr = (const CFStringRef)CFDictionaryGetValue(dicRef, (const void*)kCFNetworkProxiesHTTPSProxy);
NSString *proxy = (__bridge NSString *)proxyCFstr;
if (proxy) {
return YES;
} else {
return NO;
}
}
用 HTTPDNS 返回的 IP 替換掉 URL 中的域名,指定下 HTTP 頭的 host 字段。
NSURL *httpDnsURL = [NSURL URLWithString:@"使用解析結果ip拼接的URL"];
float timeOut = 設置的超時時間;
NSMutableURLRequest *mutableReq = [NSMutableURLRequest requestWithURL:httpDnsURL cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval: timeOut];
[mutableReq setValue:@"原域名" forHTTPHeaderField:@"host"];
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue currentQueue]];
NSURLSessionTask *task = [session dataTaskWithRequest:mutableReq];
[task resume];
項目中使用的是 AFNetworking,修改 AFNetworking 中 AFURLSessionMananger.m
類:
- 添加
evaluateServerTrust:forDmain:
方法 - 修改
URLSession:task:didReceiveChallenge:completionHandler:
方法
- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain {
//創建證書校驗策略
NSMutableArray *policies = [NSMutableArray array];
if (domain) {
[policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
} else {
[policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
}
//綁定校驗策略到服務端的證書上
SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
//評估當前 serverTrust 是否可信任,
//官方建議在 result = kSecTrustResultUnspecified 或 kSecTrustResultProceed 的情況下 serverTrust 可以被驗證通過,
//https://developer.apple.com/library/ios/technotes/tn2232/_index.html
//關於SecTrustResultType的詳細信息請參考SecTrust.h
SecTrustResultType result;
SecTrustEvaluate(serverTrust, &result);
return (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
BOOL evaluateServerTrust = NO;
NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
NSURLCredential *credential = nil;
if (self.authenticationChallengeHandler) {
id result = self.authenticationChallengeHandler(session, task, challenge, completionHandler);
if (result == nil) {
return;
} else if ([result isKindOfClass:NSError.class]) {
objc_setAssociatedObject(task, AuthenticationChallengeErrorKey, result, OBJC_ASSOCIATION_RETAIN);
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
} else if ([result isKindOfClass:NSURLCredential.class]) {
credential = result;
disposition = NSURLSessionAuthChallengeUseCredential;
} else if ([result isKindOfClass:NSNumber.class]) {
disposition = [result integerValue];
NSAssert(disposition == NSURLSessionAuthChallengePerformDefaultHandling || disposition == NSURLSessionAuthChallengeCancelAuthenticationChallenge || disposition == NSURLSessionAuthChallengeRejectProtectionSpace, @"");
evaluateServerTrust = disposition == NSURLSessionAuthChallengePerformDefaultHandling && [challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
} else {
@throw [NSException exceptionWithName:@"Invalid Return Value" reason:@"The return value from the authentication challenge handler must be nil, an NSError, an NSURLCredential or an NSNumber." userInfo:nil];
}
} else {
evaluateServerTrust = [challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust];
}
if (evaluateServerTrust) {
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
} else {
objc_setAssociatedObject(task, AuthenticationChallengeErrorKey,
[self serverTrustErrorForServerTrust:challenge.protectionSpace.serverTrust url:task.currentRequest.URL],
OBJC_ASSOCIATION_RETAIN);
disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
}
}
// 修改----------------------------部分
//獲取原始域名信息
NSURLRequest *request = task.currentRequest;
NSString *host = [[request allHTTPHeaderFields] objectForKey:@"host"];
if (!host) {
host = challenge.protectionSpace.host;
}
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:host]) {
disposition = NSURLSessionAuthChallengeUseCredential;
credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
}
}
if (completionHandler) {
completionHandler(disposition, credential);
}
}
測試#
按照上面的接入步驟接入完成後進行測試,上傳圖片出現了:首次失敗、再次成功的情況,失敗的原因是,疑似不受信任服務器 xxx.xx.xxx.xx,還需要再修改AFSecurityPolicy
類
+ (instancetype)defaultPolicy {
AFSecurityPolicy *securityPolicy = [[self alloc] init];
securityPolicy.SSLPinningMode = AFSSLPinningModeNone;
// 添加下面兩行代碼
securityPolicy.allowInvalidCertificates = YES;
securityPolicy.validatesDomainName = NO;
return securityPolicy;
}
加入後再次測試,完美運行
總結#
- 接入前,先考慮考慮要接入的方式,即:刷新 HTTPDNS 的邏輯
- 接入時:
- 阿里 HTTPDNS 需要注意 Pod 中依賴不要按照官方文檔設置版本;
- 騰訊的 HTTPDNS 則要注意初始化的方法;
- 注意兩家平台上設置域名時的不同;
- 接入後:
- 修改 AFNetworking 中
AFURLSessionMananger.m
類 - 修改
AFSecurityPolicy
類
- 修改 AFNetworking 中