Chapter 10. Ruby, Python, and Other Languages

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.

Ruby

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.

Registering a Service in Ruby

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 Stuart32Cheshire's32PowerBook
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.

Browsing for Services in Ruby

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 Stuart32Cheshire's32PowerBook32G4._
http._tcp.local. interface:lo0>
Browsing stopping

Resolving a Service in Ruby

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 Stuart32Cheshire's32PowerBook32G4.
_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

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/.

Registering a Service in Python

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.

Browsing for Services in Python

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.

Resolving a Service in Python

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: Stuart32Cheshire's32PowerBook32G4._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.

Embedded Responders

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.17.162.247