The preceding chapters covered the DNS Service Discovery programming APIs developed primarily by Apple’s engineers, which include both cross-platform and Mac-specific APIs. The cross-platform C and Java APIs are available on Macintosh, Microsoft Windows, Linux, FreeBSD, Solaris, and other Unix variants. The Mac-specific Core Foundation API and Cocoa API are available for programmers writing software designed solely for Mac OS X.
Those are just the tip of the iceberg. There exist a range of open source projects (e.g., ones that implement higher-level DNS Service Discovery API layers built on top of the C DNSServiceDiscovery API foundation provided by Apple), some of which complement Apple’s work, and some of which overlap or even compete with it. This is made possible by the careful separation of the background daemon and the client library in Apple’s own implementation of Multicast DNS and DNS Service Discovery.
The first component is a background daemon, which runs in its own address space and implements all the protocol logic, timing, packet sending and reception, record caching, and similar functionality. The second component is the client library, which client applications link with, in the application’s address space, in order to communicate with the background daemon. Apple’s background daemon is licensed under the Apple Public Source License 2.0, an FSF-approved open source license. Apple’s client library is licensed under an even more liberal three-clause BSD-style license, which allows it to be used in just about anything, from the most secretive proprietary products to the most resolutely open projects using open source licenses such as the GNU Public License (GPL).
This separation into two components offers benefits both technical and legal. The technical benefit is that all the client programs on a machine get the efficiency benefit of sharing a common protocol engine and a common record cache. Also, if a client advertising a service crashes, the daemon detects that and sends the goodbye packet to de-register the advertised service. From a legal standpoint, this separation allows greater freedom for everyone involved. The background daemon is separated from its programming interface, so other independent implementations of the background daemon—with different characteristics and licensing terms—are possible. Meanwhile, the client library, the interface a client program uses to communicate with the daemon, doesn’t have to change. Commercial software vendors can link with Apple’s client library without the fear of “tainting” sometimes associated with linking GPL libraries, and, at the same time, authors working on GPL programs can also safely link with Apple’s client library without fear that they might be violating the strict terms of the GPL.
Similarly, programmers can create their own layers built on top of the C client API, with the freedom to license them as they choose, while users are free to mix and match components as best meets their needs. The key to this flexibility is that as long as all the client layers build on top of the common C API—with its BSD-style license—then all a programmer needs to do to create an alternative daemon implementation is to create a background daemon offering that standard C API, and all of the higher-level client programs and API layers can work with it without change.
In this chapter, we cover two of those higher-level third-party DNS Service Discovery API layers built on top of the C DNSServiceDiscovery API foundation. The first is a language binding for the Ruby programming language. The second is a more general-purpose interface specification written for the Simplified Wrapper and Interface Generator (SWIG), which is used to generate interfaces automatically for a range of languages. That range of languages includes Java and Ruby, which might make you wonder why there are also specific APIs for those languages. The answer is that SWIG does a fairly mechanical translation of the C interface into other languages, yielding an interface with the same basic operating model. For example, just as calls in the C interface hand you back a file descriptor, which you add to your event loop, the SWIG-derived interfaces do the same. In contrast, the specific APIs for Java and Ruby both take advantage of those languages’ inherent multithreading support to provide callbacks that are invoked automagically on some other thread. We explore the SWIG interfaces from the perspective of the Python programming language, which doesn’t have its own handcrafted API at this time.
Finally, this chapter wraps up with a brief mention of the mDNSEmbeddedAPI.h interface, used by hardware devices that don’t really need the benefits of an independent mdnsd background process and instead use a monolithic piece of software dedicated to advertising the services of that device, using the core Multicast DNS functions directly.
At publication time, the current version of the Ruby DNS Service Discovery (DNS-SD) interface is 0.6.0. If you use RubyGems to install and manage your Ruby libraries, then you can install Ruby DNS-SD by typing:
sudo gem install dndsd
If you don’t use RubyGems, you can get Ruby DNS-SD by downloading the dnssd-0_6_0.tar.gz file from http://rubyforge.org/projects/dnssd/ and running the three commands shown in the README file:
ruby setup.rb config ruby setup.rb setup ruby setup.rb install
In future versions of OS X and other operating systems, it may come preinstalled by default. You can tell if it’s already preinstalled with the following command:
ruby -e "require 'dnssd'"
If there’s no error message, then you have the Ruby dnssd package already installed. If it prints “No such file to load...,” you will need to install it. Once you have the Ruby DNS-SD API installed, you can experiment with registering, browsing, and resolving from a Ruby program.
The Ruby code to advertise a service is just a few lines:
require 'dnssd' registration = DNSSD.register("", "_http._tcp", nil, 8080) do |register_reply| puts "Registration result: #{register_reply.inspect}" end puts "Registration started" sleep 30 registration.stop puts "Registration stopped"
This code:
Tells the Ruby language interpreter that it requires the dnssd package
Registers a pretend service of type _http._tcp
using the computer’s default service name
Prints out the advertised name when the service is successfully registered
Waits for 30 seconds, then stops the registration and exits
If you save this file as register.rb and run it, you should see something like this:
% ruby register.rb Registration started Registration result: #<DNSSD::RegisterReply Stuart 32Cheshire's 32PowerBook 032G4._http._tcp.local.> Registration stopping
If
you’re new to Ruby, some explanation is called for. Ruby supports code blocks as
first-class entities, much like integers or strings in other languages. The block of
code from do
to end
is not equivalent to a similar block of code enclosed within curly
braces in C. In C, lines of code in a routine execute more or less sequentially. The
body of an if
or for
statement enclosed within curly braces may be executed zero, one,
or more times, but it’s always executed before the lines that
appear later in the routine. In Ruby
, the block of code from do
to
end
is actually a
parameter passed to the DNSSD.register()
routine. It doesn’t execute at all until DNSSD.register()
decides it’s time to execute it,
asynchronously, on an automatically created background thread. If you save the
program above as register.rb and then run it by
typing ruby register.rb
, you’ll see that it prints
the “Registration started” message before the “Registration
result...” message.
Following the “do” keyword, you’ll see an identifier enclosed between a pair of vertical bar symbols (the same character as the Unix “pipe” symbol), which names the parameter that is passed to the code block when it executes, rather like the parameter list enclosed in parentheses when you declare a C function.
In some ways, a Ruby code block is somewhat similar to passing
a function pointer in a C program—there’s some code to execute and a parameter that’s
passed to that code—but there’s one important difference. A Ruby code block is what
computer scientists call a closure. It’s not
just the code but also its environment. Similar to the way the
body of an if
or for
statement may access local variables declared in the enclosing
function, the Ruby code block passed to DNSSD.register()
may access local variables declared in the enclosing
function, even though the code block might not even get to execute until after the
function that created it has exited. The Ruby interpreter knows that there’s a code
block that has access to those local variables and makes sure that they continue to
remain valid even after the function that declared them has exited.
If
you want to register a service with TXT record attributes, Ruby DNS-SD supports that
too. The full signature of the DNSSD.register()
function is as shown
here:
DNSSD.register(name, type, domain, port, text_record=nil, flags=0, interface=DNSSD::InterfaceAny) {|reply| block }
Ruby
supports automatic default values for unspecified parameters. In this example, we
didn’t specify anything for text_record
, flags
or interface
,
so they automatically got the default values nil
,
0
, and DNSSD::InterfaceAny
. If you want to register a service with TXT record
attributes, you just need to pass a hash (key/value) or a string in the proper format
for the text_record
parameter.
The general structure of the Ruby code to browse for services follows the same outline as the code to register a service:
require 'dnssd' browser = DNSSD.browse('_http._tcp') do |browse_reply| if (browse_reply.flags.to_i & DNSSD::Flags::Add) != 0 puts "Add: #{browse_reply.inspect}" else puts "Rmv: #{browse_reply.inspect}" end end puts "Browsing started" sleep 30 browser.stop puts "Browsing stopped"
If you save this file as browse.rb and run it, you should see something like this:
% ruby browse.rb Browsing started Add: #<DNSSD:: BrowseReply Stuart 32Cheshire's 32PowerBook 32G4._ http._tcp.local. interface:lo0> Browsing stopping
When you’ve browsed to discover available services, and the user has picked one, the next step is to resolve the named service to its target host and port number:
require 'dnssd' name = ARGV.shift puts "Resolving: #{name}" resolver = DNSSD.resolve(name, "_http._tcp", "local") do |resolve_reply| puts "Resolve result: #{resolve_reply.inspect}" end puts "Resolver started" sleep 30 resolver.stop puts "Resolver stopped"
If you save this file as resolve.rb and run it, you should see something like this:
% ruby resolve.rb "Stuart Cheshire's PowerBook G4" Resolving: Stuart Cheshire's PowerBook G4 Resolver started Resolve result: #<DNSSD::ResolveReply Stuart 32Cheshire's 32PowerBook 32G4. _http._tcp.local. interface:en0 target:chesh7.local.:123> Resolver stopped
In a real program, you’d take the resolve results you get and use them to initiate connections, and as soon as one succeeds, you’d then cancel the ongoing resolve operation.
Python programmers can access DNS-SD functionality via the SWIG interface definition created by Tom Uram, Argonne National Laboratory. At time of writing, you can get it from http://www.mcs.anl.gov/fl/research/accessgrid/bonjour-py/bonjour-py.html or from the link on the DNS-SD web site http://www.dns-sd.org/. All of these examples are included in the bonjour-py package, along with a more involved example: a graphical service browser built using the wxPython GUI toolkit. To build the Python interfaces using SWIG, you’ll need to have SWIG installed; you can get that from http://sourceforge.net/projects/swig/.
The Python code to advertise a service is a little longer than the Ruby code, but it’s still quite simple:
import sys import time import select import socket import bonjour # Callback for service registration def RegisterCallback(sdRef, flags, errorCode, name, regtype, domain, context): print "Service registered:", name, regtype, domain if len(sys.argv) < 4: print "Usage: register.py servicename regtype port" sys.exit(1) servicename = sys.argv[1] regtype = sys.argv[2] port = int(sys.argv[3]) # Allocate a service discovery reference and register the specified service flags = 0 interfaceIndex = 0 domain = '' host = '' txtLen = 0 txtRecord = '' userdata = None serviceRef = bonjour.AllocateDNSServiceRef() ret = bonjour.pyDNSServiceRegister(serviceRef, flags, interfaceIndex, servicename, regtype, domain, host, port, txtLen, txtRecord, RegisterCallback, userdata) if ret != bonjour.kDNSServiceErr_NoError: print "error %d returned; exiting" % ret sys.exit(ret) # Get the socket and loop fd = bonjour.DNSServiceRefSockFD(serviceRef) while 1: ret = select.select([fd], [], []) ret = bonjour.DNSServiceProcessResult(serviceRef) # Deallocate the service discovery ref bonjour.DNSServiceRefDeallocate(serviceRef)
This code:
Defines the callback function to be invoked when the registration succeeds
Calls bonjour.pyDNSServiceRegister
to
register the service
Accesses the serviceRef
’s file
descriptor, adding it to a select()
set and
calling bonjour.DNSServiceProcessResult()
each time data arrives on that socket
If you save this file as register.py and run it, you should see something like this:
% python register.py "" _http._tcp 123 Service registered: Stuart Cheshire's PowerBook G4 _http._tcp. local.
In this case, we’re just using an empty string name to advertise the service using the system default name, but as with the other APIs, if you want something else you can specify an explicit name instead.
It should be little surprise by now to see that the outline of the browsing code looks a lot like the service registration code:
import sys import select import bonjour # Callback for service browsing def BrowseCallback(sdRef, flags, interfaceIndex, errorCode, serviceName, regtype, replyDomain, userdata): if flags & bonjour.kDNSServiceFlagsAdd: print "Service added: ", serviceName, regtype, replyDomain, interfaceIndex else: print "Service removed: ", serviceName, regtype, replyDomain, interfaceIndex if len(sys.argv) < 2: print "Usage: browse.py regtype" sys.exit(1) regtype = sys.argv[1] # Allocate a service discovery ref and browse for the specified service type flags = 0 interfaceIndex = 0 domain = '' userdata = None serviceRef = bonjour.AllocateDNSServiceRef() ret = bonjour.pyDNSServiceBrowse(serviceRef, flags, interfaceIndex, regtype, domain, BrowseCallback, userdata) if ret != bonjour.kDNSServiceErr_NoError: print "ret = %d; exiting" % ret sys.exit(1) # Get socket descriptor and loop fd = bonjour.DNSServiceRefSockFD(serviceRef) while 1: ret = select.select([fd], [], []) ret = bonjour.DNSServiceProcessResult(serviceRef) # Deallocate the service discovery ref bonjour.DNSServiceRefDeallocate(serviceRef)
This code:
Defines the callback function to be invoked when results are found
Calls bonjour.pyDNSServiceBrowse
to start
the browse operation running
Accesses the serviceRef
’s file
descriptor, adding it to a select()
set and
calling bonjour.DNSServiceProcessResult()
each time data arrives on that socket
If you save this file as browse.py and run it, you should see something like this:
% python browse.py _http._tcp Service added: Stuart Cheshire's PowerBook G4 _http._tcp. local. 4
This browse operation will run indefinitely, reporting as services come and go, until you press Ctrl-C to stop it.
When you’ve browsed to discover available services, and the user has picked one, the next step is to resolve the named service to its target host and port number:
iimport sys import select import bonjour # Callback for service resolving def ResolveCallback(sdRef, flags, interfaceIndex, errorCode, fullname, hosttarget, port, txtLen, txtRecord, userdata): print "Service:", fullname print "is at", hosttarget, ":", port if len(sys.argv) < 4: print "Usage: resolve.py serviceName serviceType serviceDomain" sys.exit(1) serviceName = sys.argv[1] serviceType = sys.argv[2] serviceDomain = sys.argv[3] # Allocate a service discovery ref and resolve the named service flags = 0 interfaceIndex = 0 userdata = None serviceRef = bonjour.AllocateDNSServiceRef() ret = bonjour.pyDNSServiceResolve(serviceRef, flags, interfaceIndex, serviceName, serviceType, serviceDomain, ResolveCallback, userdata); if ret != bonjour.kDNSServiceErr_NoError: print "ret = %d; exiting" % ret sys.exit(1) # Get socket descriptor and loop fd = bonjour.DNSServiceRefSockFD(serviceRef) while 1: ret = select.select([fd], [], []) ret = bonjour.DNSServiceProcessResult(serviceRef) # Deallocate the service discovery ref bonjour.DNSServiceRefDeallocate(serviceRef)
If you save this file as resolve.py and run it, you should see something like this:
% python resolve.py "Stuart Cheshire's PowerBook G4" _http._tcp. local. Service: Stuart 32Cheshire's 32PowerBook 32G4._http._tcp.local. is at chesh7.local. : 123
In a real program, you’d take the resolve results you get and use them to initiate connections, and as soon as one succeeds, you’d then cancel the ongoing resolve operation.
On most general-purpose computers, there may be several different services advertising their presence via DNS-SD and several different clients browsing or resolving. On such computers, it makes sense to have a single shared daemon that all the clients and servers talk to.
However, if you’re making a very simple single-purpose hardware device, then another API choice is available. If your device offers one and only one service or a small known set of services, and it offers those services from the moment it’s powered on and boots up to the moment it’s powered down, then you can save a bit of memory by dispensing with the background daemon and just having a single monolithic process that advertises your service(s). To write such a monolithic DNS-SD advertising process, you use the source code from Apple’s Darwin mDNSResponder project, adding some code of your own to advertise the appropriate service(s). That code of yours interfaces directly with the core Multicast DNS functions defined in the mDNSEmbeddedAPI.h file. The mDNSResponder project’s mDNSPosix folder contains some example code showing how to do this. For example, Responder.c builds a binary called mDNSResponderPosix, a single monolithic process that advertises one or more services.
If you’re building a hardware device with very limited
memory, then one of the benefits of using the raw mDNSEmbeddedAPI.h API is that it’s malloc-free. That means that there is
never a case where the code can suffer intermittent failures because the mDNSCore
implementation calls malloc()
, and sometimes malloc()
returns NULL
because memory is so limited that no
more is available. Instead, all the memory needs are precisely known in advance, and for
each mDNSCore
call, the caller is responsible for
passing in a pointer to the storage that will be used in the execution of that call, in
the form of a C structure defining that required storage. That storage can be a global
variable, a local stack variable, a member of another enclosing structure, or allocated
any other way the caller chooses. It can even be allocated via malloc()
if the caller wishes, but the important point is that the
decision about how to allocate that memory is in the hands of the caller. Because of
this, all the mDNSCore
calls (when called with
correct parameters) are guaranteed to succeed, with no errors like “out of memory,” “no
more resources,” or something similar. In the simple case of a device advertising a few
known services, the easiest thing to do is just to declare each advertised service
structure as a global variable, and then you know with certainty that there can’t be any
runtime failures because of memory shortage. At compile time, you know whether it’s
going to work, because you either have enough space for your declared globals or you
don’t. There’s no runtime uncertainty.
This malloc-free operation is ideal
for devices that need to have precisely known fixed memory requirements, with absolutely
no uncertainty or runtime variation, but that certainty comes at a cost of more
difficult programming. Generally, if your device has enough memory and runs a
conventional general-purpose operating system like Linux, then using the standard mdnsd
background daemon is the best choice. However, if that choice doesn’t fit your product,
then embedding mDNSCore
and calling it directly with
your own code may be attractive. If you do choose that route, you won’t be alone. Most
of today’s Zeroconf hardware devices, such as printers and network cameras, do exactly
that.
3.17.162.247