In a previous post I showed you how we use DNS Service Discovery in a product I work on for Cisco Systems. That project uses the Avahi browser, which does not have a Windows port. In this article, I’ll show you how to perform service discovery on Windows using Apple’s Bonjour SDK for Windows.

DNS-SD On Windows

Microsoft has been pushing us to use UPnP as our network discovery protocol, to the exclusion of all others. As a result, Windows ships with no support for DNS-SD – zip. And that might be the end of it, if it weren’t for Apple’s vested interest in having iTunes installed on every Windows machine in the world.

iTunes uses DNS-SD to share music catalogs across Local Area Networks – a natural choice with the native support in OS X for Zeroconf. Rather than isolate Windows users in an pocket UPnP universe, Apple chose to instead port the service discovery components of Bonjour to Windows, and install it with every copy of Windows. Thus, Windows and OS X users can happily share their iTunes libraries with no unusual calisthenics required.

To sweeten the deal, Apple has released a Bonjour SDK for Windows, which is currently shipping version 3.0 from their developer support site. In Apple’s words:

The Bonjour SDK for Windows includes the latest version of Bonjour as well as header files, libraries, and sample code to assist developers in creating Bonjour enabled applications on Windows. The SDK has been updated with the Bonjour core that is bundled with iTunes 10.3.1. This release will bring all existing Bonjour functionality released in Mac OS X 10.7 into the Bonjour for Windows product.

It sounds pretty good, doesn’t it?

An Emphasis on Kit

Installing the SDK is a decision-free breeze:

Installing the SDK


The installation may be easy, but the real dirty truth about the Bonjour SDK is that it is not much of an SDK at all. The developer interface to Bonjour services are packaged in a single DLL, and the SDK provides programmers with sample code that illustrates how to use some of the bindings in C, C#, Java, and VB. There is no documentation on the interfaces, no source for the Bonjour components, and the few samples don’t begin to provide comprehensive coverage of the interface.

In other words, this is pretty much a code dump.

An Overview

For the most part, the Bonjour SDK interfaces follow a single pattern. Each request made to the API returns immediately, and gives you a reference that you can use to track the progress of your request. That reference can be converted to a file handle, and you can use the file handle to see when your request has some data to produce.

When your request has generated some data, and the Bonjour components are ready to deliver it to you, they do so by a callback mechanism – the Bonjour components make calls into your C or C++ program and provide the data you requested.

Most of the callbacks include a flag parameter. You can check the flag to see if there is any more data expected. If there isn’t, you can delete the reference and you are then done with that particular call. Otherwise, you will have to wait for the request to be responded to when the Bonjour components get around to it.

An Example – Discover Service Types

I’ve written a demo program that browses the network for details on every service instance it can find, and presents the results in a tree format. The figure below shows the program running on my home network, where I have drilled down to get information about an instance of a print service.

The ServiceBrowser Sample Program


This program has to start by doing a top-level iteration of all the service types currently seen on my network. From my previous article, you might remember that I can use a special browse command to accomplish this. If you’ve installed the Bonjour SDK, you can run the dns-sd program and browse for service type _services._dns-sd._udp, which should give results something like this:

C:\Users\Mark>dns-sd -B _services._dns-sd._udp
Browsing for _services._dns-sd._udp
Timestamp     A/R Flags if Domain  Service Type  Instance Name
17:38:09.407  Add     3 13 .       _tcp.local.     _smb
17:38:09.407  Add     3 13 .       _tcp.local.     _printer
17:38:09.408  Add     3 13 .       _tcp.local.     _pdl-datastream
17:38:09.408  Add     3 13 .       _tcp.local.     _http
17:38:09.408  Add     3 13 .       _tcp.local.     _tivo-videos
17:38:09.409  Add     2 13 .       _tcp.local.     _csco-sb

Looking to do the same thing in my demo program, I turn to the header file dns_sd.h. This file ships with the Bonjour SDK and contains not only the API definition, but what passes for documentation. In that header file I see that there is a function called DNSServiceBrowse, and it looks like it does exactly what I want. In my sample program, my call to this routine is shown below:

DNSServiceRef client = NULL;
DNSServiceErrorType err;
err = DNSServiceBrowse( &client, 
                        0, 
                        0, 
                        "_services._dns-sd._udp", 
                        "", 
                        IterateServiceTypes, 
                        this );

You can get some exposition on each of the arguments I pass to the function from the header file, my brief comments on each are given below:

sdRef Every call to the Bonjour API creates a new DNSServiceRef handle. It is initialized by the function call, and used later to retrieve the results.
flags This parameter is not used in this version of the SDK.
interfaceIndex This argument is used to select a specific network interface. For this particular function I want to browse on all available interfaces, so a value of 0 is used.
regType The type of service being browsed for. Normally when you call DNSServiceBrowse you will use this parameter to specify a specific service type that you are interested in, such as _http._tcp. I’m using the special type of _services._dns-sd._udp in order to get a list of all published service types, not specific instances.
domain By passing an empty string I tell the service that I want to see advertisements from all domains.
callback The pointer to a callback function that will receive the responses to this request. The argument I pass in, IterateServiceTypes, is a static member of my MCF Dialog class.
context The context variable is an opaque pointer type that is passed in to the Bonjour service. When it performs a callback, it will include a copy of the context pointer for the use of the callback function. I always pass in a pointer to my MFC Dialog class, so my callback functions have full access to the class members.

The important thing to note here is that the call to the API function doesn’t return any important data. All I get back is an error code indicating that the request is being processed, and a DNSServiceRef handle that I use to track that progress.

The real action comes when my callback function is invoked. IterateServiceTypes is a static member of my Dialog class. Apple kept things simple by having one callback type for C and C++, which means no member functions. You could easily build shims to make it appear as though the DLL was calling member functions – it would just take a small modification to the code I’ll show you here.

The function definition has to follow exactly the declaration given in dns_sd.h. My implementation starts like this:

void DNSSD_API CServiceBrowserDlg::IterateServiceTypes( DNSServiceRef sdRef,
                                                        DNSServiceFlags flags,
                                                        uint32_t interfaceIndex,
                                                        DNSServiceErrorType errorCode,
                                                        const char *serviceName,
                                                        const char *regtype,
                                                        const char *domain,
                                                        void *context )
{
	CServiceBrowserDlg *p = (CServiceBrowserDlg *) context;

It’s worth walking through a look at each of the parameters in the callback:

sdRef This is the same DNSServiceRef value that was created when the call was made to DNSServiceBrowse. You don’t need to make any use of it in the callback, but it does provide a good way to correlate results with function calls, particularly if you are using a single callback function to process many different results.
flags There are two important flag bits to check in this value. The first value, kDNSServiceFlagsMoreComing is used to indicate that there are definitely more callbacks coming. If that bit is cleared, there is no more pending data. The second flag bit, kDNSServiceFlagsAdd is used to indicate whether this service is being added or deleted. When you first start browsing, all the callbacks will be for services added. As services are added and removed from the system, additional callbacks will be generated with this bit both set and cleared.
interfaceIndex In the callback, this index will be set to the index of the network interface where the advertisement was found. When it comes time to resolve this service, you need to pass in the correct index.
errorCode If this value is not zero, the callback is indicating an error. As long as it is zero your code can process the input safely.
serviceName This value contains the name of a discovered service – it is the whole point of the callback. Normally this will contain the name of an instance of a service. However, when browsing for the special name _services._dns-sd._udp, the instance name is actually a service type.
regtype The type of the service – you may already know this information by the time you reach the callback, but if the callback is handling the results from multiple queries, it can be helfpul.
domain The domain of the discovered service. Like the interface index, you need use the domain when you are attempting to resolve the service
context A copy of the context variable passed in when the browse call was made.

If you look at the first line of code above, the first thing I do is cast the context pointer to its correct type, a pointer to my MFC Dialog class. Now I can make full use of all the members of the class, albeit via a call through a pointer instead of directly.

So what do I do with these services once I receive them? Well, for each service type that I find, I kick off a new browse process, looking for specific instances of the service. Just as an example, in my callback IterateServiceTypes, one of the callbacks returns a service type of _printer._tcp. In order to find all instances of this service, I have to call DNSServiceBrowse again, with that service name and the correct interface and domain. After inserting the service type into the tree, I make that call so I can start adding those instances:

HTREEITEM item = p->m_Tree.InsertItem( CA2T(service_type.c_str()), TVI_ROOT, TVI_SORT );
DNSServiceRef client = NULL;
DNSServiceErrorType err;
err = DNSServiceBrowse( &client, 
                        0, 
                        0, 
                        service_type.c_str(), 
                        "", 
                        IterateServiceInstances, 
                        context );

The key point to note about this call is that the callback function, IterateServiceInstances, is a different member function – one that expects to get the results of my browsing for instances of a specific service.

Driving the Callbacks

One thing I’ve skipped over so far – how do these callbacks actually get generated? Does the DLL asynchronously make calls into my code whenever events occur?

The Bonjour SDK lets your program control when callbacks occur by giving you the handle to the message pump. When you call DNSServiceProcessResult() with a single argument of a DNSServiceRef, you will generate a single callback message for the given reference. The callback will occur within the context of the call to DNSServiceProcessResult().

When you call DNSServiceProcessResult(), the Bonjour DLL will block if there are no messages ready to process. So how do you know when there are messages ready?

The indicator that messages are ready is given by a file descriptor associated with the DNSServiceRef. You can get a copy of the file descriptor by calling DNSServiceRefSockFD(), passing in a copy of the reference. When the file descriptor has data ready to read, you have callbacks pending. The easiest way to check this condition is to use the select() function, which can check multiple references in one fell swoop.

In my implementation of the callback message pump, I rely on an unordered_map called m_ClientToFdMap that contains a copy of all the DNSServiceRef references currently waiting for responses. I create the necessary data structure used by select(), then call it to get a list of all references that have callbacks pending. The core of this code looks like this:

int result = select(0, &readfds, (fd_set*)NULL, (fd_set*)NULL, &tv);
if ( result > 0 ) {
//
// While iterating through the loop, the callback functions might delete
// the client pointed to by the current iterator, so I have to increment
// it BEFORE calling DNSServiceProcessResult
//
    for ( auto ii = m_ClientToFdMap.cbegin() ; ii != m_ClientToFdMap.cend() ; ) {
        auto jj = ii++;
        if (FD_ISSET(jj->second, &readfds) ) 
            DNSServiceErrorType err = DNSServiceProcessResult(jj->first);
    }
}

This generates my callbacks efficiently, and because they are in the context of my main program’s UI thread, I avoid a lot of unpleasant issues.

Threading Issues

My program manages the Bonjour callbacks in a fairly ugly fashion. When my browsing activity starts, I create a timer that fires once every 250 milliseconds. I process up to 10 callbacks in that timer call, then exit. This continues until there are no pending browser or resolution requests, at which time I kill the timer.

Depending on your use of DNS-SD, you may find that this is not as efficient as you like. If this is the case, you might find it useful to move your message pump code to a separate thread.

Once you do that, you can wait on all your callbacks by calling select with a long or infinite timeout. This has the effect of blocking your callback thread until it has actual work to do – resulting in a better use of CPU time.

There are some obvious downsides to this approach. Clearly you have to use some sort of locking mechanism on the data structures that are shared between your callback thread and the rest of your program. And the use of the select() statement with an infinite timeout is complicated by the possibility that you may be making or canceling browsing or resolution calls while your program runs.

A good way to deal with both of these problems is to invoke a socket-based message passing protocol between the callback thread and the other components of your program. If you restrict your interface to messages, you don’t have to worry about locking access to shared data. And because you are using a socket for communications, your select() statement will be used to activate the thread when new messages arrive.

Character Sets

The days when DNS was limited to seven-bit ASCII characters are long gone. Service instances are encoded as UTF-8, and can use whatever Unicode characters they like. In the figure shown below, you can see the effects of that when I browse for instances of iTunes:

Character set problems in service instance names


You can see that OS X users have so-called curly quotes in their library instance names, and curly quotes are definitely outside the range of seven-bit ASCII. DNS-SD collects the names as UTF-8 encoded strings, and sends them to the console in that format.

By default, the Windows cmd.exe window doesn’t render UTF-8 properly, but changing the code page to 65001 results in the correct rendering.

In my sample program, I deal with this with a two step approach. First, my program is built using the Unicode libraries, ensuring that I am able to render Unicode output properly. To conform with Microsoft’s C++ paradigms, I use CString for all my Unicode strings, and wrap all my string literals in the _T() wrapper.

This works fine for my UI, but I can’t use strings built of wchar_t to communicate with the Bonjour SDK – it expects eight bit characters with UTF-8 encoding. In m program I use the C++ std::string class everywhere where I am working with 8-bit characters that might be encoded in UTF-8. When it comes time to render one of those strings in my Unicode context, all I have to do is use the handy CA2T macro with the CP_UTF8 parameter, and things work properly.

Library Issues

The design of the Bonjour SDK imposes some uncomfortable restrictions on you when it comes to building your C or C++ program. Because you are linking directly to code found in the library dnssd.lib, you have to ensure that your program and that library link against the same version of the C run time library. And for the Bonjour SDK under Windows, this means you must link with the static, multithreaded, release version of the library.

You’ll see the problem in this right away when you create an MFC project and try to build with dnssd.lib. By default, the project generator will probably have you using MFC in a shared DLL, and using the Multithreaded Debug DLL version of the C libraries. When you try to build like this, you will get some unpleasant error messages:

1>LINK : warning LNK4098: 
         defaultlib 'msvcrtd.lib' conflicts with use of other libs; 
         use /NODEFAULTLIB:library
1>LINK : warning LNK4098: 
         defaultlib 'LIBCMT' conflicts with use of other libs; 
         use /NODEFAULTLIB:library

A full featured SDK would provide libraries built for multiple scenarios, and you would pick the one of your choice depending on your build parameters. But with the Bonjour SDK, you don’t get this choice, so you need to ensure that your project follows a few guidelines:

  • Under Configuration Properties/General, field Use of MFC needs to be set to Use MFC in a Static Library for both debug and release builds.
  • Under Configuration Properties/C++/Code Generation, field Runtime Library needs to be set to Multi-threaded (/MT) for both debug and release builds.
  • Under Configuration Properties/C++/Preprocessor, field Preprocessor Definitions the constant _DEBUG needs to be changed to NDEBUG for Debug configurations.

To build a project that uses the SDK, you will also need to add dns_sd.lib to your list of linker inputs, add dns_sd.h to your header files, and add the appropriate directories in the configuration under Configuration Properties/C++/General/ in field Additional Include Directories, and under Configuration Properties/Linker/General/ in field Additional Include Directories.

Overview Of the Demo Program

I’ve included the full source for a project that will build with Visual Studio 10, as long as you have the Bonjour SDK installed. It browses all available services on the network and displays the information about them in a tree form.

The program starts by kicking off a browser for _services._dns-sd._udp. The results are processed in member function IterateServiceTypes(). As each new service type is discovered, it is added to the tree, and a call to DNSServiceBrowse()is made to discover all instances of that service type. The callback for that browse call is member function IterateServiceInstances().

In IterateServiceInstances() I add the instance to the tree, then call DNSServiceResolve(). This function operates much like the browse function, but it actually gets the DNS record for the service. This record contains the host name, service port and a list of name/value pairs that a service can advertise as part of its record. You can see those values put to good work with service types like _ipp._tcp, in which printer parameters are exposed as part of service discovery.

ResolveInstance() is the callback routine that receives the information about the service instance. The host name, port, and name/value pairs are added to the tree, and then one final call is made to a Bonjour SDK entry called DNSServiceGetAddrInfo(). This function resolves the IP address for the given host name. The address is stuffed into the tree in callback function GetAddress().

Conclusion

DNS service discover is powerful tool, but Windows programmers might be put off by the lack of a nicely packaged SDK. Using this simple example program might be a good way to get comfortable with an SDK that gives you a powerful tool that provides a good multi-platform alternative to UPnP.

Sample program source: ServiceBrowser.zip
Sample program executable: ServiceBrowserExe.zip