iOS端HTTPDNS+Alamofire最佳实践

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

1. 概述

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

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

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

适用于普通HTTPHTTPS + 非SNI这两种场景。

2.1 创建自定义Session

import Alamofire
import AlicloudHttpDNS

class AlamofireHttpsScenario {
    
    static let sharedSession: Session = {
        return Session(delegate: CustomerSessionDelegate())
    }()
}

2.2 HTTPDNS域名解析

class func resolveAvailableIp(host: String) -> String? {
    let httpDnsService = HttpDnsService.sharedInstance()
    let result = httpDnsService.resolveHostSyncNonBlocking(host, by: .auto)
    
    print("resolve host result: \(String(describing: result))")
    if result == nil {
        return nil
    }
    
    if result!.hasIpv4Address() {
        return result?.firstIpv4Address()
    } else if result!.hasIpv6Address() {
        return "[\(result!.firstIpv6Address())]"
    }
    return nil
}

2.3 发送网络请求并处理证书校验

class func httpDnsQueryWithURL(originalUrl: String, completionHandler: @escaping (_ message: String) -> Void) {
    var tipsMessage: String = ""guard let url = NSURL(string: originalUrl), let originalHost = url.host else {
        print("Error: invalid url: \(originalUrl)")
        return
    }
    
    let resolvedIpAddress = resolveAvailableIp(host: originalHost)
    
    var requestUrl = originalUrl
    if resolvedIpAddress != nil {
        // 将域名替换为解析得到的IP
        requestUrl = requestUrl.replacingOccurrences(of: originalHost, with: resolvedIpAddress!)
        
        let log = "Resolve host(\(originalHost)) by HTTPDNS successfully, result ip: \(resolvedIpAddress!)"print(log)
        tipsMessage = log
    } else {
        let log = "Resolve host(\(originalHost) by HTTPDNS failed, keep original url to request"print(log)
        tipsMessage = log
    }
    
    // 发送网络请求
    sendRequestWithURL(requestUrl: requestUrl, host: originalHost) { message in
        tipsMessage = tipsMessage + "\n\n" + message
        completionHandler(tipsMessage)
    }
}

class func sendRequestWithURL(requestUrl: String, host: String, completionHandler: @escaping (_ message: String) -> Void) {
    // 关键:设置Host头,确保服务器能正确识别域名var header = HTTPHeaders()
    header.add(name: "host", value: host)
    
    sharedSession.request(requestUrl, method: .get, encoding: URLEncoding.default, headers: header)
        .validate()
        .response { response invar responseStr = ""switch response.result {
            case .success(let data):
                if let data = data, !data.isEmpty {
                    let dataStr = String(data: data, encoding: .utf8) ?? ""
                    responseStr = "HTTP Response: \(dataStr)"
                } else {
                    responseStr = "HTTP Response: [Empty Data]"
                }
            case .failure(let error):
                responseStr = "HTTP request failed with error: \(error.localizedDescription)"
            }
            
            completionHandler(responseStr)
        }
}

// 自定义SessionDelegate处理证书校验
class CustomerSessionDelegate: SessionDelegate {
    
    override func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
        
        var disposition: URLSession.AuthChallengeDisposition = .performDefaultHandling
        var credential: URLCredential?
        
        let request = task.currentRequest
        let host = request?.value(forHTTPHeaderField: "host") ?? ""// 关键:使用原始域名进行证书校验if !host.isEmpty {
            if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
                if evaluate(serverTrust: challenge.protectionSpace.serverTrust, host: host) {
                    disposition = .useCredential
                    credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
                } else {
                    disposition = .performDefaultHandling
                }
            } else {
                disposition = .performDefaultHandling
            }
        }
        completionHandler(disposition, credential)
    }
    
    func evaluate(serverTrust: SecTrust?, host: String?) -> Bool {
        guard let serverTrust = serverTrust else {
            return false
        }
        
        // 创建证书校验策略var policies = [SecPolicy]()
        if let host = host {
            policies.append(SecPolicyCreateSSL(true, host as CFString))
        } else {
            policies.append(SecPolicyCreateBasicX509())
        }
        
        // 绑定校验策略到服务端证书SecTrustSetPolicies(serverTrust, policies as CFTypeRef)
        
        // 评估证书信任度var result: SecTrustResultType = .invalid
        if SecTrustEvaluate(serverTrust, &result) == errSecSuccess {
            return result == .unspecified || result == .proceed
        } else {
            return false
        }
    }
}

3. 方案二:HTTPS + SNI场景

适用于需要SNI支持的场景,如CDN或多域名共享IP的情况。

3.1 配置支持SNISession

import Alamofire
import AlicloudHttpDNS

class AlamofireHttpsWithSNIScenario {
    
    static let sharedSession: Session = {
        let configuration = URLSessionConfiguration.af.default
        // 关键:注册自定义NSURLProtocol来处理SNI
        configuration.protocolClasses = [HttpDnsNSURLProtocolImpl.classForCoder()]
        return Session(configuration: configuration)
    }()
}

3.2 HTTPDNS域名解析

class func resolveAvailableIp(host: String) -> String? {
    let httpDnsService = HttpDnsService.sharedInstance()
    let result = httpDnsService.resolveHostSyncNonBlocking(host, by: .auto)
    
    print("resolve host result: \(String(describing: result))")
    if result == nil {
        return nil
    }
    
    if result!.hasIpv4Address() {
        return result?.firstIpv4Address()
    } else if result!.hasIpv6Address() {
        return "[\(result!.firstIpv6Address())]"
    }
    return nil
}

3.3 发送SNI场景的网络请求

class func httpDnsQueryWithURL(originalUrl: String, completionHandler: @escaping (_ message: String) -> Void) {
    var tipsMessage: String = ""guard let url = NSURL(string: originalUrl), let originalHost = url.host else {
        print("Error: invalid url: \(originalUrl)")
        return
    }
    
    let resolvedIpAddress = resolveAvailableIp(host: originalHost)
    
    var requestUrl = originalUrl
    if resolvedIpAddress != nil {
        requestUrl = requestUrl.replacingOccurrences(of: originalHost, with: resolvedIpAddress!)
        
        let log = "Resolve host(\(originalHost)) by HTTPDNS successfully, result ip: \(resolvedIpAddress!)"print(log)
        tipsMessage = log
    } else {
        let log = "Resolve host(\(originalHost) by HTTPDNS failed, keep original url to request"print(log)
        tipsMessage = log
    }
    
    // 发送网络请求
    sendRequestWithURL(requestUrl: requestUrl, host: originalHost) { message in
        tipsMessage = tipsMessage + "\n\n" + message
        completionHandler(tipsMessage)
    }
}

class func sendRequestWithURL(requestUrl: String, host: String, completionHandler: @escaping (_ message: String) -> Void) {
    // 设置Host头部var header = HTTPHeaders()
    header.add(name: "host", value: host)
    
    // 注意:由于使用了自定义NSURLProtocol,SNI和证书验证已在Protocol层处理// 直接发送请求即可
    sharedSession.request(requestUrl, method: .get, encoding: URLEncoding.default, headers: header)
        .validate()
        .response { response invar responseStr = ""switch response.result {
            case .success(let data):
                if let data = data, !data.isEmpty {
                    let dataStr = String(data: data, encoding: .utf8) ?? ""
                    responseStr = "HTTP Response: \(dataStr)"
                } else {
                    responseStr = "HTTP Response: [Empty Data]"
                }
            case .failure(let error):
                responseStr = "HTTP request failed with error: \(error.localizedDescription)"
            }
            
            completionHandler(responseStr)
        }
}

说明:SNI场景下,证书校验和域名处理由NSURLProtocol层自动处理,无需额外配置证书校验回调。

如果需要参考示例,阿里云提供了httpdns_ios_demoHttpDnsNSURLProtocolImpl.m的示例实现,可根据业务需求进行修改或复用。

4. 使用示例

4.1 基础HTTPS请求

// 普通HTTPS请求(非SNI场景)
AlamofireHttpsScenario.httpDnsQueryWithURL(originalUrl: "https://example.com/api/data") { 
  message inprint("请求结果: \(message)")
}

4.2 SNI场景请求

// SNI场景请求(如CDN)
AlamofireHttpsWithSNIScenario.httpDnsQueryWithURL(originalUrl: "https://cdn.example.com/api/data") { 
  message inprint("请求结果: \(message)")
}

5. 总结

Alamofire集成HTTPDNS的核心步骤:

  1. 初始化Session - 配置基础网络参数和代理

  2. HTTPDNS域名解析 - 获取IP地址替换域名

  3. 发送网络请求 - 设置Host头并处理HTTPS证书校验

关键要点

  • Host头设置:确保服务器能正确识别请求的域名

  • 证书校验:使用原始域名而非IP进行证书验证

  • SNI处理:复杂场景使用NSURLProtocol自动处理

  • 降级策略:HTTPDNS解析失败时回退到系统DNS

完整的示例代码请参考:HTTPDNS iOS Demo