Using mDNS for hybrid web and native iOS app development

View My GitHub Profile

Network problems

Recently I had to build a teeny tiny iOS native application which was basically a thin wrapper to a NSWebView which hit a Ruby on Rails application where most of the heavy lifting was done. To do this it was convenient to have different target rails apps in development and production environments. Moreover, it was particularly convenient to make my local development environment the target for the application in the development phase. In order to avoid hard-coding my own IP address or hostname into the application, which would mean each developer on the project would have to override it, I decided to turn to a more flexible solution leveraging mDNS on OSX. Read more after the break to find out how!

By default, in new iOS projects, XCode provides a DEBUG definition which can be leveraged to setup different configurations in your debug and relase environments. For example, if you were running a local rails application that you would like to hit from the simulator, but on release you’d like to hit the actual production website you could do something like the following.

#ifdef DEBUG
#  define kURL @"http://localhost:10001"
#  define kURL @""

The above works great if you are only testing using the simulator, but what if you have to test your native app with some officially licensed iPhone, iPod Touch, or iPad accessory? Clearly the app isn’t running on localhost and hardcoding your IP or hostname is not flexible for the future… enter mDNS.

mDNS (aka ZeroConf networking aka Bonjour) is super cool and allows things like all of Apple’s cool photo/music/video sharing in your house, for projects like Pow to make your life much easier as a developer, and in theory should mean we never ever EVER have to run an internal DNS on our home networks again. It is even supported under Linux via Avahi. All that said, even with a past life as a network engineer, getting it working took much longer than I am willing to admit. Luckily, the win in the end made it worth while!

After reading the manpage and way too much manual/shotgun debugging of various parameters I was able to convince dns-sd to proxy access requests to a given .local hostname to my running Ruby on Rails application.

By executing

dev$ dns-sd -P myapp _http._tcp . 10001 myapp.local `hostname`

we can then hit our running application from anywhere on our local network by visiting http://myapp.local:10001, including from our native application on an external iDevice. To observe this in real-time open another terminal and run $dns-sd -B to watch for all mDNS registrations as the occur.

On most of many projects Foreman is to manage the services we need to launch so naturally I wanted it to do this heavy lifting for me too. Foreman provides the $PORT variable which you set by passing the -p <PORT> Surprisingly repeated uses of the variable actually increment the provided port number so given a Procfile containing

service1: service1 start -p $PORT
service2: service2 start -p $PORT

and starting services with it via foreman start -p 1000 produces running services like

Timestamp service1.1     | start -p 1000
Timestamp service2.1     | start -p 1001

This was a bit surprising for me! So, instead of using the default $PORT “variable” we can fall back to using Foreman’s .env file and specify a port to launch on that is used consistently across many services in our project. I realize this is a rare use case as generally $PORT specifies which port a service should bind to, and rarely would two attempt to bind the same port.

Foreman allows for automatically creating environment variables by updating a .env file. You probably should not commit this file to your git repo but instead should add to your .gitignore and provide a dotenv.sample file with your code for the ease of other developers. By adding two variables to your .env like the following

HOSTNAME=<output of `hostname` on the commandline>

and updating your Procfile to look (something) like

web: bundle exec rails server thin -p $MYAPP_PORT
mdns: dns-sd -P myapp _http._tcp . $MYAPP_PORT myapp.local $HOSTNAME
worker: some_background_worker
etc:  # etc...

you can let Foreman help you out in automatically creating this mapping. You should definitely practice some good README Driven Development and describe to your future self that the -p <PORT> flag to Foreman no longer does what is expected, and note the existence of, and importance of the dotenv.sample file.

#ifdef DEBUG
#  define kURL @"http://myapp.local:10001"
#  define kURL @""

This has actually been unexpectedly handy in that our designer is now able to do responsive design testing on her actual physical devices by hitting the mDNS URL from them!

Is there an even better way? Can I make dns-sd do the even more correct thing and somehow, magically, make it such that I don’t need to provided the port number in the URL (I kinda suspect not)?Does this work for Android devices (e.g. do they listen to multicast advertisements and can you hit .local addresses from their browser)? Hit me up in Issues to let me know!

Image credit: rothwerx

Have something to contribute? Open an Issue on Github and let's have a chat!