全部產品
Search
文件中心

HTTPDNS:iOS端HTTPDNS+AFNetworking最佳實務

更新時間:Oct 21, 2025

本文檔介紹在iOS用戶端上使用AFNetworking接入HTTPDNS的方案。

1. 概述

AFNetworking是iOS開發中廣泛使用的網路請求架構,提供了簡潔的API和強大的功能。本文檔介紹如何在使用AFNetworking的專案中整合HTTPDNS。

關於HTTPDNS的基礎知識和在iOS上使用時遇到的技術挑戰,請先參考:iOS端Native情境使用HTTPDNS

2. 普通HTTP情境、HTTPS+非SNI情境接入方案

適用於普通HTTP或HTTPS + 非SNI這兩種情境。

2.1 建立AFHTTPSessionManager

+ (AFHTTPSessionManager *)sharedAfnManager {
    static AFHTTPSessionManager *manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        manager = [AFHTTPSessionManager manager];
        
        // 配置安全性原則
        AFSecurityPolicy *securityPolicy = [AFSecurityPolicy defaultPolicy];
        securityPolicy.allowInvalidCertificates = NO;
        securityPolicy.validatesDomainName = YES;
        manager.securityPolicy = securityPolicy;
        
        // 配置序列化器
        manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects: 
            @"application/json", @"text/json", @"text/javascript", @"text/plain", nil];
        manager.requestSerializer.timeoutInterval = 10.0f;
    });
    return manager;
}

2.2 HTTPDNS網域名稱解析

+ (NSString *)resolveAvailableIp:(NSString *)host {
    HttpDnsService *httpDnsService = [HttpDnsService sharedInstance];
    HttpdnsResult *result = [httpDnsService resolveHostSyncNonBlocking:host 
                                                             byIpType:HttpdnsQueryIPTypeAuto];
    if (!result) return nil;
    
    if (result.hasIpv4Address) {
        return result.firstIpv4Address;
    } else if (result.hasIpv6Address) {
        return [NSString stringWithFormat:@"[%@]", result.firstIpv6Address];
    }
    return nil;
}

2.3 發送網路請求並處理認證校正

+ (void)httpDnsQueryWithURL:(NSString *)originalUrl 
          completionHandler:(void(^)(NSString *message))completionHandler {
    
    NSURL *url = [NSURL URLWithString:originalUrl];
    NSString *resolvedIpAddress = [self resolveAvailableIp:url.host];

    NSString *requestUrl = originalUrl;
    if (resolvedIpAddress) {
        // 將網域名稱替換為解析得到的IP
        requestUrl = [originalUrl stringByReplacingOccurrencesOfString:url.host 
                                                           withString:resolvedIpAddress];
        NSLog(@"HTTPDNS解析成功,網域名稱: %@, IP: %@", url.host, resolvedIpAddress);
    } else {
        NSLog(@"HTTPDNS解析失敗,使用原始URL: %@", url.host);
    }

    AFHTTPSessionManager *manager = [self sharedAfnManager];
    
    // 關鍵:設定Host頭,確保伺服器能正確識別網域名稱
    [manager.requestSerializer setValue:url.host forHTTPHeaderField:@"host"];
    
    // 關鍵:配置HTTPS認證校正,使用原始網域名稱進行驗證
    [manager setSessionDidReceiveAuthenticationChallengeBlock:
     ^NSURLSessionAuthChallengeDisposition(NSURLSession *session, 
                                          NSURLAuthenticationChallenge *challenge, 
                                          NSURLCredential **credential) {
        
        if ([challenge.protectionSpace.authenticationMethod 
             isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            
            if ([self evaluateServerTrust:challenge.protectionSpace.serverTrust 
                               forDomain:url.host]) {
                *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
                return NSURLSessionAuthChallengeUseCredential;
            }
        }
        return NSURLSessionAuthChallengePerformDefaultHandling;
    }];
    
    // 發送請求
    [manager GET:requestUrl parameters:nil headers:nil progress:nil 
         success:^(NSURLSessionDataTask *task, id responseObject) {
             NSData *data = [[NSData alloc] initWithData:responseObject];
             NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
             if (completionHandler) {
                 completionHandler([NSString stringWithFormat:@"請求成功: %@", dataStr]);
             }
         } 
         failure:^(NSURLSessionDataTask *task, NSError *error) {
             if (completionHandler) {
                 completionHandler([NSString stringWithFormat:@"請求失敗: %@", error.localizedDescription]);
             }
         }];
}

+ (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);
    
    // 評估認證信任度
    SecTrustResultType result;
    SecTrustEvaluate(serverTrust, &result);
    
    return (result == kSecTrustResultUnspecified || result == kSecTrustResultProceed);
}

3. HTTPS+SNI情境接入方案

適用於需要SNI支援的情境,如CDN或多網域名稱共用IP的情況。

3.1 配置支援SNI的AFHTTPSessionManager

+ (AFHTTPSessionManager *)sharedAfnManagerWithSNI {
    static AFHTTPSessionManager *manager = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
        
        // 關鍵:註冊自訂NSURLProtocol來處理SNI
        NSMutableArray *protocolsArray = [NSMutableArray arrayWithArray:configuration.protocolClasses];
        [protocolsArray insertObject:[HttpDnsNSURLProtocolImpl class] atIndex:0];
        [configuration setProtocolClasses:protocolsArray];
        
        manager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:configuration];
        
        // 配置安全性原則
        AFSecurityPolicy *securityPolicy = [AFSecurityPolicy defaultPolicy];
        securityPolicy.allowInvalidCertificates = NO;
        securityPolicy.validatesDomainName = YES;
        manager.securityPolicy = securityPolicy;
        
        // 配置序列化器
        manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects: 
            @"application/json", @"text/json", @"text/javascript", @"text/plain", nil];
        manager.requestSerializer.timeoutInterval = 10.0f;
    });
    return manager;
}

3.2 HTTPDNS網域名稱解析

+ (NSString *)resolveAvailableIp:(NSString *)host {
    HttpDnsService *httpDnsService = [HttpDnsService sharedInstance];
    HttpdnsResult *result = [httpDnsService resolveHostSyncNonBlocking:host 
                                                             byIpType:HttpdnsQueryIPTypeAuto];
    if (!result) return nil;
    
    if (result.hasIpv4Address) {
        return result.firstIpv4Address;
    } else if (result.hasIpv6Address) {
        return [NSString stringWithFormat:@"[%@]", result.firstIpv6Address];
    }
    return nil;
}

3.3 發送SNI情境的網路請求

+ (void)httpDnsQueryWithSNIURL:(NSString *)originalUrl 
             completionHandler:(void(^)(NSString *message))completionHandler {
    
    NSURL *url = [NSURL URLWithString:originalUrl];
    NSString *resolvedIpAddress = [self resolveAvailableIp:url.host];

    NSString *requestUrl = originalUrl;
    if (resolvedIpAddress) {
        requestUrl = [originalUrl stringByReplacingOccurrencesOfString:url.host 
                                                           withString:resolvedIpAddress];
        NSLog(@"HTTPDNS解析成功,網域名稱: %@, IP: %@", url.host, resolvedIpAddress);
    } else {
        NSLog(@"HTTPDNS解析失敗,使用原始URL: %@", url.host);
    }

    AFHTTPSessionManager *manager = [self sharedAfnManagerWithSNI];
    
    // 設定Host頭部
    [manager.requestSerializer setValue:url.host forHTTPHeaderField:@"host"];
    
    // 注意:由於使用了自訂NSURLProtocol,SNI和認證驗證已在Protocol層處理
    // 直接發送請求即可
    [manager GET:requestUrl parameters:nil headers:nil progress:nil 
         success:^(NSURLSessionDataTask *task, id responseObject) {
             NSData *data = [[NSData alloc] initWithData:responseObject];
             NSString *dataStr = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
             if (completionHandler) {
                 completionHandler([NSString stringWithFormat:@"請求成功: %@", dataStr]);
             }
         } 
         failure:^(NSURLSessionDataTask *task, NSError *error) {
             if (completionHandler) {
                 completionHandler([NSString stringWithFormat:@"請求失敗: %@", error.localizedDescription]);
             }
         }];
}

說明:SNI情境下,認證校正和網域名稱處理由NSURLProtocol層自動處理,無需額外配置認證校正回調。

如果需要參考樣本,阿里雲提供了httpdns_ios_demoHttpDnsNSURLProtocolImpl.m的樣本實現,可根據業務需求進行修改或複用。

4. 使用樣本

4.1 基礎HTTPS請求

// 普通HTTP請求(非SNI情境)
[AFNHttpsScenario httpDnsQueryWithURL:@"http://example.com/api/data" 
                    completionHandler:^(NSString *message) {
    NSLog(@"請求結果: %@", message);
}];

4.2 SNI情境請求

// SNI情境請求
[AFNHttpsWithSNIScenario httpDnsQueryWithSNIURL:@"https://example.com/api/data" 
                               completionHandler:^(NSString *message) {
    NSLog(@"請求結果: %@", message);
}];

5. 總結

AFNetworking整合HTTPDNS的核心步驟:

  1. 初始化AFHTTPSessionManager - 配置基礎網路參數

  2. HTTPDNS網域名稱解析 - 擷取IP地址替換網域名稱

  3. 發送網路請求 - 設定Host頭並處理HTTPS認證校正

關鍵要點

  • Host頭設定:確保伺服器能正確識別請求的網域名稱

  • 認證校正:使用原始網域名稱而非IP進行認證驗證

  • SNI處理:複雜情境使用NSURLProtocol自動處理

  • 降級策略:HTTPDNS解析失敗時回退到系統DNS

完整的範例程式碼請參考:HTTPDNS iOS Demo