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

I work with eskimo code and it runs perfect, but I have one problem.

If you’re referring to this code then that result is expected. If you have multiple devices on a network, each will have its own unique host name. When you resolve that host name you’ll get the set of IP addresses currently associated with that device. If you want to browse for multiple devices, use the NWBrowser class to browse for a service that they advertise. And if you need to remember a device, or a set of devices, record their service names not their host names because the latter is less stable.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

thank you for the fast answer. I think you didn't understand me correctly. :-) i have some devices, but all have the same serviceType and name:  let service = NetService(domain: "local.", type: "_ssh._tcp", name: "mesh")  my problem is that your code find only one device of them. if I switch the founded device off, I can find the next but I need them all I hope my explanation is understandable

but all have the same serviceType and name

I doubt that. If two peers start out with the same name / service / domain tuple then the second peer to join the network should detect that and rename itself. That’s part of the Bonjour standards [1].

Now, it’s possible that you’re working with a non-compliant accessory but, if you are, there’s not much that the Apple APIs can do to help you out.

It’s more likely, however, that your accessory is working correctly and you’ve just missed the rename. And that’s why I suggested that you browse for services of the specified type. That’ll give you a set of service names, which you can then resolve to map to DNS names (or IP addresses, if that’s your thing).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Specifically:

  • RFC 6762 Multicast DNS

  • RFC 6763 DNS-Based Service Discovery

I've try to follow your recommend to use NWBrowser but i got only the results "start function" and "end function" with this code

public func findServices() {
        print("start function")
        let parameter = NWParameters()
        parameter.includePeerToPeer = true
        let browser = NWBrowser(for: .bonjour(type: "_mesh-http._tcp.", domain: "local."), using: parameter)
        browser.stateUpdateHandler = { state in
            switch state {
            case .ready:
                print("the sericeHandler is ready")
            case .failed(let error):
                print("error:", error.localizedDescription)
            default:
                break
            }
        }
        browser.browseResultsChangedHandler = { result, changed in
            print("in handler")
            result.forEach { device in
                device.interfaces.forEach {interface in
                    //ADD THE NAME TO A LIST
                    print(interface.name)
                }
            }
            changed.forEach{change in
                switch change
                {
                case .identical:
                    print("no change")
                    break
                case .added:
                    print("A new result was discovered. ")
                case .removed:
                    print("A previously discovered result was removed.")
                default:
                    break
                }
            }
        }
        print("end function")
        browser.start(queue: .main)
    }

now my Solution. Please leave feedback

--> it's a combination of some answers in this forum

        private func findService(with name:String) {
             let service = NetService(domain: "local.", type: "_mesh-http._tcp.", name: name)
             print("will resolve, service: \(service)")
             BonjourResolver.resolve(service: service) { result in
                 switch result {
                  case .success(let hostName):
                       print("did resolve, host: \(hostName)")
                  case .failure(let error):
                       print("did not resolve, error: \(error)")
            }
        }
    }

    public func findServices()
    {
        let agent = BrowserAgent()
        let browser = NetServiceBrowser()
        browser.delegate = agent
        browser.searchForServices(ofType: "_mesh-http._tcp.", inDomain: "local.")
        browser.schedule(in: RunLoop.main, forMode: .common)
        DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) { // give the browser time to brows
                   print("finish")
            for device in agent.getDevices(){
                print(device)
                self.findService(with: device)
            }
                }
        RunLoop.main.run()
    }


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 {
        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: 3.0)
        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
        if let data = sender.txtRecordData() {
            let dict = NetService.dictionary(fromTXTRecord: data)
            print(dict.mapValues { String(data: $0, encoding: .utf8) })
        }
        print("\(hostName)  \(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))
    }
}

class ServiceAgent : NSObject, NetServiceDelegate {
    func netServiceDidResolveAddress(_ sender: NetService) {
        if let data = sender.txtRecordData() {
            let dict = NetService.dictionary(fromTXTRecord: data)
            print("Resolved: \(dict)")
            print(dict.mapValues { String(data: $0, encoding: .utf8) })
        }
    }
}

class BrowserAgent : NSObject, NetServiceBrowserDelegate {
    var currentService:NetService?
     let serviceAgent = ServiceAgent()
    var devices = [String]()
   public func getDevices() -> [String]{
        return devices
    }

    func netServiceBrowser(_ browser: NetServiceBrowser, didFindDomain domainString: String, moreComing: Bool) {
        print("domain found: \(domainString)")
    }
    func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
        print("service found: \(service.name)")
        devices.append(service.name)
    }
}

I make the double search because the result of func netServiceBrowser have no Mac address in the NetService-Object(it's always empty) and I need this. the Solution works but I think it's very awkward

Mac address

Do you mean MAC address here?

It seems like you’re resolving every service that you find. This is something we specifically recommend against because it can trigger a punishing amount of unnecessary network traffic on a large network. In most cases you should only resolve a service if you need to connect to it. Is there a specific reason why you’re resolving every service you find? What other info are you looking for?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

yes, I need to connect with all devices with this service. I send a httpRequest and get some parameter from all devices. Then I create a dynamic ListView to show DeviceInformations like Devicename, temperature, lightstate...

i find out that all devices have hostname like device.local device-1.local device-2.local .... so I have the best results if search with a for_loop

private func fillHostName()

    {
        let number = 30
        var counter = 0

        self.hostNames.removeAll()
        if newHostNames.count == 0 {
            while counter < number  {
                if counter == 0
                {
                    self.hostNames.append(String("device.local"))
                }
                else
                {
                    self.hostNames.append(String("device-\(counter).local"))
                }
                counter += 1
            }
        }
            else
            {
                self.hostNames = self.newHostNames
            }
    } 


self.hostNames.forEach{ host in
                    self.getMeshInfo(from: host)
                usleep(80000)
}

I know that is "stupid polling" but it works absolut perfekt on very fast.

is there a better way?

i find out that all devices have hostname like device.local, device-1.local, device-2.local

You can’t rely on that. Bonjour accessories have a lot of flexibility in how they choose to rename themselves to avoid collisions.

The standard technique here is to browse for services and then, if necessary, resolve the service as part of the connection process. It’s best to avoid doing the second step for all services. For this reason, many accessories don’t require you to connect to get info like this but instead put useful info in the service’s TXT record.

Consider this:

% dns-sd -B _ipp._tcp. local.
…
Timestamp     A/R    Flags  if Domain  Service Type Instance Name
11:27:46.969  Add        3   6 local.  _ipp._tcp.   Darth Inker
…
^C
% dns-sd -L "Darth Inker" _ipp._tcp. local.
Lookup Darth Inker._ipp._tcp..local.
DATE: ---Tue 14 Dec 2021---
11:28:32.636  ...STARTING...
11:28:32.637  Darth\032Inker._ipp._tcp.local. can be reached at darth-inker.local.:631 (interface 24)
 txtvers=1 qtotal=1 pdl=…,application/postscript,…

_ipp._tcp is the service type used by IPP-capable printers. As you can see, I have one on my network called Darth Inker. When I resolve that I get both the the DNS name and port (darth-inker.local.:631) but also the TXT record. This includes info like the page description language supported by the printer (application/postscript).

If you’re not sure what service is published by your accessory, you can browse for the _services._dns-sd._udp meta service:

% dns-sd -B _services._dns-sd._udp. local.
Browsing for _services._dns-sd._udp..local.
DATE: ---Tue 14 Dec 2021---
11:33:30.189  ...STARTING...
Timestamp     A/R    Flags  if Domain  Service Type Instance Name
…
11:33:30.483  Add        3  24 .       _tcp.local.  _ipp
…^C

If you’re working with a specific accessory, it’s best to run this test with it and your Mac on an isolated network, so you can pick the accessory’s service out of the list.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Stumbled across this post and solution and then found that NetService has finally been deprecated as you thought would happen. Wondering if the Network framework has a solution for this now.

I found this SO post: https://stackoverflow.com/questions/60579798/how-to-resolve-addresses-and-port-information-from-an-nwendpoint-service-enum-ca which suggests using a NWConnection and connecting to the service and then discarding the connection after finding the host/port and using that elsewhere. Not great, but likely would work.

Also links to this even older thread https://developer.apple.com/forums/thread/122638 you answered as well.

I'm using a third party C library that handles all the traffic between server/client. I'm adding a bonjour frontend to advertise things which is why I can't use the NWConnection and instead need the host/port to pass in.

Thanks

Wondering if the Network framework has a solution for this now.

No )-:

I found this SO post

That’ll certainly work, but it’s not without its drawbacks. It effectively saves code complexity at the expensive of runtime and network efficiency.

I'm using a third party C library

If you’re in the C world, you might consider switching to the DNS-SD API (<dns_sd.h>), which is not deprecated and supports this functionality (at the cost of even greater code complexity).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

I was working a DTS tech support incident today and needed a version of this code based on DNS-SD. So I wrote that, and I figured I’d share it here.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"


import Foundation
import Network
import dnssd

final class BonjourResolver: NSObject, NetServiceDelegate {

    typealias CompletionHandler = (Result<(String, Int), Error>) -> Void

    @discardableResult
    static func resolve(endpoint: NWEndpoint, completionHandler: @escaping CompletionHandler) -> BonjourResolver {
        dispatchPrecondition(condition: .onQueue(.main))
        let resolver = BonjourResolver(endpoint: endpoint, completionHandler: completionHandler)
        resolver.start()
        return resolver
    }
    
    private init(endpoint: NWEndpoint, completionHandler: @escaping CompletionHandler) {
        self.endpoint = endpoint
        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 in `start()`.
        assert(self.refQ == nil)
        assert(self.completionHandler == nil)
    }
    
    let endpoint: NWEndpoint
    private var refQ: DNSServiceRef? = nil
    private var completionHandler: (CompletionHandler)? = nil
    
    private func start() {
        dispatchPrecondition(condition: .onQueue(.main))
        precondition(self.refQ == nil)
        precondition(self.completionHandler != nil)
        
        do {
            guard
                case .service(name: let name, type: let type, domain: let domain, interface: let interface) = self.endpoint,
                let interfaceIndex = UInt32(exactly: interface?.index ?? 0)
            else {
                throw NWError.posix(.EINVAL)
            }

            let context = Unmanaged.passUnretained(self)
            var refQLocal: DNSServiceRef? = nil
            var err = DNSServiceResolve(
                &refQLocal,
                0,
                interfaceIndex,
                name, type, domain,
                { _, _, _, err, _, hostQ, port, _, _, context in
                    // We ignore the ‘more coming’ flag because we are a
                    // one-shot operation.
                    let obj = Unmanaged<BonjourResolver>.fromOpaque(context!).takeUnretainedValue()
                    obj.resolveDidComplete(err: err, hostQ: hostQ, port: UInt16(bigEndian: port))
                }, context.toOpaque())
            guard err == kDNSServiceErr_NoError else {
                throw NWError.dns(err)
            }
            let ref = refQLocal

            err = DNSServiceSetDispatchQueue(ref, .main)
            guard err == kDNSServiceErr_NoError else {
                DNSServiceRefDeallocate(ref)
                throw NWError.dns(err)
            }
            
            // The async operation is now started, so we retain ourselves.  This
            // is cleaned up when the operation stops in `stop(with:)`.

            self.refQ = ref
            _ = context.retain()
        } catch {
            let completionHandler = self.completionHandler
            self.completionHandler = nil
            completionHandler?(.failure(error))
        }
    }
    
    func stop() {
        self.stop(with: .failure(CocoaError(.userCancelled)))
    }
    
    private func stop(with result: Result<(String, Int), Error>) {
        dispatchPrecondition(condition: .onQueue(.main))

        if let ref = self.refQ {
            self.refQ = nil
            DNSServiceRefDeallocate(ref)
            
            Unmanaged.passUnretained(self).release()
        }
        
        if let completionHandler = self.completionHandler {
            self.completionHandler = nil
            completionHandler(result)
        }
    }
    
    private func resolveDidComplete(err: DNSServiceErrorType, hostQ: UnsafePointer<CChar>?, port: UInt16) {
        if err == kDNSServiceErr_NoError {
            self.stop(with: .success((String(cString: hostQ!), Int(port))))
        } else {
            self.stop(with: .failure(NWError.dns(err)))
        }
    }
}
Bonjour for discovering a specific device's ip
 
 
Q