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
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
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
andname
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)))
}
}
}