iOS端HTTPDNS+AFNetworking最佳实践

本文档介绍在iOS客户端上使用AFNetworking接入HTTPDNS的方案。

1. 概述

AFNetworkingiOS开发中广泛使用的网络请求框架,提供了简洁的API和强大的功能。本文档介绍如何在使用AFNetworking的项目中集成HTTPDNS。

关于HTTPDNS的基础知识和在iOS上使用时遇到的技术挑战,请先参考:iOSNative场景使用HTTPDNS

2. 普通HTTP场景、HTTPS+非SNI场景接入方案

适用于普通HTTPHTTPS + 非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 配置支持SNIAFHTTPSessionManager

+ (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