Jini-like discovery for RMI

Take advantage of Jini's discovery mechanism for RMI development

If you follow Jini developments, you know that Jini clients don't need to know where a service is located; they simply use the discovery mechanism to obtain a proxy to the service they want to use. Conversely in RMI (Remote Method Invocation), you must know the URL of the server you want to use. In this article, we show how you can implement a Jini-like discovery mechanism for RMI, which frees some clients from having to know an RMI server's lookup URL.

Your first thought may be, Why bother to do this in the first place; why not just use Jini? We would agree with this logic, especially for new systems. However, many RMI-based systems still exist, and until Jini is accepted into mainstream Java development, we need to provide more elegant RMI solutions. In fact, the work described here is the result of such a requirement: to develop a Jini service that can also run as a standalone RMI server, but uses Jini-like discovery.

This article is primarily targeted at RMI developers who haven't adopted Jini. By giving insight into what occurs under the Jini hood, we hope you begin to understand how powerful Jini mechanisms are. We certainly don't encourage you to reimplement Jini, but this article may help you understand how these mechanisms work. It may even help you convince your managers or department heads that they should consider Jini as a viable technology for distributed systems.

We won't go into depth about the Jini discovery mechanism, so if you are not familiar with it, we recommend you quickly review Bill Venners's "Locate Services with the Jini Lookup Service."

Basic RMI and Jini lookups

In RMI, a client must know the location of the server to which it wants to connect. An RMI server's address is in the URL form rmi://<host>:<port>/<servername>, where the port number is the port on which the rmiregistry listens for requests. For example:

Translator service

In Jini, a client finds a service using a Jini utility class, such as ServiceDiscoveryManager. In the example below, we create a ServiceTemplate instance with a list of classes; or in our case, the class we want to match -- the Translator.class:

Class [] classes=new Class[]{Translator.class};
ServiceTemplate tmpl=new ServiceTemplate(null,classes,null);
ServiceDiscoveryManager lmgr=new ServiceDiscoveryManager(null,null);
ServiceItem serviceItem =lmgr.lookup(tmpl,null);
Translator service=serviceItem.service;

As you can see from the example, the ServiceDiscoveryManager uses the lookup() method to find any available services that match the ServiceTemplate. You may also associate any number of attributes with a service lookup; we didn't here in order to keep things simple and essential.

Comparing both lookup mechanisms, you will notice that the service location isn't specified in the Jini version. It's worth pointing out that you can specify a lookup service location if required, but not the location of the actual service you want. The Jini model's power is that we don't have to know or care where a service is located.

Having compared both RMI and Jini lookup mechanisms, we can now think about how to access an RMI server in a Jini-like fashion.

A location-neutral RMI lookup

Ideally, we would like to find the first matching instance of a discovered Translator:

Translator service

Here, clazz is the RMI service's interface, and id is some unique string to differentiate between server instances implementing the clazz interface. For example, to find a Spanish translator, we use the following:

Class clazz=Translator.class;
String id="Spanish";

Now that we have a high-level idea of how to use RMI discovery, we can start investigating how to implement it. As we attempt to implement a "poor man's" discovery for RMI, we can look at how Jini does it and then adapt those principles/concepts in a way that suits an RMI server and client.

The discovery mechanism

Jini's primary discovery mechanism uses a combination of multicast UDP (User Datagram Protocol) and unicast TCP/IP. In simple terms, this means that a client sends a multicast request packet, which gets picked up by lookup services listening for it. The lookup services make unicast connections back to the client and serialize their proxy down the stream available over the connection. The client then interacts with the lookup service (proxy) to locate the service it wants. Figure 1 illustrates this process.

Figure 1. Jini multicast discovery

There is considerably more to discovery than this, but we're only interested in the key concepts of multicast UDP and unicast TCP/IP.

We won't attempt to implement a standalone RMI lookup service equivalent. Instead we will implement a simple multicast listener/unicast dispatcher that an RMI server can use, effectively making each RMI server act as its own lookup service. On the client side, we write the counterparts for the server-side sockets -- a multicast dispatcher/unicast listener. This means that the participants in Figure 1 become the RMI client and RMI server, shown below in Figure 2.

Figure 2. RMI multicast discovery

The table below describes in more detail the interactions between the RMI client and RMI server.

Interactions between RMI client and RMI server

Start listening on multicast address. 
 Start ServerSocket to listen for unicast responses from the server.
 Begin sending UDP packets to multicast address.
Parse received UDP packet. If valid, connect back to the client via unicast TCP/IP. 
Send remote stub to client. 
 Read remote object from stream.
 Close ServerSocket. Stop sending UDP multicast packets.
 Start using service.

The discovery protocol

Earlier we outlined how we want an RMI client to discover a server: it would specify an interface class and a unique name to identify a server instance. This is because multiple servers that implement the same interface may be running concurrently.

Before we implement our RMI discovery mechanism, we must define the protocol for message passing between participants. For simplicity, we'll use a delimited string containing all the information a RMI server needs to respond to a matching request. First, we define a protocol header. This prevents the server classes from attempting to fully parse packets that arrive from other sources. The remainder of the message packet will contain the unicast response port, the server's interface class name, and the server instance's unique identifier.

Below is the format of the discovery request message we will use:

<protocol>,<unicast port>,<interface class>,<unique id>

Now let's look at a sample message packet a client might send to discover a Spanish instance of the Translator server. RMI-DISCOVERY is the protocol header, and 5000 is the port number on which the client is listening for responses:


We don't include the client's hostname in the request because that information can be obtained from the UDP packet received on the server. Having defined our message format, we can start implementing the discovery classes.

Implement the server-side classes

Our cunning plan is to write a utility class that RMI servers can use to create their own personal lookup service:

//instantiate RMI server
Remote server=new SpanishTranslator();
//initiates discovery listener

The Remote parameter checks if the server implements the interface the client is trying to discover and whose RMI stub is ultimately serialized back to the client. The String parameter compares the server name with the name in the request packet.

Before going forward, let's quickly recap the server-side classes' responsibilities:

  1. Set up a multicast UDP socket to listen for requests
  2. Check the protocol header when a packet arrives
  3. Parse the message packet
  4. Match the unique server name parameter
  5. Match the interface parameter
  6. If 4 and 5 match, serialize the server's remote stub to the client via unicast TCP/IP socket

Set up a multicast UDP listener

To set up a multicast listener, you must use a known multicast address and port; those in the range to, inclusive. Some vendors reserve some of these address/port combinations; for example, Sun reserves Jini the combination (A list of reserved addresses can be found at http://www.iana.org/assignments/multicast-addresses.) Operating on the same frequencies as other vendors is not recommended, so we chose to use the same combination used in the MulticastSocket Javadoc example:

int port=6789; 
String multicastAddress="";
MulticastSocket socket=new MulticastSocket(port);
InetAddress address=InetAddress.getByName(multicastAddress);    
byte[] buf = new byte[512];
DatagramPacket packet=new DatagramPacket(buf, buf.length);
//parse packet etc

The above example shows how simply you can set up a multicast listener and receive packets on that address/port combination. In the above example, only a single packet can be processed, so we must create a loop around the DatagramPacket creation and socket.receive(); otherwise only one client will be able to discover the server:

    byte[] buf=new byte[512];
    DatagramPacket packet=new DatagramPacket(buf,buf.length);
    //process packet

We could use several strategies to process packets received:

  1. Thread per request: Create a new thread to handle each request
  2. Thread from a thread pool: Use a preinstantiated thread from a (possibly fixed) resource thread pool (see "Java Tip 78: Recycle Broken Objects in Resource Pools")
  3. Blocking: Only process one request at a time, other requests must wait

Due to the nature of the way we initiate discovery from the client, a blocking strategy will work here. This is because our clients will continue to send discovery messages at intervals until either the service is located or a predetermined number of requests have failed -- more on that later.

Check the protocol header

Having received a packet, we now check that the contained message is one of ours. To do this, we convert the byte [] into a String and use the startsWith() method. Although we have hardcoded the protocol header RMI-DISCOVERY in the example below, it will be accessed as a constant in the actual source code:

String msg=new String(packet.getData()).trim();
boolean validPacket=msg.startsWith("RMI-DISCOVERY");

Parse the message

Assuming we have a valid packet, we can parse the message. As the message is delimited, we can use StringTokenizer to unpack it:

private String [] parseMsg(String msg,String delim){
   //request in format
  StringTokenizer tok=
       new StringTokenizer(msg,delim);
   tok.nextToken(); //protocol header
   String [] strArray=new String[3];
   strArray[0]=tok.nextToken();//reply port
   strArray[1]=tok.nextToken();//interface name
   strArray[2]=tok.nextToken();//service name
   return strArray;            

Once we convert the message packet into its parameters, we can check the interface class name and unique server name against the server's name.

Match the interface and server name

To match the server's unique name against the parameter, you simply compare the two String objects. If you download the full source code, you will see that the RMILookup class takes two parameters: one specifies its unique name and the other is the Remote object.

You can compare the interface names within the interface array implemented by the server:

//done at start up
Class c=_service.getClass();
//part of the matching code
//interfaceName is part of the request
boolean match=false;
for(int i=0;!match && i<_serviceInterface.length;i++){

Unicast connection back to discoverer

If both the unique server name and interface class match, we can attempt to connect back to the client and serialize the server's stub:

//repAddress has been obtained from the incoming DatagramPacket
//repPort has been parsed from the message packet
//_service is the RMI server's Remote ref (stub)
Socket sock=new Socket(repAddress,repPort);
ObjectOutputStream oos=new ObjectOutputStream(sock.getOutputStream());
oos.writeObject(new MarshalledObject(_service));

One interesting point to note is the use of MarshalledObject in the above example. Had we simply serialized the Remote object down the stream, a ClassNotFoundException would occur on the client, unless the client had access to the server's stub (which in most cases is a bad thing). The client would experience ClassNotFoundExceptions because, unlike passing objects over RMI where the codebase is annotated into the stream, here we are using serialization over a socket, which doesn't include the codebase.

MarshalledObject was introduced in Java 2 and provides, among other things, a convenient way to pass serialized objects along with their codebases. Under the hood, MarshalledObject serializes an object into a byte array, which means when the MarshalledObject gets deserialized, the underlying object doesn't. This is extremely useful to Jini services, such as lookup services, as they aren't forced to download classes referenced by registered proxies.

1 2 Page 1
Page 1 of 2