Zero Configuration Networking: The Definitive Guide

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

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

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

8.2.1.2. 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 mechanismbuilt on IPand concentrate on making that IP-based mechanism fully featured, reliable, and efficient.

8.2.1.3. 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); } } } }

8.2.1.4. 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 parameterssame name, same type, same host, and same portthen 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.

8.2.2. Browsing for Services

Browsing to discover our advertised service using the dns-sd tool is very easy, and doing so using Java code is barely any harder. To browse, you need to perform two steps similar to those you just used to register your service:

  • Call DNSSD.browse( ) using one of the two available signatures.

  • Provide a class that implements the BrowseListener interface.

As with the DNSSD.register( ) example, it is common for the object initiating the browse operation to be the one that wants to receive the results, so it implements the BrowseListener interface itself and specifies itself (this) as the listener object in the DNSSD.browse( ) call.

To function as a BrowseListener, a class must implement operationFailed( ), serviceFound( ), and serviceLost( ). Under normal circumstances, the operation-Failed( ) method will never be invoked. In our example program, the serviceFound( ) and serviceLost( ) methods just print out information to show the events they receive, very much like the output of dns-sd.

public void serviceFound(DNSSDService browser, int flags, int ifIndex, String name, String regType, String domain) { System.out.println("Add flags:" + flags + ", ifIndex:" + ifIndex + ", Name:" + name + ", Type:" + regType + ", Domain:" + domain); }

Whenever a new instance of a service is discovered, serviceFound( ) will be called and will write a line to standard out beginning with the word "Add," followed by the name, type, and domain.

The serviceLost( ) method takes the exact same parameter list as serviceFound( ). The only difference in our example program is that instead of printing "Add" it prints "Rmv" (which stands for remove).

Example 8-2 shows a complete listing, which you can compile with javac, to browse for services using DNS-SD. In this example, the program just runs for 30 seconds, displaying add and remove events as they arrive, and then calls b.stop( ) and exits. Of course, in a real program, you wouldn't use a fixed timeout like 30 seconds. You'd start the browse operation running when the user brings up a browsing window, and stop it when they close the browsing window.

Example 8-2. Java program to browse for services using DNS-SD

import com.apple.dnssd.*; class TestBrowse implements BrowseListener { // Display error message on failure public void operationFailed(DNSSDService service, int errorCode) { System.out.println("Browse failed " + errorCode); System.exit(-1); } // Display services we discover public void serviceFound(DNSSDService browser, int flags, int ifIndex, String name, String regType, String domain) { System.out.println("Add flags:" + flags + ", ifIndex:" + ifIndex + ", Name:" + name + ", Type:" + regType + ", Domain:" + domain); } // Print a line when services go away public void serviceLost(DNSSDService browser, int flags, int ifIndex, String name, String regType, String domain) { System.out.println("Rmv flags:" + flags + ", ifIndex:" + ifIndex + ", Name:" + name + ", Type:" + regType + ", Domain:" + domain); } public TestBrowse( ) throws DNSSDException, InterruptedException { System.out.println("TestBrowse Starting"); DNSSDService b = DNSSD.browse("_example._tcp", this); System.out.println("TestBrowse Running"); Thread.sleep(30000); System.out.println("TestBrowse Stopping"); b.stop( ); } public static void main(String[] args) { try { new TestBrowse( ); } catch(Exception e) { e.printStackTrace( ); System.exit(-1); } } }

After you've compiled the TestBrowse program, we'll demonstrate it using our TestRegister program. Open three terminal windows. In the first, run java TestRegister "My Chosen Name." In the second, run that same command a second time. In the third window, a second or two later, run java TestBrowse:

% java TestBrowse TestBrowse Starting Add flags:3 ifIndex:5 Name:My Chosen Name Type:_example._tcp. Domain:local. Add flags:2 ifIndex:5 Name:My Chosen Name (2) Type:_example._tcp. Domain:local. TestBrowse Running Rmv flags:0 ifIndex:5 Name:My Chosen Name Type:_example._tcp. Domain:local. Rmv flags:0 ifIndex:5 Name:My Chosen Name (2) Type:_example._tcp. Domain:local. TestBrowse Stopping

You'll see that TestBrowse finds our two service instances, "My Chosen Name" and "My Chosen Name (2)."

Another thing to notice is that, even though TestBrowse prints out "TestBrowse Running" on the very next line of the program after the DNSSD.browse( ) call, the services are discovered so fast that they're printed even before that line gets to execute.

Each TestRegister process exits 30 seconds after it was started, and we see the "Rmv" line printed for each service as it goes away. Finally, after running for 30 seconds itself, TestBrowse calls b.stop( ) and exits.

You can now advertise a named service and discover a list of named services. The third step, to actually use a service, is to resolve its name to its current address and port number.

8.2.3. Resolving a Service

With the dns-sd command-line tool, we use dns-sd -L to resolve a named service. DNS-SD deliberately separates browsing from resolving. When you browse, you get a list of names, not IP addresses. This is because, when using link-local addresses or DHCP, IP addresses can change from day to day. When using dynamically allocated ports and NAT gateways, TCP port numbers for a given service can change from day to day, too. A program that stores a service's IP address and port number in a preference file on disk may well find that tomorrow that address and port number no longer work. What remains stable for a given service instance is its name, and the Java DNS-SD API provides the DNSSD.resolve( ) call to translateat time of usefrom service instance name to the correct target host and port number for that service at that moment.

Resolving follows the same pattern as registering and browsing: first call DNSSD.resolve( ) and then provide a class that implements the ResolveListener interface. To function as a ResolveListener, a class must implement operationFailed( ) and serviceResolved( ). Under normal circumstances, the operationFailed( ) method will never be invoked.

Our serviceResolved( ) example just prints out the information it's given. When you register a service using the dns-sd tool, you can specify a list of "key=value" attributes, which are stored in the service's DNS TXT record. Our serviceResolved( ) method prints out those, too:

for (int i = 0; i < txtRecord.size( ); i++) { String key = txtRecord.getKey(i); String value = txtRecord.getValueAsString(i); if (key.length( ) > 0) System.out.println("\t" + key + "=" + value); }

This example, for illustrative purposes, iterates through the whole TXT record, printing out every key it finds. In a real program, you would write code to retrieve just the specific named keys that you care about, using txTRecord.contains("key") when you just want to know if a given key is present, and txTRecord.getValue("key") or txtrecord.getValueAsString("key") to retrieve the value associated with a given named key.

Example 8-3 shows a complete listing, which you can compile with javac, to resolve a named service using DNS-SD. In the case of a multi-homed host, you may receive more than one successful resolve event (e.g., if the same named service is reachable via both Ethernet and wireless). In this example, the program just runs for five seconds, displaying resolve events as they arrive, and then calls r.stop( ) and exits. Ideally, in a real program, instead of using a fixed timeout, you'd present some indication to the user that the program was attempting to connect and let the user decide how long to wait before clicking the Cancel button to give up.

Example 8-3. Java program to resolve a named DNS-SD service

import com.apple.dnssd.*; class TestResolve implements ResolveListener { // Display error message on failure public void operationFailed(DNSSDService service, int errorCode) { System.out.println("Resolve failed " + errorCode); System.exit(-1); } // Display information when service is resolved public void serviceResolved(DNSSDService resolver, int flags, int ifIndex, String fullName, String hostName, int port, TXTRecord txtRecord) { System.out.println("Service Resolved: " + hostName + ":" + port); System.out.println("Flags: " + flags + ", ifIndex: " + ifIndex + ", FQDN: " + fullName); for (int i = 0; i < txtRecord.size( ); i++) { String key = txtRecord.getKey(i); String value = txtRecord.getValueAsString(i); if (key.length( ) > 0) System.out.println("\t" + key + "=" + value); } } public TestResolve(String name, String domain) throws DNSSDException, InterruptedException { System.out.println("TestResolve Starting"); DNSSDService r = DNSSD.resolve(0, DNSSD.ALL_INTERFACES, name, "_example._tcp", domain, this); System.out.println("TestResolve Running"); Thread.sleep(5000); System.out.println("TestResolve Stopping"); r.stop( ); } public static void main(String[] args) { if (args.length != 2) { System.out.println("Usage: java TestResolve name domain"); System.exit(-1); } else { try { new TestResolve(args[0], args[1]); } catch(Exception e) { e.printStackTrace( ); System.exit(-1); } } } }

After you've compiled TestResolve, we'll test it by registering a fake service using the dns-sd command:

% dns-sd -R "My Chosen Name" _example._tcp local 123 key=val anotherkey=anotherval

Now you can use your TestResolve program to look up that service:

% java TestResolve "My Chosen Name" local TestResolve Starting Service Resolved: mymac.local.:123 Flags: 0, ifIndex: 5, FQDN: My\032Chosen\032Name._example._tcp.local. key=val anotherkey=anotherval TestResolve Running TestResolve Stopping

As with the browsing example, it's common for the resolve to succeed so quickly that the program gets the result before it's even had time to print out its "TestResolve Running" line. The reason DNS-SD operations are asynchronous is not because they usually take a long time, but because occasionally they might, particularly when there's some kind of network problem; and it is precisely at those timeswhen struggling with other problemsthat the user will be least forgiving toward your program if it decides to lock up and become unresponsive.

You'll see that TestResolve finds our registered service on this host, listening (we pretend) on port 123, with two TXT record attributes, key=val and anotherkey=anotherval.

Now that you know not only how to register, browse, and resolve, but also how to access named attributes in the TXT record, it's time to revisit our registration example and add a TXT record full of attributes to it.

8.2.4. Registering a Service with DNS TXT Record Attributes

To register a service with DNS TXT record attributes, we first need to create the TXT record:

TXTRecord txtRecord = new TXTRecord( ); txtRecord.set("txtvers", "1"); txtRecord.set("status", "ready"); txtRecord.set("difficulty", "medium");

By convention, the first key in a TXT record should be a txtvers key, indicating the version that a client needs to have implemented in order to usefully understand the following keys in this TXT record. After the initial txtvers key, the rest of the keys are up to your protocol-creating imagination.

In this example, all the values we set are textual strings, but (despite the name) DNS TXT records are perfectly capable of holding raw binary data, too. If you have some binary data you wish to attach as an attribute, you can do so directly using the alternate form of the TXtrecord.set( ) method, which takes a raw byte array as the value:

TXTRecord.set(java.lang.String key, byte[] value)

You can store any binary data you wish, so you shouldn't feel compelled to use something like hexadecimal characters or Base-64 encoding to turn binary data into text before you store it as a key/value attribute. The only constraint is that key/value attributes are intended for storing small amounts of additional information about a service. The length of the key name, plus the length of the value data, cannot add up to more than 254 bytesyet another reason not to double the size of your binary data by needlessly turning it into hexadecimal text.

As your program evolves over time, you may define new key names with new meanings. If you're careful, you can generally write code to be forward- and backward-compatible. If a client tries to fetch a given named key from a TXT record and finds it missing, it can conclude that it is talking to an older server that predates the invention of that key, and most of the time, it's possible to write a client to take the right steps to work with that older server. If a client communicates with a newer server that defines new key names that were invented after the client was written, the client will generally ignore those new keysthe client only calls getValue( ) for key names it knows about, and the rest simply go unnoticed. However, if you find in the future that you have no choice but to make a change to your TXT record keys that is so drastic that compatibility is simply not going to be possible, you should specify that these new TXT records have a new version number in their txtvers key. This way, as long as you had the foresight to write your first clients so they check the txtvers key and display an error message if it does not contain a version number they understand (i.e., 1 in the first clients), this can help make the upgrade transition to the newer version of the protocol easier. Instead of simply failing mysteriously, the client can at least tell the user that she should upgrade to a newer version. Most protocol designers hope they never have to make a change so drastic that it breaks compatibility, but should you find yourself in this situation, the txtvers key can help make the transition go a little more smoothly.

To register a service with TXT record attributes, you need to use the longer version of DNSSD.register( ) with the additional parameters:

DNSSDRegistration r = DNSSD.register(0, DNSSD.ALL_INTERFACES, name, "_example._tcp", null, // Name, type, and domain null, port, // Target host and port txtRecord, this); // TXT record and listener object

If you compare this new usage of register( ) with the one presented in the section "Registering a Service," you will note that there are several extra parameters, and that most of them have the value zero or NULL to signify that DNS-SD should use sensible default values.

Example 8-4 shows a complete listing for registering a service with added TXT record attributes. The change compared to Example 8-1 is indicated by the comment, "New code to register with TXT record begins here." In this example, as in Example 8-1, the program just waits for 30 seconds doing nothing, then calls b.stop( ) and exits.

Example 8-4. Java program to advertise a service with TXT record attributes

import java.net.*; import com.apple.dnssd.*; class TestRegisterWithAttributes 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 TestRegisterWithAttributes(String name, int port) throws DNSSDException, InterruptedException { System.out.println("Registration Starting"); System.out.println("Requested Name: " + name); System.out.println(" Port: " + port); // New code to register with TXT record begins here TXTRecord txtRecord = new TXTRecord( ); txtRecord.set("txtvers", "1"); txtRecord.set("status", "ready"); txtRecord.set("difficulty", "medium"); DNSSDRegistration r = DNSSD.register(0, DNSSD.ALL_INTERFACES, name, "_example._tcp", null, // Name, type, and domain null, port, // Target host and port txtRecord, this); // TXT record and listener object // New code to register with TXT record ends 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 TestRegisterWithAttributes 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 TestRegisterWithAttributes(name, s.getLocalPort( )); } catch(Exception e) { e.printStackTrace( ); System.exit(-1); } } } }

After you've compiled TestRegisterWithAttributes, we'll demonstrate it using our TestBrowse program. In one terminal window, run:

% java TestBrowse

While that's still running, in another terminal window, run:

% java TestRegisterWithAttributes "My Chosen Name"

In the TestBrowse window, you should see the service added. Now, while TestRegisterWithAttributes is still running, run TestResolve in a third terminal window:

% java TestResolve "My Chosen Name" local TestResolve Starting Service Resolved: mymac.local.:52658 Flags: 0, ifIndex: 5, FQDN: My\032Chosen\032Name._example._tcp.local. txtvers=1 status=ready difficulty=medium TestResolve Running TestResolve Stopping

8.2.5. Adding, Updating, and Removing Additional Records

A standard DNS-SD service is described by two DNS records: an SRV record, giving target host and port number, and a TXT record, containing zero or more key/value attributes. For almost all applications, advertising a service with these two records is all that's needed. However, there are certain applicationsiChat being the prime examplethat have extra requirements. For the benefit of applications like this, DNS-SD provides some additional specialized APIs to add, update, and remove additional records.

DNS-SD allows applications to add additional DNS records to an existing service registration using DNSSDRegistration's addRecord method. iChat attaches a small JPEG image to each advertised service, containing the user's icon or picture, and because this is too large to fit in a TXT record attribute, iChat attaches it as an additional record. Adding records like this is something that should not be done indiscriminately because of the cost in increased network traffic, but in the case of iChat, it is the most appropriate way to communicate a user's icon to all the other iChat clients on the local network.

Calling addRecord( ) returns a DNSRecord object, which supports two operations, update( ) and remove( ). If you need to change the data in the record (as iChat does when the user changes the icon), then you can use update( ) to provide new data to replace the old data in the record.

When adding a record, you need to specify the DNS type of the record. The original DNS types are listed in RFC 1035, and newer types are given in later RFCs. For example, the SRV record type (type 33) is specified in RFC 2782. You can also find the list of currently defined DNS types at http://www.iana.org/assignments/dns-parameters. On many systems, you can also find the defined types listed in one of the C header files, such as /usr/include/nameser.h or /usr/include/dns_sd.h. The current IANA list of DNS types is shown in Table 8-2.

Table 8-2. DNS resource record types

TYPE

Value

Meaning

Reference

A

1

A host address

RFC1035

NS

2

An authoritative name server

RFC1035

MD

3

A mail destination (OBSOLETE; use MX)

RFC1035

MF

4

A mail forwarder (OBSOLETE; use MX)

RFC1035

CNAME

5

The canonical name for an alias

RFC1035

SOA

6

Marks the start of a zone of authority

RFC1035

MB

7

A mailbox domain name (EXPERIMENTAL)

RFC1035

MG

8

A mail group member (EXPERIMENTAL)

RFC1035

MR

9

A mail rename domain name (EXPERIMENTAL)

RFC1035

NULL

10

A null RR (EXPERIMENTAL)

RFC1035

WKS

11

A well-known service description

RFC1035

PTR

12

A domain name pointer

RFC1035

HINFO

13

Host information

RFC1035

MINFO

14

Mailbox or mail list information

RFC1035

MX

15

Mail exchange

RFC1035

TXT

16

Text strings

RFC1035

RP

17

For Responsible Person

RFC1183

AFSDB

18

For AFS Data Base location

RFC1183

X25

19

For X.25 PSDN address

RFC1183

ISDN

20

For ISDN address

RFC1183

RT

21

For Route Through

RFC1183

NSAP

22

For NSAP address, NSAP style A record

RFC1706

NSAP-PTR

23

 

SIG

24

For security signature

RFC2535 RFC3755 RFC4034

KEY

25

For security key

RFC2535 RFC3755 RFC4034

PX

26

X.400 mail mapping information

RFC2163

GPOS

27

Geographical Position

RFC1712

AAAA

28

IP6 Address

Thomson

LOC

29

Location Information

Vixie

NXT

30

Next Domain (OBSOLETE)

RFC2535, RFC3755

EID

31

Endpoint Identifier

Patton

NIMLOC

32

Nimrod Locator

Patton

SRV

33

Server Selection

RFC2782

ATMA

34

ATM Address

Dobrowski

NAPTR

35

Naming Authority Pointer

RFC2168, RFC2915

KX

36

Key Exchanger

RFC2230

CERT

37

CERT

RFC2538

A6

38

A6

RFC2874

DNAME

39

DNAME

RFC2672

SINK

40

SINK

Eastlake

OPT

41

OPT

RFC2671

APL

42

APL

RFC3123

DS

43

Delegation Signer

RFC3658

SSHFP

44

SSH Key Fingerprint

RFC-ietf-secsh-dns-05.txt

IPSECKEY

45

IPSECKEY

RFC4025

RRSIG

46

RRSIG

RFC3755

NSEC

47

NSEC

RFC3755

DNSKEY

48

DNSKEY

RFC3755

UINFO

100

IANA-Reserved

UID

101

IANA-Reserved

GID

102

IANA-Reserved

UNSPEC

103

IANA-Reserved

TKEY

249

Transaction Key

RFC2930

TSIG

250

Transaction Signature

RFC2845

IXFR

251

Incremental transfer

RFC1995

AXFR

252

Transfer of an entire zone

RFC1035

MAILB

253

Mailbox-related RRs (MB, MG, or MR)

RFC1035

MAILA

254

Mail agent RRs (OBSOLETE; see MX)

RFC1035

ANY

255

A request for any record(s)

RFC1035

When adding or updating records, it is your responsibility to make sure that the byte array data you provide is properly formatted for the DNS record type in question. You can specify the DNS time to live (TTL), though for most applications, it's most sensible to simply pass zero and let DNS-SD use its default TTL.

On the receiving side, to read records other than the standard SRV and TXT pair (which are retrieved using the resolve call), clients use DNSSD's queryRecord( ) method, providing a QueryListener object to receive the asynchronous results. A QueryListener object needs to implement the queryAnswered( ) method:

queryAnswered(DNSSDService query, int flags, int ifIndex, String fullName, int rrtype, int rrclass, byte[] rdata, int ttl)

Whenever an answer becomes available, the queryAnswered( ) method is called. Due to a quirk of the API, the queryAnswered( ) method is also called if a previously valid answer expires. You can tell if the answer is coming or going by checking bit 1 (value 2) of the flags field (the kDNSServiceFlagsAdd flag of the C API). If (flags & 2) is nonzero, then a new answer is being added; if zero, then a previous answer is being removed.

The queryAnswered( ) method is given the raw bytes of the DNS resource record; it is the responsibility of the queryAnswered( ) method to know how to interpret the DNS record type it requested.

There is another style of update that iChat performs. Your status message is stored as a key/value attribute in the service's TXT record, and whenever you update your status message, iChat doesn't de-register its service and register a new one; instead, it just updates the TXT record to contain the new data. To perform this kind of update, you don't need to add another TXT record to the service. All services implicitly have a TXT record, even if you didn't specify one. If you don't specify a TXT record when registering a service, then the service automatically gets an empty TXT record containing no key/value attributes. (Strictly speaking, to comply with the DNS rules for the format of DNS TXT records, the service gets a TXT record containing a single empty string.)

Before you can use the update method to provide new data, you need an object upon which to invoke that method. To get the object representing the service's standard TXT record, upon which to perform updates, the Java DNS-SD API provides the DNSSDRegistration.getTXTRecord( ) method. The update method requires you to provide properly formatted DNS TXT record data, and this is where the TXtrecord's getrawBytes( ) method comes in handy:

DNSRecord record = registration.getTXTRecord( ); byte rawbytes[] = txtRecord.getRawBytes( ); record.update(0, rawbytes, 0);

As with other DNS-SD methods, passing zero for the flags and zero for the record TTL causes sensible default values to be used.

Example 8-5 shows a complete listing that you can compile with javac, which first registers a service with the default empty TXT record, then at ten-second intervals updates the TXT record to say status=ready, status=steady, and finally, status=go. The change compared to Example 8-1 is indicated by the comment, "New code to update TXT record begins here."

Example 8-5. Java program to advertise a service and update its TXT record

import java.net.*; import com.apple.dnssd.*; class TestRegisterWithUpdates 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 TestRegisterWithUpdates(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); // New code to update TXT record begins here TXTRecord txtRecord = new TXTRecord( ); txtRecord.set("txtvers", "1"); Thread.sleep(10000); // Wait ten seconds before updating TXT record txtRecord.set("status", "Ready"); System.out.println("Ready"); r.getTXTRecord( ).update(0, txtRecord.getRawBytes( ), 0); Thread.sleep(5000); txtRecord.set("status", "Steady"); System.out.println("Steady"); r.getTXTRecord( ).update(0, txtRecord.getRawBytes( ), 0); Thread.sleep(5000); txtRecord.set("status", "Go"); System.out.println("Go"); r.getTXTRecord( ).update(0, txtRecord.getRawBytes( ), 0); // New code to update TXT record ends 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 TestRegisterWithUpdates 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 TestRegisterWithUpdates(name, s.getLocalPort( )); } catch (Exception e) { e.printStackTrace( ); System.exit(-1); } } } }

Example 8-6 shows a complete listing that you can compile with javac, which resolves the named service and then begins monitoring its TXT record for changes. The change compared to Example 8-1 is indicated by the comment, "New code to update TXT record begins here."

Example 8-6. Java program to monitor a TXT record for changes

import com.apple.dnssd.*; class TestResolveWithMonitoring implements ResolveListener, QueryListener { private DNSSDService monitorQ = null; // Display error message on failure public void operationFailed(DNSSDService service, int errorCode) { System.out.println("Resolve failed " + errorCode); System.exit(-1); } public void queryAnswered(DNSSDService query, int flags, int ifIndex, String fullName, int rrtype, int rrclass, byte[] rdata, int ttl) { if ((flags & 2) != 0) { boolean blankPrinted = false; TXTRecord txtRecord = new TXTRecord(rdata); for (int i = 0; i < txtRecord.size( ); i++) { String key = txtRecord.getKey(i); String value = txtRecord.getValueAsString(i); if (key.length( ) > 0) { if (!blankPrinted) { blankPrinted = true; System.out.println( ); } System.out.println("\t" + key + "=" + value); } } } } // Display information when service is resolved public void serviceResolved(DNSSDService resolver, int flags, int ifIndex, String fullName, String hostName, int port, TXTRecord txtRecord) { System.out.println("Service Resolved: " + hostName + ":" + port); System.out.println("Flags: " + flags + ", ifIndex: " + ifIndex + ", FQDN: " + fullName); // Now that we've got a resolve result, // start monitoring the TXT record and stop the resolve call. try { monitorQ = DNSSD.queryRecord(0, ifIndex, fullName, 16, 1, this); } catch (Exception e) { e.printStackTrace( ); System.exit(-1); } resolver.stop( ); Thread.sleep(1); } public TestResolveWithMonitoring(String name, String domain) throws DNSSDException, InterruptedException { System.out.println("TestResolveWithMonitoring Starting"); DNSSDService r = DNSSD.resolve(0, DNSSD.ALL_INTERFACES, name, "_example._tcp", domain, this); System.out.println("TestResolveWithMonitoring Running"); Thread.sleep(30000); System.out.println("TestResolveWithMonitoring Stopping"); if (monitorQ == null) r.stop( ); else monitorQ.stop( ); try { Thread.sleep(1); } catch (Exception e) { e.printStackTrace( ); System.exit(-1); } } public static void main(String[] args) { if (args.length != 2) { System.out.println("Usage: java TestResolveWithMonitoring name dom"); System.exit(-1); } else { try { new TestResolveWithMonitoring(args[0], args[1]); } catch (Exception e) { e.printStackTrace( ); System.exit(-1); } } } }

After you've compiled TestRegisterWithUpdates and TestResolveWithMonitoring, we can test them. In one terminal window, run:

% java TestRegisterWithUpdates "My Chosen Name"

While that's still running, in another terminal window, run TestResolveWithMonitoring:

% java TestResolveWithMonitoring "My Chosen Name" local TestResolveWithMonitoring Starting Service Resolved: mymac.local.:54444 Flags: 0, ifIndex: 5, FQDN: My\032Chosen\032Name._example._tcp.local. TestResolveWithMonitoring Running txtvers=1 status=Ready txtvers=1 status=Steady txtvers=1 status=Go TestResolveWithMonitoring Stopping

First, the TestResolveWithMonitoring client resolves the name. After it's discovered the target host and port, it starts a query for the TXT record and stops the resolve. Now, each time the TXT record is updated, the queryAnswered method gets called with the new data.

In the first version of the Java DNS-SD API there was a bug that if you stopped one DNS-SD operation and then immediately started another, the new operation could begin reusing the same underlying file descriptor before the background thread had finished cleaning up. To avoid running into this bug there are a couple of precautions you can take:

  • Use a THRead.sleep(1); after stopping any operation, to allow the background thread to run and do its necessary cleanup.

  • If you have a sequence of code that starts and stops DNS-SD operations, particularly in a listener callback method, write your code to start all the new operations first, before it begins stopping old operations. That way the kernel won't be tempted to reuse the same file descriptors, because at the point that you start each new operation, the old operations haven't been stopped yet, so the file descriptors are still in use and aren't eligible to be recycled.

Now that we've built some toy one-page programs to demonstrate the concepts, it's time to write a real program that actually does something.

Категории