Implementing Bonjour across iOS and Android
May 10, 2015
I wanted to have a demo of a crossplatform service that implements the same protocol on both iOS and Android. Apps on both platforms should advertise themselves as the same Bonjour service on the network, and respond with the same kind of information.
(I know that technically it’s “Bonjour” only on Apple platforms. The Android implementation officially calls itself Network Service Discovery. And both of them are implementations of “zero-configuration networking” using “DNS-SD (Service Discovery)” and “Multicast DNS” and… enough alphabet soup yet? Let’s just use “Bonjour” in this post.)
Long story short, here’s the code. The contains the OSX tool that discovers the services and talks to them, and service implementations on iOS and Android.
Here’s what the tool does.
A few notes about the implementation.
Programming model
The programming model is similar on both platforms. If you’ve built it on one, you’ll feel right at home on the other. On iOS, the relevant classes are NSNetService
and NSNetServiceBrowser
, while their Android equivalents are NsdServiceInfo
and NsdManager
.
On the provider/server side, you just create your service and register it.
On the consumer/client side, you ask the manager/browser to listen to services of some type (_jktest._tcp
in my example), and you implement the delegate/callback methods that are called when new services are found. When a service is found, you don’t talk to it directly: you must first invoke another step, “resolve”, to resolve the actual service address.
Building the actual service (web server)
Service discovery is only about what it says, discovery. It says, “here’s a service with this name running on this port”. It doesn’t actually implement the service, which you must do on your own. So we need a small webserver on both sides, since I’m building a HTTP-based service.
On iOS, I use GCDWebServer that I can only say good words about. I actually submitted a patch to it last year for custom Bonjour service names. It implements the Bonjour registration internally, so you don’t need to do any of that: you just give it the desired service name and type, and off you go. GCDWebServer handles all the details like stopping both the server and service advertising when the app goes to background, and restarting them once you come to foreground again. CocoaHTTPServer used to be a popular choice in earlier days.
On Android, I use NanoHttpd simply because it’s the first popular one that I came across that seemed to do what I needed. I didn’t evaluate all the other options. It’s a simple thing that I could just drop in to my app. It doesn’t have all the conveniences of stopping/starting at backgrounding, and it doesn’t know anything about Bonjour, so I needed to implement all of those myself.
Quirks and gotchas
Mostly, building this was quite uneventful and everything worked like it should have. There’s two things that I came across on Android worth noting.
First, it’s good practice to use a dynamic port number and not hardcode it. This was incompatible with NanoHttpd approach that expects you to give it a static port number. The way I found out here feels a bit like a hack. When you create a network socket with port 0, the system actually allocates you the next available port from the high range, which is exactly what we want. But, since you’re now bound this port to your socket, you can’t use it in NanoHttpd. So, what I do is allocate a socket, get the dynamic port from it, then close the socket so the port is no longer bound, and then use this port with NanoHttpd and my service advertising. There’s a bit of a race condition here, I think, because someone else might bind to it again after I unbind it, but it seems to be working well enough in practice. A cleaner solution would be that NanoHttpd or another embedded webserver implemented the dynamic port allocation internally so that I could give it port 0 and then ask what port it actually got, but I didn’t feel like patching the server code. (Maybe it already does this. I didn’t try. Not important enought for this example.)
Secondly, unlike the iOS simulator that directly uses the host networking, the Android emulator networking is virtualized. You don’t get a local network IP directly when running an app in emulator: the emulator creates a private network and you get an address from there instead. If you then create Bonjour services in emulator, those aren’t bridged/advertised to your real local network. Maybe there’s ways around this, but I just ran everything on a real Android device where it worked fine.