本文檔介紹在iOS用戶端上使用Alamofire接入HTTPDNS的方案。
1. 概述
Alamofire是iOS開發中廣泛使用的網路請求架構,提供了優雅的API和強大的功能。本文檔介紹如何在使用Alamofire的專案中整合HTTPDNS。
關於HTTPDNS的基礎知識和在iOS上使用時遇到的技術挑戰,請先參考:iOS端Native情境使用HTTPDNS。
2. 普通HTTP情境、HTTPS+非SNI情境接入方案
適用於普通HTTP或HTTPS + 非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 配置支援SNI的Session
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_demo中HttpDnsNSURLProtocolImpl.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的核心步驟:
初始化Session - 配置基礎網路參數和代理
HTTPDNS網域名稱解析 - 擷取IP地址替換網域名稱
發送網路請求 - 設定Host頭並處理HTTPS認證校正
關鍵要點
Host頭設定:確保伺服器能正確識別請求的網域名稱
認證校正:使用原始網域名稱而非IP進行認證驗證
SNI處理:複雜情境使用NSURLProtocol自動處理
降級策略:HTTPDNS解析失敗時回退到系統DNS
完整的範例程式碼請參考:HTTPDNS iOS Demo