最近在调查一个 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