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 2

by Daniel H. Steinberg, Stuart Cheshire

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:

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.

Zero Configuration Networking: The Definitive Guide

Related Reading

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

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 translate—at time of use—from 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 times—when struggling with other problems—that 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.

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 bytes—yet 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 keys—the 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

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 applications—iChat being the prime example—that 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.

TIP: 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:

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.

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.