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


jRendezvous: Java and Rendezvous Working Together

by Seth Ladd
07/29/2003

To Apple's credit, they design with the user in mind. This is true not only for their user interface components, but for the internals of their products as well. For instance, beginning with OS X 10.2 (Jaguar), even the networking experience is user friendly. With the introduction of Rendezvous, Apple has introduced the ease of use of AppleTalk to a standards compliant IP network.

While there are no standard development languages for Mac OS X, Java developers will feel right at home. The Java runtime and JDK are shipped and ready to run, allowing the vast amount of Java applications and applets to run natively on OS X. The Java "Write Once, Run Anywhere" promise continues to be press on.

With Rendezvous making IP networking simple, and Java making cross platform network programming possible, we see the possibility for a beautiful friendship. An implementation of Rendezvous for Java, called jRendezvous, now allows Java programs running anywhere (not just Mac OS X) to participate in Rendezvous networks. With jRendezvous, Java programs can listen for, and advertise, services on the network with ease.

Related Reading

Mac OS X for Java Geeks
By Will Iverson

Rendezvous Review

With the release of Jaguar (OS X 10.2), Apple introduced Rendezvous, their branding of their Zeroconf implementation. Zeroconf is a set of IETF standards for a Zero Configuration IP network. A Zero Configuration network means two things. The network itself does not need to have a DHCP or DNS server installed for clients to use. Any client that connects to the network will obtain an IP address through standard negotiation protocols. A Zero Configuration network also means that clients can discover services on the network. For instance, a client does not need to be preconfigured with an IP address of a printer. Through Rendezvous's discovery mechanisms, the client will automatically become aware of all Rendezvous-enabled printers on the network. It is this second Rendezvous feature we will look at closely with jRendezvous.

There is already a great collection of Rendezvous and zeroconf information available. If you are unfamiliar with either, you might want to investigate these resources:

Specifying a Service

Much of what is discussed below concerns either listening for or advertising a service. While a service's implementation is orthogonal to Rendezvous, the way it is named is well specified. Before we look at the classes and interfaces of jRendezvous, it is important to understand Rendezvous's service naming. jRendezvous follows the standards specified in DNS-Based Service Discovery, so we will review them now.

Rendezvous services are specified with a Service Instance Name. It is constructed from three parts: an instance name, a service name, and a domain name.

The instance name is any arbitrary UTF-8 encoded text. It has a maximum of 63 octets. The text of the instance name can have any combination of characters, and usually distinguishes a service from others within its type.

A service name is specified with an application protocol name and a transport protocol name. The protocol name must be registered with IANA's assigned application port numbers. An example of an application protocol name is _http. The transport protocol value can be either _tcp or _udp. Notice the underscores preceding the names, they are required.

The domain name is a standard DNS domain name. For instance, both "brivo.com" and "math.dept.university.example.edu" are valid here. Also valid is simply "foo.local.", which indicates a machine on the local link.

jRendezvous often uses what it calls a type name. It is a combination of the service name and a domain name in the form of "local.". A type name of _http._tcp.local. would identify all instances of HTTP servers running over TCP on the local link. The type name is used when a service browser wishes to search for services of a particular type.

A fully qualified Service Instance Name is formed by combining all three name parts. An example Service Instance Name is Seth's Web Server._http._tcp.local.. A Rendezvous Service advertisement includes a server name with the service name. The server name tells clients what machine is hosting the service. While this server name resolves to a physical IP address, clients should never cache that address. Rendezvous works so well because services may move around on the network, even changing IP addresses. Clients should cache the Service Instance Name instead, allowing the service the flexibility of movement.

Warning: jRendezvous, at the time of this writing, has a bug regarding the server name of services. In fact, it doesn't support it at all. It assumes the service name is the same as a server name. This might be true for some situations, but for many, it is not a safe assumption. For instance, many different web services may be hosted on a single physical server. "Seth's Web Site" and "Justin's Web Site" are distinct services, but would be hard to tell apart if they had to be named "www". I have provided a patch for this, so don't worry.

jRendezvous Overview

jRendezvous is the work of Arthur van Hoff while at Strangeberry. Its current version, 0.1.2, supports service discovery and registration. It's available for download via Strangeberry's public FTP server (though the examples only work with my patched version, see below). jRendezvous is licensed under the LGPL. This makes it available for both proprietary and open software.

Because of the bug mentioned above, I strongly encourage you to download the patched version of the library. I have provided both the source and the compiled jar. The source and jar provided are considered patched to the full jRendezvous distribution from Strangeberry. The Strangeberry distribution has the license and javadocs. To get the full experience, please get the native package, and then the patched sources and jar.

Package, Class, and Interface Tour

Let's dive into the structure of jRendezvous. Luckily, there are both sample programs and tools built on jRendezvous. There is plenty of code to read to gain a deep understanding of this library.

From a user's perspective, there are three main classes that are interesting. We will be looking at the com.strangeberry.rendezvous.Rendezvous, com.strangeberry.rendezvous.ServiceListener, and com.strangeberry.rendezvous.ServiceInfo classes. With just these three classes, a Java program can both advertise and discover services.

com.strangeberry.rendezvous.Rendezvous

The Rendezvous class is the main workhorse of the package. To begin using this class, simply create an instance with the default constructor.

Rendezvous rendezvous = new Rendezvous();

There are three methods provided by the Rendezvous class we will look at.

The addServiceListener is used to register a ServiceListener to be notified of services arriving and leaving the network. The requestServiceInfo method is used to obtain a full ServiceInfo object encapsulating a service. And the registerService method advertises a local service for the network to discover.

com.strangeberry.rendezvous.ServiceListener

To be notified of services on the network, create an implementation of the com.strangeberry.rendezvous.ServiceListener interface and pass it to the rendezvous instance using the addServiceListener method. You also need to specify the type of the service you want the listener to listen for.

The callbacks from the ServiceListener class include:

The addService method is called by the rendezvous object when a service is first discovered on the network. All that is known at this point is the type and its instance name. To fully resolve the service, use the requestServiceInfo method from the passed rendezvous object. Once the service is resolved and more information is known, the resolveService method is called giving your listener a fully populated ServiceInfo object. The removeService callback is called when a service has been removed from the network.

To see the ServiceListener in action, there's a small sample included in the jRendezvous download. The example class is com.strangeberry.rendezvous.sample.SampleListener. It demonstrates a simple listener registered with a Rendezvous object. Luckily, jRendezvous doesn't need any other third party libraries on the classpath. To run this sample, use the following command (assuming you have the java executable on the path) from the directory where you unzipped the jRendezvous distribution.

java -cp jrendezvous.jar com.strangeberry.rendezvous.sample.SampleListener

The class, by default, listens for advertisements to the _http._tcp.local. type. If you have OS X's default Apache installation running, you should see the users' home WWW directories being advertised. This is due to the mod_rendezvous_apple Apache module bundled by Apple starting at OS X 10.2.4. This example class outputs the Rendezvous network conversations so you can watch and learn what is going on under the hood.

com.strangeberry.rendezvous.ServiceInfo

The final class we will look at is the ServiceInfo class. It's an encapsulation of a Service. The rendezvous object will advertise a ServiceInfo object to the network. Note that an instance of the ServiceInfo doesn't have to be the same object as the service itself. The service's advertisement and the service can be two separate things. It is more useful to have their lifecycles connected, though. If the service leaves the network, the ServiceInfo object should cease to be advertised as well.

Remember the bug mentioned above? The native version of the class did not allow a server name to be declared. The following constructor is recommended and can only be found in the patched version.

public ServiceInfo(String type, String name, InetAddress interface, int port, Hashtable props, String serverName)

To sum up the included Javadocs, this constructor creates a ServiceInfo object from a service type name, an instance name, the interface the service is running on, the port the service is listening to, a hashtable of service specific properties, and the hostname of the server where the service lives. An example follows:


Hashtable props = new Hashtable();
props.put("path", "/index.html");

ServiceInfo info = new ServiceInfo(
        "_http._tcp.local.", "Seth's Web Site",
        InetAddress.getByName("foo.example.com"), 80, props,
        "foo.example.com");

The jRendezvous download provides com.strangeberry.rendezvous.sample.SampleRegistration as a sample service registration to study. When run, it advertises a local web server (that may or may not actually be there) on port 80. Using the following command, you should see a "Hello, World" web server in Safari's Rendezvous bookmarks. If a page is actually served is dependent on a web server running on that port. This illustrates the potential separation of the service advertisement from the service itself.

java -cp jrendezvous.jar com.strangeberry.rendezvous.sample.SampleRegistration

Implementation Example: Integrating Rendezvous with Tomcat

Let's check where we are now. We've reviewed Rendezvous, how Services are named, provided a patched version of jRendezvous, and explored jRendezvous's important classes and interfaces. We're ready now to do something interesting.

With Safari, Apple's browser, users can discover and browse web sites on their local network. One way to do this is to use Apple's mod_rendezvous_apple, already bundled with their Apache distribution. To experience this, display Safari's bookmarks and select the Rendezvous Collection from the left. We'll use this to test our Tomcat integration.

Rendezvous in Safari Screenshot
Rendezvous in Safari Screenshot

Why should mod_rendezvous_apple users have all the fun? With jRendezvous and Jakarta's Tomcat Web Server and Servlet container, you can make Java powered web sites become Rendezvous services.

Servlet containers have the concept of a Context, which is a logical grouping of server resources. For instance, all of Justin's files and images might be under the /~justin context. Fortunately, Tomcat has the org.apache.catalina.core.StandardContext class that encapsulates this concept nicely. We will thus advertise Servlet contexts as Rendezvous services.

We first create a subclass of StandardContext called RendezvousContext. This class will have a static Rendezvous member that all ServiceInfo objects will register with. The entire Tomcat instance should have only one Rendezvous instance running within it, so keeping it static ensures this no matter how many RendezvousContexts are running.


public class RendezvousContext extends StandardContext {

    private static Rendezvous rendezvous = null;

    private int port = 0;
    private InetAddress address = null;
    
    static {
        try {
            rendezvous = new Rendezvous();
        } catch(IOException ioe) {
            ioe.printStackTrace();
            rendezvous = null;
        }
    }

    public RendezvousContext() {
        super();
    }
    
    ...

The method of StandardContext that is analogous to the init() method is setParent(Container parent). For explanations of Tomcat's Containers, consult their source or javadocs. For our purposes, it's only important to understand that setParent is called as the object instance is finished being setup by the container. It is here we will create the instance of ServiceInfo to advertise this context.


public void setParent(Container parent) {
    super.setParent(parent);

    createRendezvousService();
}

private void createRendezvousService() {
    if (rendezvous == null) {
        return;
     }

    Hashtable props = new Hashtable();
    props.put("path", getPath() + "/");

    try {
        ServiceInfo serviceInfo = new ServiceInfo(
                "_http._tcp.local.", getPath(),
                getAddress(), getPort(), props,
                getAddress().getHostName());

        rendezvous.registerService(serviceInfo);
    } catch(IOException ioe) {
        ioe.printStackTrace();
    }
}

The above code sets a "path" property to the context's base path. For instance, if this was a context for a user's home directory, the path property would be set to "/~user". This path property is a standard property for HTTP Rendezvous services. Safari uses this path property to properly setup a URI from the service advertisement. The StandardContext class provides a method called getPath() that provides the path of the context. We also use the getPath method to name the service, for lack of a better name. A future version of this context could get its name from the Tomcat configuration file.

The most difficult piece of the RendezvousContext was determining, at runtime, what the server's address and port number are. The service advertisement must be able to advertise where the server is hosted. The StandardContext class does not have any methods that directly provide this information. Normally, a servlet context does not care what IP addresses or port numbers are hosting it. The context might even be hosted via several addresses.

Luckily, the Tomcat API is very flexible; with a little hunting we can find a way to discover an IP address and port number that is hosting the context. Tomcat uses Connectors to physically connect contexts to the outside would. The connector might connect via HTTP/1.1, or through some other protocol to another web server. We want to advertise servlet contexts that can be reached via HTTP/1.1, so we specifically look for those connectors. Below is an example of one way to obtain a connector that is hosting this context. A more advanced version of this code would set up a service advertisement for each connector.


private CoyoteConnector getCoyoteConnector() {
    CoyoteConnector coyoteConnector = null;

    if (getParent() instanceof Host &&
            getParent().getParent() instanceof Engine) {

        Engine engine = (Engine) getParent().getParent();
        Service service = engine.getService();
        Connector[] connectors = service.findConnectors();
        for (int i = 0; i < connectors.length; i++) {
            Connector connector = connectors[i];
            if (connector instanceof CoyoteConnector &&
                ((CoyoteConnector)connector).getProtocolHandlerClassName()
                    .toUpperCase().indexOf("HTTP") != -1) {

                coyoteConnector = (CoyoteConnector) connector;
                break;

            }
        }
    }

    return coyoteConnector;
}

Once we have a reference to the connector, we can find the port and IP address that the connector is listening to.


private int getPort() {

    if (this.port == 0 && getCoyoteConnector() != null) {
        this.port = getCoyoteConnector().getPort();
    }

    return this.port;
}

private InetAddress getAddress() {
    if (this.address == null && getCoyoteConnector() != null) {
        String addr = getCoyoteConnector().getAddress();
        if (addr == null) {
            try {
                this.address = InetAddress.getByName(
                        InetAddress.getLocalHost().getHostName());
            } catch(UnknownHostException uhe) {
                uhe.printStackTrace();
            }
        }
    }

    return this.address;
}

That's all the code required to advertise a Tomcat servlet context as a Rendezvous service. The entire source is available for download, along with a compiled class version.

Installation of this class is quite straight forward. Place the compiled class file (RendezvousContext.class) into Tomcat's $TOMCAT_HOME/server/classes directory. Place the modified, bug fixed jrendezvous.jar into $TOMCAT_HOME/server/lib.

Finally, we must configure Tomcat to use this class when constructing contexts. Use the contextClass attribute of the Context element in the Tomcat configuration to specify the custom RendezvousContext class. Below is the configuration for hosting user directories via Tomcat with the Rendezvous functionality. Note that it is specific to Mac OS X, due to its use of the /Users directory. The O'Reilly article Top Ten Tomcat Configuration Tips explains this and other configurations very well.


<Listener className="org.apache.catalina.startup.UserConfig" 
directoryName="Sites" homeBase="/Users" 
contextClass="RendezvousContext"
userClass="org.apache.catalina.startup.HomesUserDatabase"/>

This example was tested with Tomcat 4.1.24 LE JDK1.4 on Mac OS X and Linux 2.4. The RendezvousContext might be portable over different Tomcat versions because of its usage of internal APIs that normally should be hidden from user code.

Once you startup Tomcat, use Safari's Rendezvous bookmarks to find your user's home directories. You should see two items for every user, one for Apache's native mod_rendezvous_apple, and one for your Tomcat server. Those user directories being served by Tomcat will look like /~user. If the new entries are missing, check Tomcat's catalina.out or other log files in the $TOMCAT_HOME/logs directory for any errors.

Final Thoughts

jRendezvous, once we worked through the few bugs, is a joy to work with. I find its API clean and simple. It's a great way to integrate Rendezvous service discovery and advertisement into your Java programs. I hope that it continues to be maintained.

The more I worked with Tomcat's internal APIs, the more I appreciated it. I think the Tomcat developers are doing a wonderful job in constructing a very flexible system that is both user friendly and developer friendly.

There is one small deficiency in the RendezvousContext class. It does not clean itself up correctly. It should capture a shutdown type event to correctly stop the Rendezvous instance. Otherwise, the threads inside Rendezvous continue to run. Issuing the shutdown command won't actually shutdown Tomcat. A challenge to the reader: adapt the RendezvousContext class to shutdown gracefully.

Seth Ladd is Lead Software Architect at Brivo Systems, Inc. He develops servlet and J2EE systems for REST based RDF and Ontology applications.


Return to Mac DevCenter

Copyright © 2009 O'Reilly Media, Inc.