Zero Configuration Networking: The Definitive Guide

8.1. 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:

  • Factory Class :

    • DNSSD

  • References to ongoing asynchronous operations :

    • DNSSDService

    • DNSSDRegistration

  • DNS Record Classes:

    • DNSRecord

    • TXtrecord

  • Callback Interface Classes , implemented by client:

    • BaseListener

    • RegisterListener

    • BrowseListener

    • ResolveListener

    • DomainListener

    • QueryListener

  • DNSSD Error Exception:

    • DNSSDException

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 asynchronousyou start an operation and get callback messages when interesting events happenand 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.

8.1.1. 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:

  • static DNSSDRegistration register(java.lang.String serviceName, java.lang.String regType, int port, com.apple.dnssd.RegisterListener listener)

  • static DNSSDRegistration register(int flags, int ifIndex, java.lang.String serviceName, java.lang.String regType, java.lang.String domain, java.lang.String host, int port, com.apple.dnssd.TXTRecord txtRecord, com.apple.dnssd.Register-Listener listener)

  • static DNSSDService browse(java.lang.String regType, com.apple.dnssd.Browse-Listener listener)

  • static DNSSDService browse(int flags, int ifIndex, java.lang.String regType, java.lang.String domain, com.apple.dnssd.BrowseListener listener)

  • static DNSSDService resolve(int flags, int ifIndex, java.lang.String serviceName, java.lang.String regType, java.lang.String domain, com.apple.dnssd.ResolveListener listener)

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.

  • static DNSSDService enumerateDomains(int flags, int ifIndex, com.apple.dnssd.DomainListener listener)

  • static DNSSDService queryRecord(int flags, int ifIndex, java.lang.String serviceName, int rrtype, int rrclass, com.apple.dnssd.QueryListener listener)

  • static void reconfirmRecord(int flags, int ifIndex, java.lang.String fullName, int rrtype, int rrclass, byte[] rdata)

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:

  • constructFullName(java.lang.String serviceName, java.lang.String regType, java.lang.String domain)

  • getIfIndexForName(java.lang.String ifName)

  • getNameForIfIndex(int ifIndex)

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( ).

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

8.1.2.1. 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 -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).

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

Категории