Twisted Python and Bonjour

January 10, 2009

Bonjour (formerly Rendezvous) is Apple’s service discovery protocol. It operates over local networks via multicast DNS. Server processes announce their availability by broadcasting service records and their associated ports. Clients browse the network in search of specific service types, potentially connecting to the service on the advertised port using the appropriate network protocol for that service.

A common example of Bonjour in action is iTunes’ music library sharing feature. iTunes sharing uses DAAP (Digital Audio Access Protocol). iTunes uses Bonjour to announce its local shared libraries as well as to browse the network for remote DAAP servers.

Twisted Python, while supporting a wide range of network protocols by default, currently lacks an official Bonjour or multicast DNS implementation. The start of a multicast DNS implementation exists in Itamar’s sandbox, but it hasn’t been updated since 2004.

Given that, applications that want to use Bonjour-based service discovery must provide their own implementation. Unfortunately, there can only be one Bonjour “responder” running on a system at one time. If multiple applications attempted to advertise services by standing up competing responders, a port conflict would arise. Therefore, all of the Bonjour-aware applications running on a system must coordinate.

On Mac OS 10.2 and later, applications can simply communicate with the operating system’s built-in Bonjour service. Most other operation systems don’t provide native Bonjour functionality, but support is generally available via third-party packages. Apple provides Bonjour for Windows, and the LGPL-licensed Avahi runs on most other platforms.

Supporting multiple potential Bonjour interfaces can be a burden for application developers. Fortunately, for Python-based projects, pybonjour exists to provide a very nice ctypes-based abstraction layer to all of the Bonjour-compatible libraries mentioned above.

pybonjour’s public API is based on “service descriptors”. Each operation returns a service descriptor reference and signals the caller via a callback when the operation completes. Service descriptors can generally be treated like read-only file descriptors, but all read operations must be done using pybonjour’s DNSServiceProcessResult() function.

We can easily wrap a pybonjour service descriptor in an object that implements Twisted’s IReadDescriptor interface:

import pybonjour
from twisted.internet.interfaces import IReadDescriptor
from zope import interface

class ServiceDescriptor(object):

    interface.implements(IReadDescriptor)

    def __init__(self, sdref):
        self.sdref = sdref

    def doRead(self):
        pybonjour.DNSServiceProcessResult(self.sdref)

    def fileno(self):
        return self.sdref.fileno()

    def logPrefix(self):
        return "bonjour"

    def connectionLost(self, reason):
        self.sdref.close()

Then, it’s simply a matter of writing some operation-specific functions that join the pybonjour interface with Twisted’s event-driven concepts. These functions initiate the pybonjour request, handle the pybonjour callback, and dispatch the result using a Twisted Deferred.

The following example broadcasts a new service record. Note that the local callback function handles both the success and error results.

from twisted.internet.defer import Deferred

def broadcast(reactor, regtype, port, name=None):
    def _callback(sdref, flags, errorCode, name, regtype, domain):
        if errorCode == pybonjour.kDNSServiceErr_NoError:
            d.callback((sdref, name, regtype, domain))
        else:
            d.errback(errorCode)

    d = Deferred()
    sdref = pybonjour.DNSServiceRegister(name = name,
                                        regtype = regtype,
                                        port = port,
                                        callBack = _callback)

    reactor.addReader(ServiceDescriptor(sdref))
    return d

The caller can then provide callback and errback functions that will be invoked using Twisted’s event-based reactor machinery.

from twisted.internet import reactor
from twisted.python import log

sdref = None

def broadcasting(args):
    global sdref
    sdref  = args[0]
    log.msg('Broadcasting %s.%s%s' % args[1:])

def failed(errorCode):
    log.err(errorCode)

d = broadcast(reactor, "_daap._tcp", 3689, "DAAP Server")
d.addCallback(broadcasting)
d.addErrback(failed)

To stop broadcasting, simply close the service descriptor (sdref.close()).