Zeroconf discovery with Swift
In the past few months i've been working with a bunch of iOS/Swift projects that required a local service discovery using Zeroconf. In my previous blog post i've experimented with Zeroconf and Go which worked out pretty nicely. So in this post i'll show how to work with local service discovery in Swift.
We don't have to add any third party dependencies to our Swift app, all this networking stuff is already part of the core framework and is pretty easy to work with. Let's get started. To illustrate the functionality i'm going to create a simple view controller that will only print out a debug log, no views.
import UIKit
class ViewController: UIViewController,
NetServiceBrowserDelegate,
NetServiceDelegate {
// Local service browser
var browser = NetServiceBrowser()
// Instance of the service that we're looking for
var service: NetService?
// View entry point
override func viewDidLoad() {
super.viewDidLoad()
// Setup the browser
browser = NetServiceBrowser()
browser.delegate = self
}
override func viewDidAppear(_ animated: Bool) {
startDiscovery()
}
private func startDiscovery() {
// Make sure to reset the last known service if we want to run this a few times
service = nil
// Start the discovery
browser.stop()
browser.searchForServices(ofType: "_http._tcp", inDomain: "")
}
// MARK: Service discovery
func netServiceBrowserWillSearch(_ browser: NetServiceBrowser) {
print("Search about to begin")
}
func netService(_ sender: NetService, didNotResolve errorDict: [String : NSNumber]) {
print("Resolve error:", sender, errorDict)
}
func netServiceBrowserDidStopSearch(_ browser: NetServiceBrowser) {
print("Search stopped")
}
func netServiceBrowser(_ browser: NetServiceBrowser, didFind svc: NetService, moreComing: Bool) {
print("Discovered the service")
print("- name:", svc.name)
print("- type", svc.type)
print("- domain:", svc.domain)
// We dont want to discover more services, just need the first one
if service != nil {
return
}
// We stop after we find first service
browser.stop()
// Resolve the service in 5 seconds
service = svc
service?.delegate = self
service?.resolve(withTimeout: 5)
}
func netServiceDidResolveAddress(_ sender: NetService) {
print("Resolved service")
// Find the IPV4 address
if let serviceIp = resolveIPv4(addresses: sender.addresses!) {
print("Found IPV4:", serviceIp)
} else {
print("Did not find IPV4 address")
}
if let data = sender.txtRecordData() {
let dict = NetService.dictionary(fromTXTRecord: data)
let value = String(data: dict["hello"]!, encoding: String.Encoding.utf8)
print("Text record (hello):", value!)
}
}
// Find an IPv4 address from the service address data
func resolveIPv4(addresses: [Data]) -> String? {
var result: String?
for addr in addresses {
let data = addr as NSData
var storage = sockaddr_storage()
data.getBytes(&storage, length: MemoryLayout<sockaddr_storage>.size)
if Int32(storage.ss_family) == AF_INET {
let addr4 = withUnsafePointer(to: &storage) {
$0.withMemoryRebound(to: sockaddr_in.self, capacity: 1) {
$0.pointee
}
}
if let ip = String(cString: inet_ntoa(addr4.sin_addr), encoding: .ascii) {
result = ip
break
}
}
}
return result
}
}
Before we run the example, we'll need to start a zeroconf service somehow. I've created a simple beacon service that you can run on OSX or Linux: zeroconf-beacon.
Start the service with the command:
zeroconf-beacon -name=swift-demo -port=8888 -txt=hello=world
When we run the program in the iPhone emulator the output will look like this:
Search about to begin
Discovered the service
- name: swift-demo
- type _http._tcp.
- domain: local.
Search stopped
Resolved service
Found IPV4: 192.168.0.107
Text record (hello): world
In our example we start the application and then search for the specific type of
service _http._tcp
. We don't really care about multiple services that might be
found, we just need to resolve the IP and TXT records for the first one we find.
Once we find a service we will need to resolve it, meaning that the program will try to extract IPv4 address which then could be used to make HTTP calls, send data or whatever you're planning on doing with the local service. If you need to embed bunch of metadata about your service, use TXT records. Basic service info only includes the type and port, but you can pass any arbitrary string as TXT record and then handle the parsing/configuration yourself.
That's pretty much it. All the discovery and metadata parsing logic could be easily extended based on your application needs but should be enough to get you going.