Swift 使用 CFHostCreateWithName 实现 DNS 查询
最近在调查一个 WKWebView 报错找不到 domain 的问题,于是就需要在报错找不到域名的时候手动通过 CFHostCreateWithName 去查询一下 DNS 是否正常能够解析。
import Foundation
public final class HostAddressQuery {
public let name: String
private var host: CFHost
weak var delegate: HostAddressQueryDelegate?
private var targetRunLoop: RunLoop?
init(domainName: String, delegate: HostAddressQueryDelegate? = nil) {
self.delegate = delegate
name = domainName
host = CFHostCreateWithName(nil, name as CFString).takeRetainedValue()
}
}
public protocol HostAddressQueryDelegate: AnyObject {
/// Called when the query completes successfully.
///
/// This is called on the same thread that called `start()`.
///
/// - Parameters:
/// - addresses: The addresses for the DNS name. This has some important properties:
/// - It will not be empty.
/// - Each element is a `Data` value that contains some flavour of `sockaddr`
/// - It can contain any combination of IPv4 and IPv6 addresses
/// - The addresses are sorted, with the most preferred first
/// - query: The query that completed.
func didComplete(addresses: [Data], hostAddressQuery query: HostAddressQuery)
/// Called when the query completes with an error.
///
/// This is called on the same thread that called `start()`.
///
/// - Parameters:
/// - error: An error describing the failure.
/// - query: The query that completed.
///
/// - Important: In most cases the error will be in domain `kCFErrorDomainCFNetwork`
/// with a code of `kCFHostErrorUnknown` (aka `CFNetworkErrors.cfHostErrorUnknown`),
/// and the user info dictionary will contain an element with the `kCFGetAddrInfoFailureKey`
/// key whose value is an NSNumber containing an `EAI_XXX` value (from `<netdb.h>`).
func didComplete(error: Error, hostAddressQuery query: HostAddressQuery)
}
我们先的定义一个 HostAddressQuery 类其中包含待查询 hostname 的 name 成员变量、CFHost 类型的 host 变量、HostAddressQueryDelegate 协议类型的 delegate 变量和一个 targetRunLoop 变量,以及一个 HostAddressQueryDelegate 协议,在 HostAddressQueryDelegate 协议中定义 didComplete 用于回调 DNS 查询成功的结果,didComplete 则回调查询失败并返回相应的错误。
public final class HostAddressQuery {
public let name: String
private var host: CFHost
weak var delegate: HostAddressQueryDelegate?
private var targetRunLoop: RunLoop?
……
private func stop(error: Error?, notify: Bool) {
precondition(RunLoop.current == self.targetRunLoop)
self.targetRunLoop = nil
CFHostSetClient(self.host, nil, nil)
CFHostUnscheduleFromRunLoop(self.host, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue)
CFHostCancelInfoResolution(self.host, .addresses)
Unmanaged.passUnretained(self).release()
if notify {
if let error = error {
self.delegate?.didComplete(error: error, hostAddressQuery: self)
} else {
let addresses = CFHostGetAddressing(self.host, nil)!.takeUnretainedValue() as NSArray as! [Data]
self.delegate?.didComplete(addresses: addresses, hostAddressQuery: self)
}
}
}
}
……
首先实现一个统一的错误回调函数 stop ,如果发生请求异常需要中断查询时统一调用 stop 函数,函数有两个参数,其中 error 是可选的 Error 错误对象、notify 则标识是否需要回调到 delegate 协议中。
public final class HostAddressQuery {
public let name: String
private var host: CFHost
weak var delegate: HostAddressQueryDelegate?
private var targetRunLoop: RunLoop?
……
public func start() {
precondition(self.targetRunLoop == nil)
self.targetRunLoop = RunLoop.current
var context = CFHostClientContext()
context.info = Unmanaged.passRetained(self).toOpaque()
var success = CFHostSetClient(self.host, { (_: CFHost, _: CFHostInfoType, _ streamErrorPtr: UnsafePointer<CFStreamError>?, _ info: UnsafeMutableRawPointer?) in
let obj = Unmanaged<HostAddressQuery>.fromOpaque(info!).takeUnretainedValue()
if let streamError = streamErrorPtr?.pointee, streamError.domain != 0 || streamError.error != 0 {
obj.stop(streamError: streamError, notify: true)
} else {
obj.stop(streamError: nil, notify: true)
}
}, &context)
CFHostScheduleWithRunLoop(self.host, CFRunLoopGetCurrent(), CFRunLoopMode.defaultMode.rawValue)
var streamError = CFStreamError()
success = CFHostStartInfoResolution(self.host, .addresses, &streamError)
if !success {
self.stop(streamError: streamError, notify: true)
}
}
private func stop(streamError: CFStreamError?, notify: Bool) {
let error: Error?
if let streamError = streamError {
// Convert a CFStreamError to a NSError. This is less than ideal because I only handle
// a limited number of error domains. Wouldn't it be nice if there was a public API to
// do this mapping <rdar://problem/5845848> or a CFHost API that used CFError
// <rdar://problem/6016542>.
switch streamError.domain {
case CFStreamErrorDomain.POSIX.rawValue:
error = NSError(domain: NSPOSIXErrorDomain, code: Int(streamError.error))
case CFStreamErrorDomain.macOSStatus.rawValue:
error = NSError(domain: NSOSStatusErrorDomain, code: Int(streamError.error))
case Int(kCFStreamErrorDomainNetServices):
error = NSError(domain: kCFErrorDomainCFNetwork as String, code: Int(streamError.error))
case Int(kCFStreamErrorDomainNetDB):
error = NSError(domain: kCFErrorDomainCFNetwork as String, code: Int(CFNetworkErrors.cfHostErrorUnknown.rawValue), userInfo: [
kCFGetAddrInfoFailureKey as String: streamError.error as NSNumber
])
default:
// If it's something we don't understand, we just assume it comes from
// CFNetwork.
error = NSError(domain: kCFErrorDomainCFNetwork as String, code: Int(streamError.error))
}
} else {
error = nil
}
self.stop(error: error, notify: notify)
}
……
}
……
现在,我们实现一下 start 函数,在函数中首先构造了一个 CFHostClientContext 上下文并将 self 保存至 context.info 中,随后调用 CFHostSetClient 异步查询 DNS 地址,在匿名回调函数中会返回 CFStreamError 类型的错误,所以我们增加了一个支持 CFStreamError 错误类型的解析 stop 函数用于转换处理错误类型。异步查询成功后将调用我们刚开始定义的 stop 函数,并在函数中通过解析 CFHost 对象来获取最终 DNS 的 ip 地址查询结果。
public final class HostAddressQuery {
……
public func cancel() {
if self.targetRunLoop != nil {
self.stop(error: NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError), notify: false)
}
}
……
}
……
最后,我们增加一个手动取消查询的函数,通过手动调用 self.stop 并回调手动取消错误给 delegate 实现。
import HostAddressQuery
final class HostAddressQueryDemo {
var tmpDelegate: HostAddressQueryDelegate?
func testExample() throws {
tmpDelegate = QueryDelegate(handling: { err, ip, _ in
if let err {
print("dnsQuery failed", err.localizedDescription)
return
}
print("dnsQuery ip:", ip)
})
let dnsQuery = HostAddressQuery(domainName: "bin.zmide.com", delegate: tmpDelegate)
dnsQuery.start()
}
class QueryDelegate: HostAddressQueryDelegate {
let handling: (Error?, [String]?, HostAddressQuery?) -> Void
public init(handling: @escaping (Error?, [String]?, HostAddressQuery?) -> Void) {
self.handling = handling
}
func numeric(for address: Data) -> String {
var name = [CChar](repeating: 0, count: Int(NI_MAXHOST))
let saLen = socklen_t(address.count)
let success = address.withUnsafeBytes { (sa: UnsafePointer<sockaddr>) in
getnameinfo(sa, saLen, &name, socklen_t(name.count), nil, 0, NI_NUMERICHOST | NI_NUMERICSERV) == 0
}
guard success else {
return "?"
}
return String(cString: name)
}
func didComplete(error: any Error, hostAddressQuery: HostAddressQuery) {
handling(error, nil, hostAddressQuery)
}
func didComplete(addresses: [Data], hostAddressQuery: HostAddressQuery) {
let addressList = addresses.map { self.numeric(for: $0) }
handling(nil, addressList, hostAddressQuery)
}
}
}
最终实现 HostAddressQueryDelegate 协议,需要注意的是 didComplete 中回调回来的 addresses 是 Data 数组,需要将 Data 转为 String 这里我利用了 numeric 函数来做这个事情。
另外,构造 HostAddressQuery 对象随后调用 start 函数即可完成 DNS IP 查询。
相关代码我都提交到代码仓库了 https://github.com/PBK-B/ios-sample-project/tree/master/swift-dns-query