Bonjour for discovering a specific device's ip

Hi, I'm new to swift programming and right now writing an app for esp8266-controlled lamp device. My lamp is broadcasting it's own IP through bonjour. So all I want is to discover any lamps in my network (http.tcp) and to read name and value. Is there any example of such implementation? All I found so far is old or a lit bit complicated for such simple question. Thanks in advance!
Answered by DTS Engineer in 662482022

Just adding .local to the host name solved the problem.

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.

The only way to get the DNS name of a service is to resolve it.

But it still freezing my app just after service found

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.

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()

What are you planning to do with the IP address once you’ve got it? Connect to it? If so, using what protocol?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"
I'm planning to send http requests. I've already done with this part - all is working as expected, but I have to enter IP manually. All I want to do - search local network for "_http._tcp" services with name starting with "embui...", find their IPs and save for later use.

I'm planning to send http requests.

Yeah, that’s a missing link in our Bonjour story right now. Apple plaforms support a number of Bonjour APIs, including:
  • <dns_sd.h>

  • Network framework

  • CFNetService

  • NSNetService

We generally advise folks to use Network framework, then only move down to the very low-level <dns_sd.h> API if Network framework is missing some functionality feature. The CFNetService and NSNetService are… well… they’re not officially deprecated, but IMO they should be (r. 74344677).

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"
Thank you for such detailed answer!) I came to thought that maybe it'll be better to find host and then communicate by host, not IP. Just adding ".local" to the host name solved the problem. So I've already managed how to find host name. Something like this:

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.
I've left only common code for my purpose:
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
Accepted Answer

Just adding .local to the host name solved the problem.

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.

The only way to get the DNS name of a service is to resolve it.

But it still freezing my app just after service found

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.

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()

Thank you for this fully commented example, it's very helpful! It works for resolving known name, but every lamp has its individual name, depending on esp8266's id (EmbUI-XXXXXX) and my goal not only to resolve it, but to find in local network as well.
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)

my goal not only to resolve it, but to find in local network as well.

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.



I surprised that there is so much coding for such standard situation)

In most cases you don’t need to do a Bonjour resolve operation because the connection APIs take a service directly. For example:
It’s much better to connect directly to a service, rather than do the resolve then connect, because that allows the system infrastructure to handle all the weird edge cases (and here are lots of those).

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"
Thank you so much for helping! But I have same problem with your code - its working as expected, but in the end, when I commented out
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))


Update: changing this fixed problem:

This suggests that you’ve misunderstood the role that run loops play here, so let’s see if I can clarify that.

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"
Thanks a lot! You sold all my problems))) Now I'll be able to complete my app and send it to appstore. I hope this thread will be useful for others, because there are quite a few examples about bonjour, especially with comments )

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.

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

Bonjour for discovering a specific device's ip
 
 
Q