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.