ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


O'Reilly Book Excerpts: Zero Configuration Networking: The Definitive Guide

Zero Configuration Networking: Using the Java APIs, Part 1

by Daniel H. Steinberg, Stuart Cheshire

Editor's Note: Zeroconf, also known by Apple's trade name of Bonjour, and previously as Rendezvous, offers interesting solutions to the problems of self-networking, in the potential absence of host naming and address assignment. Zeroconf applications can advertise themselves on the network and discover available services. Zero Configuration Networking: The Definitive Guide, by Stuart Cheshire and Daniel H. Steinberg, introduces the concepts and the many implementations of Zeroconf, including its Java API. In this first of a two-part introduction, the authors look at registering a service with Java.

Related Reading

Zero Configuration Networking: The Definitive Guide
By Daniel H. Steinberg, Stuart Cheshire

Starting in Mac OS X 10.3.9, new APIs enable Java software to advertise and discover services on the network using Zeroconf's DNS Service Discovery. The same Java DNS-SD APIs are also available in Bonjour for Windows, Bonjour for Linux, Solaris, *BSD, etc., enabling Java software to make use of Zeroconf's DNS Service Discovery across a wide range of platforms, not just on Mac OS X. In this chapter, you will take a quick look through the APIs, see short examples of how to register, browse for, add TXT records to, and resolve services, and finally see a complete example of using Java DNS-SD in a tic-tac-toe game.

Understanding the APIs

The com.apple.dnssd package exposes an abstract factory class, DNSSD, used to create the various types of DNSSDService objects, two classes used to manipulate DNS records, a collection of interfaces that are implemented as appropriate by client code to receive callback messages, and an exception:

The pattern for using the APIs will most often consist of calling a static method from the DNSSD factory class, passing in an instance of a class that implements the appropriate interface to receive callback messages. For example, when calling DNSSD.browse( ) to initiate a browsing operation, the client must supply an object that implements the BrowseListener interface.

As with all the different flavors of DNS-SD API, the Java APIs are asynchronous—you start an operation and get callback messages when interesting events happen—and to make effective use of the API, it is helpful to understand the mechanism by which those callback messages are delivered. Recall that in the C API, since there is no single event-handling model universally adopted by all C programs, the API returns a socket file descriptor to the client, so that the client can integrate it into the client's chosen event-handling model, such as a select( ) loop or similar. In contrast, when using the Mac OS X Cocoa APIs, it is assumed that the client will be using a Cocoa RunLoop, so ongoing asynchronous operations are automatically added to the current RunLoop, and events are automatically delivered sequentially to the client, as with other RunLoop events.

Unlike C (and its standard libraries), Java was designed from the start with full support for multithreaded code, so just as it is reasonable to assume that Cocoa programs use a RunLoop, it is reasonable to assume that Java programs can take advantage of threads. For this reason, Java clients get a benefit not present in the C API: Java clients don't need to take any special scheduling action to receive the events generated by the DNS-SD APIs. As soon as an asynchronous operation is initiated, the listener object will immediately begin receiving events, delivered "by magic," as it were, on a different thread automatically created for this purpose. Of course, all magic comes at some cost, and the cost is that the client code needs to be thread-safe. The moment a client thread calls DNSSD.browse( ), the listener object may start receiving events, running on another thread, even before the DNSSD.browse( ) call has returned to the caller. For this reason, even though the DNSSDService object is returned as the result of the DNSSD.browse( ) call, it is also passed as the first parameter in listener object methods, so that those methods can get reliable access to that value if they need it (for example, to stop the operation once they've received the result they need). Don't make the mistake of writing client code that calls DNSSD.browse( ) and places the result into some global or class variable, and then writing listener object methods that make use of the value in that variable. Frequently, the listener object methods will be invoked so quickly that they will be running before the main thread has done its assignment, resulting in the listener object methods accessing the value of the variable before it has been set.

Another issue to be aware of is that some other Java APIs have multithreading restrictions. For example, if you're writing Swing/AWT GUI code, it's important to remember that Swing components can be accessed by only one thread at a time. Generally, this thread is the event-dispatching thread. If you don't heed this warning, and you make Swing calls from other threads, then your program will be unreliable and is likely to fail randomly in mysterious ways. Since the typical purpose of a BrowseListener object is to update the user interface in response to services coming and going on the network, and since BrowseListener events are delivered asynchronously as they happen, on their own thread, this presents a small dilemma. How can a BrowseListener method legally perform Swing/AWT user interface operations? The solution is that the BrowseListener should use SwingUtilities.invokeAndWait to cause the event to be handled synchronously on the AWT event dispatching thread, where it can safely make user interface calls. The tic-tac-toe programming example at the end of this chapter demonstrates how to do this. There is also some sample code in the Clients/Java folder of Apple's Darwin mDNSResponder project. That code defines helper classes with names like SwingBrowseListener, which act as intermediaries between the raw DNS-SD events, delivered on their own background threads, and your own listener routines, which need to run on the AWT event-dispatching thread if they're going to make user interface calls. These helper objects receive the raw DNS-SD events on your behalf and then schedule your listener method to be executed on the AWT event-dispatching thread. Similar techniques can be used to accommodate other packages that have their own multithreading restrictions.

In this section, you will survey the com.apple.dnssd package.

The DNSSD Class

The factory class com.apple.dnssd.DNSSD is the workhorse of the Java DNS-SD API. You never instantiate objects of this class but instead call one of these public static methods:

By now, with knowledge of the dns-sd command-line tool and the C API, the register/browse/resolve operations should be quite familiar. One difference to be aware of is that, whereas the DNSServiceDiscovery C API follows the established Berkeley Sockets convention that port numbers are always given in network byte order, the standard Java networking APIs use port numbers in host integer byte order, and the DNSServiceDiscovery Java API adheres to that established Java convention. Another difference you will notice is that the Java API has two register methods and two browse methods. Whereas the C API always requires you to pass the full set of parameters for any given call (passing zero or NULL to indicate default values), the Java API makes use of method overloading to provide variants. If you don't want to limit browsing to a particular interface, you're happy to let the system pick the domain(s) to browse, and you don't need to specify any special flags, then you can just leave out those parameters completely and use the simpler version of the browse( ) method.

These three methods provide access to some of the more specialized DNS-SD functionality: enumerating the list of wide-area domains recommended for this network, querying for a specific individual named DNS Resource Record Set (RRSet), and signaling to the daemon that you believe a particular DNS Resource Record in its cache may be stale and out of date. The way they work is exactly equivalent to their counterparts in the C API.

As a simple example, you can browse for a service of type _example._tcp from the command line using:

dns-sd -B _example._tcp

In a Java program, you can accomplish the same task using a call to DNSSD.browse, as shown in the example below, where myBrowseListener is an instance of a class that implements the BrowseListener interface:

DNSSDService b = DNSSD.browse("_example._tcp", myBrowseListener);

The result of the DNSSD.browse( ) call is a reference to the newly created DNSSDService object, which you need to keep so that you can call b.stop( ) when it's time to stop the ongoing operation. If you never call b.stop( ), then the asynchronous operation you've initiated will run forever, consuming network bandwidth, memory, and CPU time until your program eventually exits! Typically, a program would call DNSSD.browse( ) when a user brings up a window to browse the network, and call b.stop( ) when the user closes that window.

It is common for the object making the DNSSD.browse call to implement the BrowseListener interface itself, in which case, you would substitute this in place of myBrowseListener.

In addition to the classes detailed above, the DNSSD class contains the following utility methods:

DNS-SD uses structured service names, containing an instance name, a service type, and a domain. In the on-the-wire format used in DNS packets, the three components are concatenated into a single, fully qualified DNS name. If you need to mix and match the service-oriented DNS-SD APIs with conventional low-level DNS APIs, you'll need to know the right fully qualified DNS name to use for a particular service. The constructFullName( ) call builds the correct fully qualified DNS name from DNS-SD's serviceName, regtype, and domain. The name is also properly escaped according to the standard DNS conventions; for example, if the instance name contains a dot (.), then it will appear as (\.) in the escaped DNS name, as required by the standard DNS APIs.

On machines with multiple physical interfaces, DNS-SD allows you to optionally restrict registering, browsing, and resolving to a single physical interface. To do this, you pass an interface index value when making the API calls. Because Java has historically not provided APIs for working with interface indexes, the Java DNS-SD API provides a couple of helper functions, getIfIndexForName( ) and getNameForIfIndex( ), which convert from an interface name to its index value, and vice versa.

The DNSSD class also includes constants (of type public final static int) that are used in various places by the API. For example, when you register a service, if a different service of that type already exists with the same name, Multicast DNS will normally pick a new unique name for you automatically. If, instead, you would like the service registration to simply fail and signal an error so that you can write your own code to select a new name, then you would pass the flag value NO_AUTO_RENAME when calling DNSSD.register( ).

The Listener Interfaces

With most DNS-SD operations, you don't want to block and wait for a response. You will generally issue a request and pass in a handle to an object that implements the appropriate listener interface. This listener will then be called when interesting events occur, such as discovery of an instance of the service type you're looking for.

BaseListener

All of the interfaces in the com.apple.dnssd package extend BaseListener. As a result, every listener must implement the operationFailed method:

operationFailed(com.apple.dnssd.DNSSDService service, int errorCode)

If an asynchronous operation encounters a failure condition, then the listener's operationFailed( ) method is called. These kinds of failures are rare. For example, one contrived way you could deliberately cause the operationFailed( ) method to be called would be to start a DNS-SD operation and then kill the background daemon with a Unix kill 8-9 command. Currently, under normal circumstances, the only asynchronous failure that applications may reasonably need to expect are name conflicts for service registrations. If a program registers a service using the NO_AUTO_RENAME flag, and the computer subsequently joins a network where a service of the same type with that name already exists, then the program will get informed via an operationFailed( ) callback that it's service registration had to be cancelled, and if it wants to continue advertising, then it should pick a new name and try again. (If the program didn't specify NO_AUTO_RENAME, then its service registration will be automatically renamed on its behalf, and it will be notified of the new name via a serviceRegistered callback instead.)

Each asynchronous operation (e.g., register, browse, resolve) has its corresponding Listener interface (e.g., RegisterListener, BrowseListener, ResolveListener).

DNSSDException

The DNSSDException class is used to report DNS-SD error conditions. This exception generally indicates a programming error and is not expected to occur during normal program operation. (The only exceptional condition a program should be prepared to deal with during normal operation are name conflicts for advertised services, and those events are reported asynchronously via operationFailed or serviceRegistered callbacks.)

Just as with all Java exceptions, your exception handler can use getMessage( ) to get a string describing the nature of the error, and it can use printStackTrace( ) to show you where the error occurred. If you want to find the actual error code from the daemon, you can use the DNSSDException class's getErrorCode( ) method. Table 8-1 shows error codes returned by the mdnsd daemon.

Table 8-1. Error codes returned by the Java DNS-SD API

Error

Code

NO_ERROR

0

UNKNOWN

-65537

NO_SUCH_NAME

-65538

NO_MEMORY

-65539

BAD_PARAM

-65540

BAD_REFERENCE

-65541

BAD_STATE

-65542

BAD_FLAGS

-65543

UNSUPPORTED

-65544

NOT_INITIALIZED

-65545

ALREADY_REGISTERED

-65547

NAME_CONFLICT

-65548

INVALID

-65549

INCOMPATIBLE

-65551

BAD_INTERFACE_INDEX

-65552

Using the APIs

In this section, you will see quick examples of performing specific tasks using the Java APIs. You will begin by registering a service and verifying that it is being advertised correctly by using the dns-sd command-line tool. Next, you will browse using Java code to discover the service you just registered, and resolve the service to get the target host and port number. The final example in this section revisits registering a service, but this time with attached attributes, stored in the service's TXT record.

Registering a Service

There are two steps you must take to register a service:

  1. Call DNSSD.register( ) using one of the two available signatures.

  2. Provide a class that implements the RegisterListener interface.

The DNSSD.register( ) call

The first step can be as simple as a single line of code:

DNSSDRegistration r = DNSSD.register("Moët & Chandon", "_example._tcp", 9099, this);

This advertises a service of type _example._tcp, which is listening on port 9099, with the instance name Moët & Chandon. Remember that instance names are not restricted like conventional DNS hostnames. Service instance names can contain uppercase, lowercase, spaces, punctuation, accented characters, and even non-roman characters like Kanji.

The return value from calling DNSSD.register( ) is a DNSSDRegistration object. DNSSDRegistration extends DNSSDService, so you can use the stop( ) method when it is time to stop advertising the service.

You can also add additional records to a registered service, and you can get a reference to the service's primary TXT record if you need to update that record to contain new data:

DNSRecord addRecord(int flags, int rrType, byte[] rData, int ttl)
DNSRecord getTXTRecord(  )

Most applications don't need to use the calls, but one well-known example that does is iChat, which adds the user's image icon as an additional record and updates the service's TXT record each time the user's status message changes.

The register( ) method might throw a DNSSDException, which must be caught. In our example program, the only reason you'd get an exception would be if you had an illegal parameter because of a typing mistake, say, "_txp" where it should say "_tcp." Using printStackTrace( ) in your exception handler can help you track down and debug this kind of mistake.

Before you compile this code, you first need to make sure that the calling object (this) implements the required RegisterListener interface.

The RegisterListener

To fulfill the requirement for a RegisterListener, you could create a whole new class especially for this purpose, but usually that is not necessary. Usually, the object responsible for registering a service is also the natural place to handle events pertaining to that registration, so all you have to do is add serviceRegistered( ) and operationFailed( ) methods to that class and declare that it now implements the RegisterListener interface.

In this current example, name conflicts should be handled automatically for us because we don't use NO_AUTO_RENAME, so we don't expect the operationFailed( ) method to be called at all. If it is called, it just prints a message to the standard error output to help us debug the code and find out what went wrong.

The serviceRegistered( ) method in our example will print a message to standard output displaying the advertised service's name, type, and domain. Note that the service's name may not be the name we asked for, if that name is already in use. Indeed, many programs don't specify a name at all, just passing in an empty string for the name and letting DNS-SD automatically use the system-wide default name, handling name conflicts as necessary.

public void serviceRegistered(DNSSDRegistration registration, int flags,
  String serviceName, String regType, String domain)
  {
  System.out.println("Registered Name  : " + serviceName);
  System.out.println("           Type  : " + regType);
  System.out.println("           Domain: " + domain);
  }

It's important to understand that in the dynamic world of networking, success at one moment in time is not a guarantee of continued future success in perpetuity. The serviceRegistered callback indicates that, at this moment, the service is being advertised on the network under the indicated name. Over the lifetime of a long-running program, the program should expect that it is quite possible that the name may change, and the serviceRegistered( ) method may be called again with new data. After the initial probing and announcement of the chosen unique service name, that name may subsequently change as a result of both internal and external factors.

The internal factor is explicit user action. If you registered your service using an empty string for the name so that your service uses the system-wide default name, and the user subsequently decides to change the system-wide default name, then she doesn't need to quit and relaunch your server for it to get the new name. The name is updated live, and you will get a new serviceRegistered( ) callback telling you the new name.

The external factor that may cause your service name to change is connecting to a new network. If the user starts your server on his laptop when it's not connected to any network, and then (perhaps hours or days later) connects to a network where your chosen name is already in use, one of two things will happen. If you specified NO_AUTO_RENAME, then your operationFailed method will be called. If you did not specify NO_AUTO_RENAME, then Multicast DNS will automatically select a new unique name for you and notify you with a new serviceRegistered callback.

The importance of the instance name provided in the serviceRegistered( ) callback depends on what kind of program you're writing. For a background process like an FTP server with no user interface, the program itself may not care at all what name is being advertised, as long as users can find it and connect. For a server program with a user interface, it may want to find out its advertised name simply for cosmetic reasons, to display it in a status window.

The kind of program for which the reported instance name is most interesting is the kind that's both a client and a server. For example, iChat advertises its presence on the network using Bonjour and, at the same time, browses to find other iChat instances on the network. One of the instances it discovers will be itself, but naturally iChat wants its Bonjour window to show only other users on the network, not itself. By comparing discovered instances against its own name as reported in the serviceRegistered( ) callback, iChat can tell when it has discovered itself on the network and filter that particular discovered entity from the list displayed in its Bonjour window.

Some developers have asked why DNS-SD doesn't do this filtering automatically. The problem is that the definition of self is slippery. Does self mean the same machine? Same user ID? Same process? Same thread? Automatically preventing discovery of all services on the same machine would be wrong. Some background processes use a web-based configuration interface, which they advertise with DNS-SD. If DNS-SD couldn't discover services on the same machine, these local background processes wouldn't show up in Safari on that machine. This would create the nonsensical situation where you could configure the process from any machine on the network except the one where the process is actually running! Another problem scenario is multiuser Unix machines, which can have more than one user logged on at a time. If DNS-SD couldn't discover services on the same machine, two users logged onto the same Unix machine from different X Window terminals would be effectively invisible to each other. Preventing discovery of services that happen to be running with the same user ID causes a similar set of inadvertent problems.

Automatically filtering discovery of services advertised from the same Unix process ID also doesn't necessarily give the results you might want. Sometimes the entity doing the browsing and the entity doing the advertising aren't the same process, even though they are conceptually related. For example, in Mac OS X Printer Sharing, the UI code showing the list of network printers doesn't want to show local printers that are being shared on the network by this machine, but the code displaying the print dialog user interface is not the same Unix process as the background process advertising those printers. In this case, automatic filtering based on Unix process IDs would fail to provide the desired result.

Ultimately, the only way to meet the needs of all applications is to report the names of advertised services in the serviceRegistered( ) callback and let applications that require some kind of self-filtering implement that filtering, in the way that makes sense for that particular application.

For the most part, though, most applications don't need any kind of self-filtering. If you find yourself thinking that you don't want to discover entities on the same machine, the question to ask is, "Why?" Usually, the answer will be that there's a different way to discover and communicate with entities on the same machine. If that's the case, the question to ask is, "Why?" Why have two different ways of doing the same thing, one for local entities and a different one for remote entities? Sometimes there are valid performance arguments for making local entities a special case, but in most cases, it is just a historical design accident. In most cases, instead of having two different mechanisms for doing roughly the same thing, each with their own bugs, features, and idiosyncrasies, it is smarter to have one mechanism—built on IP—and concentrate on making that IP-based mechanism fully featured, reliable, and efficient.

Complete TestRegister program listing

Example 8-1 shows a complete listing, which you can compile with javac, to advertise a named service using DNS-SD. This program uses new ServerSocket(0); to get a unique port number assigned by the system so that it can advertise it via DNS-SD, but it does not include code to actually provide any real service on this port. In this example, the program just waits for 30 seconds doing nothing, then calls b.stop( ) and exits.

Example 8-1. Java program to advertise a named service using DNS-SD
import java.net.*;
import com.apple.dnssd.*;

class TestRegister implements RegisterListener
  {
  // Display error message on failure
  public void operationFailed(DNSSDService service, int errorCode)
    {
    System.out.println("Registration failed " + errorCode);
    }

  // Display registered name on success
  public void serviceRegistered(DNSSDRegistration registration, int flags,
    String serviceName, String regType, String domain)
    {
    System.out.println("Registered Name  : " + serviceName);
    System.out.println("           Type  : " + regType);
    System.out.println("           Domain: " + domain);
    }

  // Do the registration
  public TestRegister(String name, int port)
    throws DNSSDException, InterruptedException
    {
    System.out.println("Registration Starting");
    System.out.println("Requested Name: " + name);
    System.out.println("          Port: " + port);
    DNSSDRegistration r = DNSSD.register(name, "_example._tcp", port, this);
    Thread.sleep(30000);  // Wait thirty seconds, then exit
    System.out.println("Registration Stopping");
    r.stop(  );
    }

  public static void main(String[] args)
    {
    if (args.length > 1)
      {
      System.out.println("Usage: java TestRegister name");
      System.exit(-1);
      }
    else
      {
      try
        {
        // If name specified, use it, else use default name
        String name = (args.length > 0) ? args[0] : null;
        // Let system allocate us an available port to listen on
        ServerSocket s = new ServerSocket(0);
        new TestRegister(name, s.getLocalPort(  ));
        }
      catch(Exception e)
        {
        e.printStackTrace(  );
        System.exit(-1);
        }
      }
    }
  }

Testing the registration program

The easiest way to verify that the program successfully registers a service is to start up dns-sd and start browsing for services of type _example._tcp using the command:

% dns-sd -B _example._tcp
Browsing for _example._tcp

Open a separate terminal window and compile TestRegister.java:

% javac TestRegister.java

Now you can run the TestRegister program by executing:

% java TestRegister "My Chosen Name"
Registration Starting
Requested Name: My Chosen Name
          Port: 51619

After a one-second pause, when it has confirmed that the name is indeed unique, it also prints:

Registered Name  : My Chosen Name
           Type  : _example._tcp.
           Domain: local.

In the first terminal window, where you are running dns-sd, you will now see that "My Chosen Name" appears. After 30 seconds, the program will display "Registration Stopping" and exit, and in the dns-sd window you should see a remove event as the named service goes away.

With our TestRegister program, we can also demonstrate name conflict detection and automatic renaming. Run the TestRegister program again in the second terminal window and, while it is still running, quickly open a third terminal window and run the same command again:

% java TestRegister "My Chosen Name"
Registration Starting
Requested Name: My Chosen Name
          Port: 51625
Registered Name  : My Chosen Name (2)
           Type  : _example._tcp.
           Domain: local.

This time you'll see that, because the name "My Chosen Name" was already in use for a different advertised service, the second instance was automatically renamed to "My Chosen Name (2)."

One detail worth noting here is that a conflict is detected because we have two different instances of our program running, listening on different ports. Two different instances of a service can't use the same name; when browsing, the user would see only one service instance instead of two, and one or the other service would be rendered inaccessible. However, if instead of having two different instances on different ports, we had just one service instance running, listening on one port, and we simply registered that service twice with the exact same parameters—same name, same type, same host, and same port—then no conflict would be reported. Registering the same service twice is arguably a programming error, but it's not a conflict because the two registrations are in complete agreement. The API permits duplicate registrations like this to allow for proxy servers where (perhaps for fault-tolerance reasons) a given service may be deliberately advertised by multiple proxies.

This simple example highlighted the code you need to write to register an instance of a service in a Java application. What you have done is advertised that a named service of a particular type is available on this machine at the specified port. You have not set up the code to listen on that port or to react when your service is contacted. The section "An Extended Example: Tic-Tac-Toe" at the end of this chapter will take you through this additional step.

Daniel H. Steinberg is the editor for the new series of Mac Developer titles for the Pragmatic Programmers. He writes feature articles for Apple's ADC web site and is a regular contributor to Mac Devcenter. He has presented at Apple's Worldwide Developer Conference, MacWorld, MacHack and other Mac developer conferences.

Stuart Cheshire is currently a Senior Scientist with Apple Computer, specializing in Internet Protocols.


View catalog information for Zero Configuration Networking: The Definitive Guide

Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.