Wiki

Zero-Configuration Networking in Qt

by Trenton Schulz

Bonjour is Apple's implementation of zero-configuration networking (Zeroconf), which allows various applications to advertise their services on a local area network. Using Bonjour greatly simplifies finding and using network services. In this article, we will create Qt objects that will handle the various parts of Bonjour and then we will see how we can use those objects in some of Qt's networking examples.

Who's Who: Zeroconf, Bonjour, Avahi

Zeroconf is meant to solve the problem of finding services and connecting to them. Instead of having to know a machine's IP address and port number for the service, a machine offering a service simply announces that it offers the service. Clients who want to use a service ask for all the machines that are offering it and then the user decides which one to connect to.

Traditionally, you would have to make sure that each machine is configured correctly and on the network. Zeroconf takes care of all of this for you for a local area network. Lots of new hardware, such as printers with networking support or wireless routers, come with their own Zeroconf server to allow easy network configuration. On Mac OS X, many applications take advantage of Bonjour to advertise services, such as the ssh server, iTunes shares, or iChat availability. Zeroconf is a powerful way of simplifying your applications, and there are implementations available for most operating systems.

For this article we use Apple's implementation of Zeroconf called Bonjour. Bonjour consists of the mDNSResponder daemon and the multicast DNS service discovery library to interface with the daemon. While early releases of Bonjour used a controversial open source license, the current daemon is released under the standard Apache 2.0 license, while the library itself is under the liberal "three-clause" BSD license.

If you have Mac OS X, you already have Bonjour installed; otherwise, you can download the source code from the Apple website (http://developer.apple.com/Bonjour) and build and install Bonjour in relatively short order. Most modern Linux distributions come with Avahi, an LGPL implementation of Zeroconf with a compatibibility API for Bonjour. The examples presented here were tested to work with both Apple's Bonjour implementation and Avahi's Bonjour compatibility layer.

Service discovery consists of three steps: registering a service, browsing for available services, and resolving the service to an actual address. A server will register its services with the Bonjour daemon. Clients will browse for services to get a list to provide to the user. Finally, when it is time to connect to a service, the client will resolve the selected service to an actual IP address and port and then connect to the service provide using TCP/IP.

Storing a Bonjour Entry

To begin using Bonjour, we first create a simple class to contain all the information contained in a Bonjour entry:

    class BonjourRecord
    {
    public:
      BonjourRecord() {}
      BonjourRecord(const QString &name,
                    const QString &regType,
                    const QString &domain)
        : serviceName(name), registeredType(regType),
          replyDomain(domain) {}
      BonjourRecord(const char *name, const char *regType,
                    const char *domain) {
        serviceName = QString::fromUtf8(name);
        registeredType = QString::fromUtf8(regType);
        replyDomain = QString::fromUtf8(domain);
      }
 
      QString serviceName;
      QString registeredType;
      QString replyDomain;
 
      bool operator==(const BonjourRecord &other) const {
        return serviceName == other.serviceName
               && registeredType == other.registeredType
               && replyDomain == other.replyDomain;
      }
    };
 
    Q_DECLARE_METATYPE(BonjourRecord)

The BonjourRecord class is a simple data structure with public members for the human-readable name of the service, the service type, and the reply domain (which we won't use). We also declare it as a Qt meta-type so we can store a BonjourRecord in a QVariant or emit it in a cross-thread signal.

We will now review the Bonjour wrapper classes, which provide a high-level Qt API to Bonjour. The classes are called BonjourRegistrar, BonjourBrowser, and BonjourResolver, and follow the same pattern: We first let Bonjour know we are interested in an activity (registering, browsing, or resolving) by registering a callback to get that information. Bonjour then gives us a data structure and socket to let us know when more information is ready. When information is ready, we pass the structure back to Bonjour, and Bonjour invokes our callback.

Registering a Bonjour Service

Here is the declaration of the BonjourRegistrar class:

    class BonjourRegistrar : public QObject
    {
      Q_OBJECT
 
    public:
      BonjourRegistrar(QObject *parent = 0);
      ~BonjourRegistrar();
 
      void registerService(const BonjourRecord &record,
                           quint16 servicePort);
      BonjourRecord registeredRecord() const
        { return finalRecord; }
 
    signals:
      void error(DNSServiceErrorType error);
      void serviceRegistered(const BonjourRecord &record);
 
    private slots:
      void bonjourSocketReadyRead();
 
    private:
      static void DNSSD_API bonjourRegisterService(
            DNSServiceRef, DNSServiceFlags,
            DNSServiceErrorType, const char *, const char *,
            const char *, void *);
 
      DNSServiceRef dnssref;
      QSocketNotifier *bonjourSocket;
      BonjourRecord finalRecord;
    };

The class consists of one main member function, registerService(), that registers our service. As long as the object is alive, the service stays registered. There are some signals to check the results of the registration, but they aren't strictly necessary to successfully register a service. The rest of the class wraps the various bits of Bonjour service registration. The static callback function is marked with the DNSSD_API macro to make sure that the callback has the correct calling convention on Windows.

Here are the class constructor and destructor:

    BonjourRegistrar::BonjourRegistrar(QObject *parent)
      : QObject(parent), dnssref(0), bonjourSocket(0)
    {
    }
 
    BonjourRegistrar::~BonjourRegistrar()
    {
      if (dnssref) {
        DNSServiceRefDeallocate(dnssref);
        dnssref = 0;
      }
    }

In the constructor, we zero out the dnssref and bonjourSocket member variables. We will use them later on when we register the service. In our destructor, if we've registered a service, we deallocate it. This will also unregister it.

    void BonjourRegistrar::registerService(
        const BonjourRecord &record, quint16 servicePort)
    {
      if (dnssref) {
        qWarning("Already registered a service");
        return;
      }
 
      quint16 bigEndianPort = servicePort;
    #if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
      bigEndianPort = ((servicePort & 0x00ff) << 8)
                      | ((servicePort & 0xff00) >> 8);
    #endif
      DNSServiceErrorType err = DNSServiceRegister(&dnssref,
          0, 0, record.serviceName.toUtf8().constData(),
          record.registeredType.toUtf8().constData(),
          record.replyDomain.isEmpty() ? 0
                    : record.replyDomain.toUtf8().constData(),
          0, bigEndianPort, 0, 0, bonjourRegisterService,
          this);
      if (err != kDNSServiceErr_NoError) {
        emit error(err);
      } else {
        int sockfd = DNSServiceRefSockFD(dnssref);
        if (sockfd == -1) {
          emit error(kDNSServiceErr_Invalid);
        } else {
          bonjourSocket = new QSocketNotifier(sockfd,
                                 QSocketNotifier::Read, this);
          connect(bonjourSocket, SIGNAL(activated(int)),
                  this, SLOT(bonjourSocketReadyRead()));
        }
      }
    }

The registerService() function is where most of the action takes place. First, we check if we've already registered a service for this object. If that's the case, we warn and return. Bonjour requires that we register the port in big-endian byte order; therefore, we convert the port number if we are on a little-endian machine. (Qt 4.3 introduced qToBigEndian(), which we could use to make our code a bit prettier.)

With the port in the right byte order, we call DNSServiceRegister(). This function has a lot of parameters to control which network interfaces we advertise on and how we want to handle name conflicts, and lets us specify our own DNS-SRV record. We pass 0 for these because default behavior is fine. We pass our DNSServiceRef, our unique (to us at least) service name, our registered type, and our optional reply domain. We also pass our port number and our callback to handle extra information. We pass a pointer to the this object as the last argument so that we can access it in the callback.

If the call to DNSServiceRegister() fails, we emit the error through our error() signal. By connecting to this signal, slots can do whatever error handling they want. If DNSServiceRegister() succeeds, we get the socket descriptor associated with the service and create a QSocketNotifier for it to let us know when we need to "read" from it. We connect the socket notifier's activated() signal to our bonjourSocketReadyRead() slot.

    void BonjourRegistrar::bonjourSocketReadyRead()
    {
      DNSServiceErrorType err =
            DNSServiceProcessResult(dnssref);
      if (err != kDNSServiceErr_NoError)
        emit error(err);
    }

In bonjourSocketReadyRead(), we tell Bonjour to process the information on the socket. This will invoke our callback. Again, if there is an error, we emit the error code through the error() signal.

    void BonjourRegistrar::bonjourRegisterService(
        DNSServiceRef, DNSServiceFlags,
        DNSServiceErrorType errorCode, const char *name,
        const char *regType, const char *domain, void *data)
    {
      BonjourRegistrar *registrar =
            static_cast<BonjourRegistrar *>(data);
      if (errorCode != kDNSServiceErr_NoError) {
        emit registrar->error(errorCode);
      } else {
        registrar->finalRecord =
              BonjourRecord(QString::fromUtf8(name),
                            QString::fromUtf8(regType),
                            QString::fromUtf8(domain));
        emit registrar->serviceRegistered(
                                      registrar->finalRecord);
      }
    }

The callback checks to see if the process succeeded. If it did, we fill in the final values for our BonjourRecord. In practice, the only thing that could potentially change is the name that we provided in registerService() since there could have been another item providing the service with that name already. In this case, Bonjour returns a new unique name to us. We then emit the serviceRegistered() signal with that information.

Browsing Through Available Bonjour Services

In a Bonjour client, we need a way to retrieve the list of available services. This is handled by the BonjourBrowser class:

    class BonjourBrowser : public QObject
    {
      Q_OBJECT
 
    public:
      BonjourBrowser(QObject *parent = 0);
      ~BonjourBrowser();
 
      void browseForServiceType(const QString &serviceType);
      QList<BonjourRecord> currentRecords() const
        { return bonjourRecords; }
      QString serviceType() const { return browsingType; }
 
    signals:
      void currentBonjourRecordsChanged(
            const QList<BonjourRecord> &list);
      void error(DNSServiceErrorType err);
 
    private slots:
      void bonjourSocketReadyRead();
 
    private:
      static void DNSSD_API bonjourBrowseReply(DNSServiceRef,
            DNSServiceFlags, quint32, DNSServiceErrorType,
            const char *, const char *, const char *, void *);
 
      DNSServiceRef dnssref;
      QSocketNotifier *bonjourSocket;
      QList<BonjourRecord> bonjourRecords;
      QString browsingType;
    };

BonjourBrowser follows a very similar pattern to what we've seen in the BonjourRegistrar class. Once we've created a BonjourBrowser and told it what service we want to look for, it will let us know which hosts currently provide that service, and inform us of any changes via its currentBonjourRecordsChanged() signal.

Let's look at the browseForServiceType() function:

    void BonjourBrowser::browseForServiceType(
          const QString &serviceType)
    {
      DNSServiceErrorType err = DNSServiceBrowse(&dnssref, 0,
            0, serviceType.toUtf8().constData(), 0,
            bonjourBrowseReply, this);
      if (err != kDNSServiceErr_NoError) {
        emit error(err);
      } else {
        int sockfd = DNSServiceRefSockFD(dnssref);
        if (sockfd == -1) {
          emit error(kDNSServiceErr_Invalid);
        } else {
          bonjourSocket = new QSocketNotifier(sockfd,
                                 QSocketNotifier::Read, this);
          connect(bonjourSocket, SIGNAL(activated(int)),
                  this, SLOT(bonjourSocketReadyRead()));
        }
      }
    }

By calling browseForServiceType(), we tell BonjourBrowser to start browsing for a specific type of service. Internally, we call DNSServiceBrowse(), which has a similar signature to DNSServiceRegister(). If the call succeeds, we create a QSocketNotifier for the associated socket and connect its activated() signal to the bonjourSocketReadyRead() slot. This slot is identical to the slot of the same name in BonjourRegistrar, so let's take a look at the callback:

    void BonjourBrowser::bonjourBrowseReply(DNSServiceRef,
          DNSServiceFlags flags, quint32,
          DNSServiceErrorType errorCode,
          const char *serviceName, const char *regType,
          const char *replyDomain, void *context)
    {
      BonjourBrowser *browser =
            static_cast<BonjourBrowser *>(context);
      if (errorCode != kDNSServiceErr_NoError) {
        emit browser->error(errorCode);
      } else {
        BonjourRecord record(serviceName, regType,
                             replyDomain);
        if (flags & kDNSServiceFlagsAdd) {
          if (!browser->bonjourRecords.contains(record))
            browser->bonjourRecords.append(record);
        } else {
          browser->bonjourRecords.removeAll(record);
        }
        if (!(flags & kDNSServiceFlagsMoreComing)) {
          emit browser->currentBonjourRecordsChanged(
                                     browser->bonjourRecords);
        }
      }
    }

The BonjourBrowser callback is more complicated. First, we get our BonjourBrowser object from the context pointer that is passed in. If we are in an error condition, we emit that; otherwise, we examine the flags passed in.

The flags indicate if a service has been added or removed; we update our QList accordingly. If a service is added, we first check to see if the record doesn't already exist in our list before adding it. (We could get a record twice if, for example, we are offering a service locally and it would be available through the loopback device and our Ethernet port.) Since the callback can only be called for one record at a time, it also specifies the kDNSServiceFlagsMoreComing flag to indicate that it is going to be called again shortly. When all the records have been sent, this flag will be cleared and then we emit our currentBonjourRecordsChanged() signal with the current set of records.

Resolving Bonjour Services

Clients can now browse for services, but there may be a point when they want to resolve services to an actual IP address and port number. This is handled via the BonjourResolver class:

    class BonjourResolver : public QObject
    {
      Q_OBJECT
 
    public:
      BonjourResolver(QObject *parent);
      ~BonjourResolver();
 
      void resolveBonjourRecord(const BonjourRecord &record);
 
    signals:
      void recordResolved(const QHostInfo &hostInfo,
                          int port);
      void error(DNSServiceErrorType error);
 
    private slots:
      void bonjourSocketReadyRead();
      void cleanupResolve();
      void finishConnect(const QHostInfo &hostInfo);
 
    private:
      static void DNSSD_API bonjourResolveReply(DNSServiceRef,
            DNSServiceFlags, quint32, DNSServiceErrorType,
            const char *, const char *, quint16, quint16,
            const char *, void *);
 
      DNSServiceRef dnssref;
      QSocketNotifier *bonjourSocket;
      int bonjourPort;
    };

The BonjourResolver is a bit more complicated than the other classes presented thus far, but it still follows the same general pattern that we've seen in BonjourRegistrar and BonjourBrowser.

    void BonjourResolver::resolveBonjourRecord(const BonjourRecord &record)
    {
      if (dnssref) {
        qWarning("Resolve already in process");
        return;
      }
 
      DNSServiceErrorType err = DNSServiceResolve(&dnssref, 0,
            0, record.serviceName.toUtf8().constData(),
            record.registeredType.toUtf8().constData(),
            record.replyDomain.toUtf8().constData(),
            bonjourResolveReply, this);
      if (err != kDNSServiceErr_NoError) {
        emit error(err);
      } else {
        int sockfd = DNSServiceRefSockFD(dnssref);
        if (sockfd == -1) {
          emit error(kDNSServiceErr_Invalid);
        } else {
          bonjourSocket = new QSocketNotifier(sockfd,
                                 QSocketNotifier::Read, this);
          connect(bonjourSocket, SIGNAL(activated(int)),
                  this, SLOT(bonjourSocketReadyRead()));
        }
      }
    }

The resolveBonjourRecord() function is fairly straightforward. We can only resolve one record at a time and it takes a little time to resolve, so we return if a resolve is in progress. Otherwise, we call DNSServiceResolve() with our DNSServiceRef, the values of the BonjourRecord, our callback, and the object to use as the callback's context.

Again, if the call succeeds, we create a socket notifier for the socket associated with the DNSServiceResolverRef. Errors go through the error() signal.

    void BonjourResolver::bonjourResolveReply(DNSServiceRef,
          DNSServiceFlags, quint32,
          DNSServiceErrorType errorCode, const char *,
          const char *hostTarget, quint16 port, quint16,
          const char *, void *context)
    {
      BonjourResolver *resolver =
            static_cast<BonjourResolver *>(context);
      if (errorCode != kDNSServiceErr_NoError) {
        emit resolver->error(errorCode);
        return;
      }
 
    #if Q_BYTE_ORDER == Q_LITTLE_ENDIAN
      port = ((port & 0x00ff) << 8) | ((port & 0xff00) >> 8);
    #endif
      resolver->bonjourPort = port;
      QHostInfo::lookupHost(QString::fromUtf8(hostTarget),
            resolver, SLOT(finishConnect(const QHostInfo &)));
    }

In our callback, we make sure to convert to the proper byte-order for the machine we are on and store the port number. We then get the host name and call QHostInfo::lookupHost(), passing the results to the finishConnect() slot.

    void BonjourResolver::finishConnect(
          const QHostInfo &hostInfo)
    {
      emit recordResolved(hostInfo, bonjourPort);
      QMetaObject::invokeMethod(this, "cleanupResolve",
                                Qt::QueuedConnection);
    }

The finishConnect() slot receives the actual QHostInfo. We emit that with the port number. This information can be used by Qt network classes such as QTcpSocket, QUdpSocket, and QSslSocket.

Example: Fortune Server and Client

Now that we have the classes to deal with Bonjour, we can start taking a look at where we could use them. We actually don't have to wander too far. Among the networking examples located in Qt's examples/network directory, we have the Fortune Server and the Fortune Client. As they stand now, the user must specify the server's IP address and port number in the client. These are ideal candidates for using Bonjour.

Let's do a quick port starting with the Fortune Server.

    BonjourRegistrar *registrar = new BonjourRegistrar(this);
    registrar->registerService(
          BonjourRecord(tr("Fortune Server on %1")
                        .arg(QHostInfo::localHostName()),
                        "_trollfortune._tcp", ""),
                        tcpServer->serverPort());

Basically, we just add a BonjourRegistrar and register our service with a unique name. We use the name "Fortune Server" along with the hostname and the port number. We've created a service type called "_trollfortune._tcp" that will work for our example. If we were going to put this example out in the real world, it would be necessary to register our service type (for free) at http://dns-sd.org/ to make sure we don't collide with other service types. We should also hook up our error handling as well, but to keep the example simple we forego it at this point.

We could do a similar thing for the Threaded Fortune Server. However, the Fortune Client needs a bit of a bigger change. Here are the relevant parts from the constructor:

    BonjourBrowser *browser = new BonjourBrowser(this);
    treeWidget = new QTreeWidget(this);
    treeWidget->setHeaderLabels(
            QStringList() << tr("Available Fortune Servers"));
    connect(browser,
            SIGNAL(currentBonjourRecordsChanged(...)),
            this, SLOT(updateRecords(...)));
    ...
    connect(getFortuneButton, SIGNAL(clicked()),
            this, SLOT(requestNewFortune()));
    ...
    browser->browseForServiceType("_trollfortune._tcp");

We remove the two QLineEdits for the IP address and port number, and then we create a BonjourBrowser as well as a QTreeWidget to present the list of servers to the users. We convert the currentBonjourRecordsChanged() signal to an updateRecords() slot in the client. We also change the slot that gets called when the Get Fortune button is clicked.

    void Client::updateRecords(
          const QList<BonjourRecord> &list)
    {
      treeWidget->clear();
      foreach (BonjourRecord record, list) {
        QVariant variant;
        variant.setValue(record);
        QTreeWidgetItem *processItem =
              new QTreeWidgetItem(treeWidget,
                         QStringList() << record.serviceName);
        processItem->setData(0, Qt::UserRole, variant);
      }
 
      if (treeWidget->invisibleRootItem()->childCount() > 0)
        treeWidget->invisibleRootItem()->child(0)
                                       ->setSelected(true);
      enableGetFortuneButton();
    }

In the updateRectords() slot, we clear all the items in the QTreeWidget. We then iterate through the records in the list and create a QTreeWidgetItem for each record, displaying the service name. We also store the complete BonjourRecord into a QVariant and add it as extra data to the QTreeWidgetItem. Finally, we select the first item in the tree and call the existing enableGetFortuneButton() function.

    void Client::requestNewFortune()
    {
      getFortuneButton->setEnabled(false);
      blockSize = 0;
      tcpSocket->abort();
      QList<QTreeWidgetItem *> selectedItems =
            treeWidget->selectedItems();
      if (selectedItems.isEmpty())
        return;
 
      if (!resolver) {
        resolver = new BonjourResolver(this);
        connect(resolver,
              SIGNAL(recordResolved(const QHostInfo &, int)),
              this,
              SLOT(connectToServer(const QHostInfo &, int)));
      }
      QTreeWidgetItem *item = selectedItems.first();
      QVariant variant = item->data(0, Qt::UserRole);
      bonjourResolver->resolveBonjourRecord(
                              variant.value<BonjourRecord>());
    }

When the user clicks the Get Fortune button, we attempt to get the currently selected QTreeWidget item. If we have one, we create a BonjourResolver and connect its signal to the original connectToServer() slot. Finally, we get the BonjourRecord out of the QTreeWidgetItem and then call resolveBonjourRecord() to obtain the server's IP address and port number. The rest of the client code is the same as before and works exactly the same.

Fortune client

Aside from the changes to the code above, we also need to link to the Bonjour library on platforms other than Mac OS X. This can be done by adding the following line to the profile.

!mac:LIBS += -ldns_sd

Conclusion

The examples presented here show how easy it is to add Bonjour support to an existing application once we have good classes to wrap the Bonjour protocol. The Bonjour wrapper classes presented here probably handle over 90"%" of the cases. However, there could be some things that would make them more suitable for general purpose use:

  • Give the BonjourRegistrar the ability to register multiple services. A good way to do this would be to have only the static public methods, registerService() and unregisterService(), and use Bonjour's API for registering multiple services.
  • Merge the BonjourResolver into BonjourRecord and have it work more like QHostInfo::lookupHost().
  • Add support for wide area networking Bonjour.
  • Add support for SRV records.
  • Add better error handling.

In the tradition of great computer science textbooks, these are left as exercises for the reader.

The source code for the examples in this article is also available.