Zero-Configuration Networking in Qt

Материал из Wiki.crossplatform.ru

Перейти к: навигация, поиск
Image:qt-logo_new.png Image:qq-title-article.png
Qt Quarterly | Выпуск 23 | Документация

by Trenton Schulz

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

Содержание


[править] Who's Who: Zeroconf, Bonjour, Avahi

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

Traditionally, you would have to make sure that each machine isconfigured correctly and on the network. Zeroconf takes care of allof this for you for a local area network. Lots of new hardware, suchas printers with networking support or wireless routers, come withtheir own Zeroconf server to allow easy network configuration. OnMac OS X, many applications take advantage of Bonjour to advertiseservices, such as the ssh server, iTunes shares, or iChatavailability. Zeroconf is a powerful way of simplifying yourapplications, and there are implementations available for mostoperating systems.

For this article we use Apple's implementation of Zeroconf calledBonjour. Bonjour consists of the mDNSResponder daemon and themulticast DNS service discovery library to interface with the daemon.While early releases of Bonjour used a controversial open sourcelicense, the current daemon is released under thestandard Apache 2.0 license, while the library itself is under theliberal "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 installBonjour in relatively short order. Most modern Linux distributionscome with Avahi, an LGPL implementation of Zeroconf with acompatibibility API for Bonjour. The examples presented here weretested to work with both Apple's Bonjour implementation and Avahi'sBonjour compatibility layer.

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

[править] Storing a Bonjour Entry

To begin using Bonjour, we first create a simple class to contain allthe 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 publicmembers 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 Qtmeta-type so we can store a BonjourRecord in a QVariant oremit it in a cross-thread signal.

We will now review the Bonjour wrapper classes, which provide ahigh-level Qt API to Bonjour. The classes are calledBonjourRegistrar, BonjourBrowser, and BonjourResolver, andfollow the same pattern: We first let Bonjour know we are interested inan activity (registering, browsing, or resolving) by registering acallback to get that information. Bonjour then gives us a datastructure and socket to let us know when more information is ready.When information is ready, we pass the structure back to Bonjour, andBonjour 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, theservice stays registered. There are some signals to check the resultsof the registration, but they aren't strictly necessary tosuccessfully register a service. The rest of the class wraps thevarious bits of Bonjour service registration. The static callbackfunction is marked with the DNSSD_API macro to make sure that thecallback 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 bonjourSocketmember variables. We will use them later on when we register theservice. In our destructor, if we've registered a service, wedeallocate 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 &amp; 0x00ff) << 8)
                      | ((servicePort &amp; 0xff00) >> 8);
    #endif
      DNSServiceErrorType err = DNSServiceRegister(&amp;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 takesplace. First, we check if we've already registered a service for thisobject. If that's the case, we warn and return. Bonjour requires thatwe register the port in big-endian byte order; therefore,we convert the port number if we are on a little-endian machine. (Qt4.3 introduced qToBigEndian(), which we could use to make ourcode a bit prettier.)

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

If the call to DNSServiceRegister() fails, we emit the errorthrough our error() signal. By connecting to this signal, slotscan do whatever error handling they want. If DNSServiceRegister()succeeds, we get the socket descriptor associated with the serviceand create a QSocketNotifier for it to let us know when we needto "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 theinformation on the socket. This will invoke our callback. Again, ifthere 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 weprovided in registerService() since there could have been anotheritem providing the service with that name already. In this case,Bonjour returns a new unique name to us. We then emit theserviceRegistered() signal with that information.

[править] Browsing Through Available Bonjour Services

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

    class BonjourBrowser : public QObject
    {
      Q_OBJECT
 
    public:
      BonjourBrowser(QObject *parent = 0);
      ~BonjourBrowser();
 
      void browseForServiceType(const QString &amp;serviceType);
      QList<BonjourRecord> currentRecords() const
        { return bonjourRecords; }
      QString serviceType() const { return browsingType; }
 
    signals:
      void currentBonjourRecordsChanged(
            const QList<BonjourRecord> &amp;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 seenin the BonjourRegistrar class. Once we've created aBonjourBrowser and told it what service we want to look for, itwill let us know which hosts currently provide that service, and informus of any changes via its currentBonjourRecordsChanged() signal.

Let's look at the browseForServiceType() function:

    void BonjourBrowser::browseForServiceType(
          const QString &amp;serviceType)
    {
      DNSServiceErrorType err = DNSServiceBrowse(&amp;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 tostart browsing for a specific type of service. Internally, we callDNSServiceBrowse(), which has a similar signature toDNSServiceRegister(). If the call succeeds, we create a QSocketNotifier for the associated socket and connect itsactivated() signal to the bonjourSocketReadyRead() slot. Thisslot is identical to the slot of the same name inBonjourRegistrar, 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 &amp; kDNSServiceFlagsAdd) {
          if (!browser->bonjourRecords.contains(record))
            browser->bonjourRecords.append(record);
        } else {
          browser->bonjourRecords.removeAll(record);
        }
        if (!(flags &amp; kDNSServiceFlagsMoreComing)) {
          emit browser->currentBonjourRecordsChanged(
                                     browser->bonjourRecords);
        }
      }
    }

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

The flags indicate if a service has been added orremoved; we update our QList accordingly. If a service is added,we first check to see if the record doesn't already exist in our listbefore adding it. (We could get a record twice if, for example, weare offering a service locally and it would be available through theloopback device and our Ethernet port.) Since the callback can onlybe called for one record at a time, it also specifies thekDNSServiceFlagsMoreComing flag to indicate that it is going tobe called again shortly. When all the records have been sent, thisflag will be cleared and then we emit ourcurrentBonjourRecordsChanged() signal with the current set ofrecords.

[править] Resolving Bonjour Services

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

    class BonjourResolver : public QObject
    {
      Q_OBJECT
 
    public:
      BonjourResolver(QObject *parent);
      ~BonjourResolver();
 
      void resolveBonjourRecord(const BonjourRecord &amp;record);
 
    signals:
      void recordResolved(const QHostInfo &amp;hostInfo,
                          int port);
      void error(DNSServiceErrorType error);
 
    private slots:
      void bonjourSocketReadyRead();
      void cleanupResolve();
      void finishConnect(const QHostInfo &amp;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 otherclasses presented thus far, but it still follows the same generalpattern that we've seen in BonjourRegistrar andBonjourBrowser.

    void BonjourResolver::resolveBonjourRecord(const BonjourRecord &amp;record)
    {
      if (dnssref) {
        qWarning("Resolve already in process");
        return;
      }
 
      DNSServiceErrorType err = DNSServiceResolve(&amp;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. Wecan only resolve one record at a time and it takes a little time toresolve, so we return if a resolve is in progress. Otherwise, we callDNSServiceResolve() with our DNSServiceRef, the values of theBonjourRecord, our callback, and the object to use as thecallback's context.

Again, if the call succeeds, we create a socket notifier for thesocket associated with the DNSServiceResolverRef. Errors gothrough 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 &amp; 0x00ff) << 8) | ((port &amp; 0xff00) >> 8);
    #endif
      resolver->bonjourPort = port;
      QHostInfo::lookupHost(QString::fromUtf8(hostTarget),
            resolver, SLOT(finishConnect(const QHostInfo &amp;)));
    }

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

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

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

[править] Example: Fortune Server and Client

Now that we have the classes to deal with Bonjour, we can starttaking a look at where we could use them. We actually don't have towander too far. Among the networking examples located in Qt'sexamples/network directory, we have the Fortune Server and theFortune Client. As they stand now, the user must specify the server'sIP address and port number in the client. These are ideal candidatesfor 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 ourservice with a unique name. We use the name "Fortune Server" alongwith the hostname and the port number. We've created a service typecalled "_trollfortune._tcp" that will work for our example. If wewere going to put this example out in the real world, it would benecessary to register our service type (for free) athttp://dns-sd.org/ to make sure we don't collide with otherservice types. We should also hook up our error handling as well, butto 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 therelevant 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 QTreeWidgetto present the list of servers to the users. We convert thecurrentBonjourRecordsChanged() signal to an updateRecords()slot in the client. We also change the slot that gets called when theGet Fortune button is clicked.

    void Client::updateRecords(
          const QList<BonjourRecord> &amp;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 andcreate a QTreeWidgetItem for each record, displaying the servicename. 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 existingenableGetFortuneButton() 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 &amp;, int)),
              this,
              SLOT(connectToServer(const QHostInfo &amp;, 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 getthe currently selected QTreeWidget item. If we have one,we create a BonjourResolver and connect its signal to theoriginal connectToServer() slot. Finally, we get theBonjourRecord out of the QTreeWidgetItem and then callresolveBonjourRecord() to obtain the server's IP address and portnumber. The rest of the client code is the same as before and worksexactly the same.

center

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

!mac:LIBS += -ldns_sd

[править] Conclusion

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

  • Give the BonjourRegistrar the ability to register multipleservices. A good way to do this would be to have only the static publicmethods, registerService() and unregisterService(), and useBonjour's API for registering multiple services.
  • Merge the BonjourResolver into BonjourRecord and have itwork 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 leftas exercises for the reader.

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