DNS Service Discovery On Windows
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 none of the 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:
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:
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:
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:
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
my 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 |