Accepted Reply
No it doesn’t. The Bonjour service name and the local DNS name do not need to be related and, even when they are, it’s common for them to be significantly different. For some fun examples of this, see this post.Just adding .local to the host name solved the problem.
The only way to get the DNS name of a service is to resolve it.
Hmmm, I can’t spot the error. Rather than spend time debugging that I created a small test program that illustrates one way to do this. If you paste the code below into a new command-line tool project, it should be able to resolve any service you give it.But it still freezing my app just after service found
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
Code Block import Foundation final class BonjourResolver: NSObject, NetServiceDelegate { typealias CompletionHandler = (Result<(String, Int), Error>) -> Void @discardableResult static func resolve(service: NetService, completionHandler: @escaping CompletionHandler) -> BonjourResolver { precondition(Thread.isMainThread) let resolver = BonjourResolver(service: service, completionHandler: completionHandler) resolver.start() return resolver } private init(service: NetService, completionHandler: @escaping CompletionHandler) { // We want our own copy of the service because we’re going to set a // delegate on it but `NetService` does not conform to `NSCopying` so // instead we create a copy by copying each property. let copy = NetService(domain: service.domain, type: service.type, name: service.name) self.service = copy self.completionHandler = completionHandler } deinit { // If these fire the last reference to us was released while the resolve // was still in flight. That should never happen because we retain // ourselves on `start`. assert(self.service == nil) assert(self.completionHandler == nil) assert(self.selfRetain == nil) } private var service: NetService? = nil private var completionHandler: (CompletionHandler)? = nil private var selfRetain: BonjourResolver? = nil private func start() { precondition(Thread.isMainThread) guard let service = self.service else { fatalError() } service.delegate = self service.resolve(withTimeout: 5.0) // Form a temporary retain loop to prevent us from being deinitialised // while the resolve is in flight. We break this loop in `stop(with:)`. selfRetain = self } func stop() { self.stop(with: .failure(CocoaError(.userCancelled))) } private func stop(with result: Result<(String, Int), Error>) { precondition(Thread.isMainThread) self.service?.delegate = nil self.service?.stop() self.service = nil let completionHandler = self.completionHandler self.completionHandler = nil completionHandler?(result) selfRetain = nil } func netServiceDidResolveAddress(_ sender: NetService) { let hostName = sender.hostName! let port = sender.port self.stop(with: .success((hostName, port))) } func netService(_ sender: NetService, didNotResolve errorDict: [String: NSNumber]) { let code = (errorDict[NetService.errorCode]?.intValue) .flatMap { NetService.ErrorCode.init(rawValue: $0) } ?? .unknownError let error = NSError(domain: NetService.errorDomain, code: code.rawValue, userInfo: nil) self.stop(with: .failure(error)) } } func main() { let service = NetService(domain: "local.", type: "_ssh._tcp", name: "Fluffy") print("will resolve, service: \(service)") BonjourResolver.resolve(service: service) { result in switch result { case .success(let hostName): print("did resolve, host: \(hostName)") exit(EXIT_SUCCESS) case .failure(let error): print("did not resolve, error: \(error)") exit(EXIT_FAILURE) } } RunLoop.current.run() } main()
-
For the DNS-SD version of this code, see this post.
Replies
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
Yeah, that’s a missing link in our Bonjour story right now. Apple plaforms support a number of Bonjour APIs, including:I'm planning to send http requests.
<dns_sd.h>
Network framework
CFNetService
NSNetService
Except for this specific case )-: The issue here is that Network framework does not support the Bonjour resolve operation, and you need that in order to build a URL from a Bonjour service (r. 73266838). This means that you must use one of the other APIs for that task, and the easiest API for that is NSNetService.
Unfortunately this puts you in a bit a quandary. Ideally you’d want to use Network framework (specifically NWBrowser) to do the Bonjour browse operation, but there’s a bit of an impedance mismatch between it and NSNetService. OTOH, using NSNetServiceBrowser for the browse operation means that all your code is… well… not quite deprecated.
Sorry I don’t have a better answer for you here.
Oh, one last thing. When you’re done with the resolve operation don’t build the URL with IP addresses based on the addresses property. Rather, build a URL with a DNS name based on the hostName property. This is easier and it’ll work better in various edge cases.
Oh, and another last thing (-: NSNetService relies on the run loop, so you need to make sure you use it on a thread that runs its run loop. A good option here is the main thread. A bad option is some code running on a Dispatch queue, because Dispatch worker threads don’t run their run loop.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
Code Block swift class ServiceAgent : NSObject, NetServiceDelegate { func netServiceDidResolveAddress(_ sender: NetService) { if let data = sender.txtRecordData() { let dict = NetService.dictionary(fromTXTRecord: data) } } } class BrowserAgent : NSObject, NetServiceBrowserDelegate { var currentService:NetService? let serviceAgent = ServiceAgent() func netServiceBrowser(_ browser: NetServiceBrowser, didFindDomain domainString: String, moreComing: Bool) { print("domain found: \(domainString)") } func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) { Lamp.lampHost = service.name+".local" self.currentService = service service.delegate = self.serviceAgent service.resolve(withTimeout: 5) } } let agent = BrowserAgent() let browser = NetServiceBrowser() browser.stop() browser.delegate = agent browser.schedule(in: RunLoop.current, forMode: .default) browser.searchForServices(ofType: "_http._tcp", inDomain: "local.") RunLoop.main.run()
But so far this code after finding my host name just freezing my app forever.
Code Block language class BrowserAgent : NSObject, NetServiceBrowserDelegate { var currentService:NetService? func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) { Lamp.lampHost = service.name+".local" } } .... let agent = BrowserAgent() let browser = NetServiceBrowser() browser.stop() browser.delegate = agent browser.schedule(in: RunLoop.main, forMode: .default) browser.searchForServices(ofType: "_http._tcp", inDomain: "local.") RunLoop.main.run()
But it still freezing my app just after service found
No it doesn’t. The Bonjour service name and the local DNS name do not need to be related and, even when they are, it’s common for them to be significantly different. For some fun examples of this, see this post.Just adding .local to the host name solved the problem.
The only way to get the DNS name of a service is to resolve it.
Hmmm, I can’t spot the error. Rather than spend time debugging that I created a small test program that illustrates one way to do this. If you paste the code below into a new command-line tool project, it should be able to resolve any service you give it.But it still freezing my app just after service found
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
Code Block import Foundation final class BonjourResolver: NSObject, NetServiceDelegate { typealias CompletionHandler = (Result<(String, Int), Error>) -> Void @discardableResult static func resolve(service: NetService, completionHandler: @escaping CompletionHandler) -> BonjourResolver { precondition(Thread.isMainThread) let resolver = BonjourResolver(service: service, completionHandler: completionHandler) resolver.start() return resolver } private init(service: NetService, completionHandler: @escaping CompletionHandler) { // We want our own copy of the service because we’re going to set a // delegate on it but `NetService` does not conform to `NSCopying` so // instead we create a copy by copying each property. let copy = NetService(domain: service.domain, type: service.type, name: service.name) self.service = copy self.completionHandler = completionHandler } deinit { // If these fire the last reference to us was released while the resolve // was still in flight. That should never happen because we retain // ourselves on `start`. assert(self.service == nil) assert(self.completionHandler == nil) assert(self.selfRetain == nil) } private var service: NetService? = nil private var completionHandler: (CompletionHandler)? = nil private var selfRetain: BonjourResolver? = nil private func start() { precondition(Thread.isMainThread) guard let service = self.service else { fatalError() } service.delegate = self service.resolve(withTimeout: 5.0) // Form a temporary retain loop to prevent us from being deinitialised // while the resolve is in flight. We break this loop in `stop(with:)`. selfRetain = self } func stop() { self.stop(with: .failure(CocoaError(.userCancelled))) } private func stop(with result: Result<(String, Int), Error>) { precondition(Thread.isMainThread) self.service?.delegate = nil self.service?.stop() self.service = nil let completionHandler = self.completionHandler self.completionHandler = nil completionHandler?(result) selfRetain = nil } func netServiceDidResolveAddress(_ sender: NetService) { let hostName = sender.hostName! let port = sender.port self.stop(with: .success((hostName, port))) } func netService(_ sender: NetService, didNotResolve errorDict: [String: NSNumber]) { let code = (errorDict[NetService.errorCode]?.intValue) .flatMap { NetService.ErrorCode.init(rawValue: $0) } ?? .unknownError let error = NSError(domain: NetService.errorDomain, code: code.rawValue, userInfo: nil) self.stop(with: .failure(error)) } } func main() { let service = NetService(domain: "local.", type: "_ssh._tcp", name: "Fluffy") print("will resolve, service: \(service)") BonjourResolver.resolve(service: service) { result in switch result { case .success(let hostName): print("did resolve, host: \(hostName)") exit(EXIT_SUCCESS) case .failure(let error): print("did not resolve, error: \(error)") exit(EXIT_FAILURE) } } RunLoop.current.run() } main()
-
For the DNS-SD version of this code, see this post.
And the part for finding host name would be nearly this size too? I surprised that there is so much coding for such standard situation)
For that you need a browser. And the nice thing here is that you can use NWBrowser, which is a lot easier to get a handle on than NetServiceBrowser. A good intro is WWDC 2019 Session 713 Advances in Networking, Part 2. Post back if you have problems with it.my goal not only to resolve it, but to find in local network as well.
In most cases you don’t need to do a Bonjour resolve operation because the connection APIs take a service directly. For example:I surprised that there is so much coding for such standard situation)
If you’re using NWConnection you can construct the connection using a .service(…) endpoint.
If you’re using NetService you can call getInputStream(_:outputStream:) directly on that service.
The reason you need so much code is that you’re trying to construct a URL, and for that you need a host name, and for that you need to resolve the service. That is, as I mentioned, a “missing link in our Bonjour story right now” )-: It’s also the reason I allocated time to write the code example I posted above, to help any other folks who bump into this.
Oh, and regarding that code snippet, I just posted a small but important update. The original code just returned the host name. The new code returns the host name and port. This is because resolving a Bonjour service gives you back a host name (and its IP addresses) and a port, and it’s important that you connect to the right port. This allows, for example, a server to register a service on a non-standard port.
When you construct a URL for this you should take the default port into account. For example:
Code Block let hostName: String = … let port: Int = … var components = URLComponents(string: "https://example.com")! components.host = hostName if port != 443 { components.port = port } let url = components.url!
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
Code Block language exit(EXIT_SUCCESS)
my app just freezing partially - I can go back to previous view and even change any sliders, but all buttons and navigation stop working. I have three views in app - main, additional one and settings. After starting I go to "settings" view and push button "find lamp", that call "main" resolving function from you example. Then I receive "host resolved" and may go back to main view, but then it partially stops working, as I described above...
Update: changing this fixed problem:
Code Block language RunLoop.current.run(until: Date(timeIntervalSinceNow: 5))
This suggests that you’ve misunderstood the role that run loops play here, so let’s see if I can clarify that.Update: changing this fixed problem:
An NetService needs to be scheduled on a run loop. When you call resolve(withTimeout:) the service schedules itself on the current run loop, that is, the run loop associated with the current thread. That’s the reason why my start method has this as the first line:
Code Block precondition(Thread.isMainThread)
I want to make sure that the NetService is scheduled on the main thread’s run loop.
The main thread runs its run loop by default. You don’t have to manually do this. Indeed, all of the callbacks associated with running your app are scheduled on the run loop. So, if you schedule a NetService on the main thread, you don’t have to take special steps to run the run loop; it will just run. Moreover, trying to run the run loop manually will cause all sorts of grief, not least of which is that it might prevent all the standard event source callbacks from running, and hence lock up your app.
In short, stop doing anything with run loops (-:
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
Hello! I've looked at @eskimo's sample code, and have ended in a situation. NetService
has been deprecated, and I'm looking into what can be done in its stead? I'm currently running a temporary connection with NWConnection
but even here I fail to find the host, so I can make a HTTP connection.
Right now your best option is to continue using NetService
. The alternative is to move to <dns_sd.h>
, which is way more complex.
When you use a deprecated API like this you enter a race: Will a Network framework equivalent be released (r. 73266838) before NetService
stops working? At this point I suspect that you will win that race, because NetService
is used by a lot of apps. I could be wrong though — I’m unable to predict the future with 100% accuracy, alas — but, if I am, the consequences are not dire. We usually only disable APIs in major OS releases, and so you’ll have time to move over to <dns_sd.h>
if necessary.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
-
I ended up deciding to write an
AsyncSequence
wrapper. If it helps anyone else, then it looks like this in its current state, but changes will happen. I might release it as a Swift package in the future, so I can reuse it laterimport Foundation class BonjourBrowser: NSObject, NetServiceDelegate { typealias ServiceFunction = (NetService) -> () private let delegate: Delegate = Delegate() private let browser: NetServiceBrowser = NetServiceBrowser() private var foundService: (ServiceFunction)? private class Delegate : NSObject, NetServiceBrowserDelegate { unowned var browser: BonjourBrowser? override init() { super.init() } func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) { self.browser?.didFind(serivce: service) } } init(type: String) { super.init() delegate.browser = self browser.delegate = delegate browser.searchForServices(ofType: type, inDomain: "local.") } fileprivate func didFind(serivce: NetService) { foundService?(serivce) } func didFindService() -> AsyncStream<NetService> { AsyncStream { continuation in foundService = { service in service.delegate = self service.resolve(withTimeout: 5.0) continuation.yield(service) } } } func netServiceDidResolveAddress(_ sender: NetService) { print(sender.hostName) } func netService(_ sender: NetService, didNotResolve errorDict: [String: NSNumber]) { let code = (errorDict[NetService.errorCode]?.intValue) .flatMap { NetService.ErrorCode.init(rawValue: $0) } ?? .unknownError let error = NSError(domain: NetService.errorDomain, code: code.rawValue, userInfo: nil) print(error) } }
I ended up deciding to write an
AsyncSequence
wrapper.
Your code snippet didn’t make it, alas. Try putting it in a reply rather than a comment.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
Hello, I work with @eskimo code and it runs perfect, but I have one problem. In my local network are more devices and I find only one of them.
Or do I get all of them and only one device is issued with
switch result {
case .success(let hostName):
print("did resolve, host: \(hostName)")
?
how I get the other one too?
--> at the end, I would like to add them to a List for using later