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
   =(Translator)Naming.lookup("rmi://theHost/SpanishTranslator");

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
  =(Translator)RMIDiscovery.lookup(clazz,id);

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

Server Client
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:

RMI-DISCOVERY,5000,Translator,Spanish

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
RMILookup.bind(server,"Spanish")

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 224.0.0.1 to 239.255.255.255, inclusive. Some vendors reserve some of these address/port combinations; for example, Sun reserves Jini the combination 224.0.1.85:4160. (A list of reserved addresses can be found at https://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="228.5.6.7";
MulticastSocket socket=new MulticastSocket(port);
InetAddress address=InetAddress.getByName(multicastAddress);    
socket.joinGroup(address);
byte[] buf = new byte[512];
DatagramPacket packet=new DatagramPacket(buf, buf.length);
socket.receive(packet);
//parse packet etc
socket.leaveGroup(address);

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:

while(active){
    byte[] buf=new byte[512];
    DatagramPacket packet=new DatagramPacket(buf,buf.length);
    socket.receive(packet);
    //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
   //<header><delim><port><delim><interface><delim><serviceName>
 
  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();
 _serviceInterface=c.getInterfaces();
//part of the matching code
//interfaceName is part of the request
boolean match=false;
for(int i=0;!match && i<_serviceInterface.length;i++){
    match=_serviceInterface[i].getName().equals(interfaceName);
}

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));
oos.flush();
oos.close();

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.

To access the underlying object, you invoke the get() method on MarshalledObject in the client.

Implement the client-side classes

Earlier we described how an RMI client would discover an RMI server by specifying the interface class and the server’s unique name as shown below:

Class clazz=Translator.class;
String id="Spanish";
Translator service
  =(Translator)RMIDiscovery.lookup(clazz,id);

Before we look at implementing our RMIDiscovery class, let’s quickly recap its responsibilities:

  1. Listen for unicast responses from the server’s RMILookup
  2. Send UDP packets to multicast address
  3. Read remote object from stream
  4. Stop sending multicast packets
  5. Stop listening on unicast socket
  6. Use server

Set up a unicast TCP/IP listener

To set up a unicast TPC/IP socket, we must pick a port on which to listen. However, we can’t simply define a constant with a fixed port number because another process may be using that port. We therefore need to specify a range of ports to use:

private ServerSocket startListener(int port,int range){
    
    ServerSocket listener=null;
    for(int i=port;listener==null && i<(port+range+1);i++){
        try{
            listener =new ServerSocket(i);
        }catch(IOException ex){
            //port (probably) already in use
            //handle exception
    }
    return listener;
}

The startListener() method attempts to create a ServerSocket on a port within the specified range. The calling method can then check whether the return value is not null (null indicates that a ServerSocket could not be created) and obtain the port being used. Another option would be to throw an exception if the ServerSocket couldn’t be created:

ServerSocket listener=startListener(START_PORT,RANGE);
if(listener!=null){
    int port=listener.getLocalPort();
    //format message to include port number
    //start the multicast message dispatcher
    Socket sock=listener.accept();
    //read remote stub from stream
}

Once we successfully set up the unicast listener, we can format the message packet and start the multicast dispatcher.

Set up a multicast UDP dispatcher

As with the multicast listener, we must use a known multicast address/port combination. We should access this data either via System properties or via a constant:

int port=6789; 
String multicastAddress="228.5.6.7";
MulticastSocket socket=new MulticastSocket(port);
InetAddress address=InetAddress.getByName(multicastAddress);   
socket.joinGroup(address);
                   
//outMsg is the delimited request
byte [] buf=outMsg.getBytes(); 
//loop n times or until response received by unicast listener   
DatagramPacket packet=new DatagramPacket(buf,
                         buf.length,address,multicastPort);  
socket.send(packet);
//end loop
socket.leaveGroup(address);
socket.close();

Stepping through the code, you can see that once we configure the MulticastSocket, the outMsg String is converted into a byte array ready to be sent down the socket. The comments indicate that we then send the message a preconfigured number of times or until the unicast listener receives a response. We have omitted the thread coordination with the unicast listener from the examples to keep them concise; you can download the full source code to see how this is done.

Read the server’s stub

Earlier we saw how to set up the unicast ServerSocket. Now we need to look at the code that reads the server’s stub. The method ServerSocket.accept() is blocking, so it won’t return with a Socket object until an incoming connection is made:

Socket sock=listener.accept();
ObjectInputStream ois=new ObjectInputStream(sock.getInputStream());
MarshalledObject mo=(MarshalledObject)ois.readObject();
sock.close();
//server is a field
server=(Remote)mo.get();

Once we have a reference to the server, we can then wake up the thread blocking in the call to RMIDiscovery.lookup(), which will return the Remote object to the client.

Adopting Jini

In this article, we showed how you can apply a similar technique to Jini’s discovery concept to vanilla RMI clients and servers. Although we recommend using Jini for new projects, you could benefit from enhancing existing RMI systems with a discovery-like mechanism.

The RMI discovery mechanism described above has some limitations that Jini can overcome. For example, multicast UDP has a restricted range often within a subnet. This means that clients using our multicast mechanism cannot discover RMI servers running outside the multicast range. Jini, however, has the concept of federated lookup services to join different subnets and make the discovery process transparent to clients across a WAN (wide area network).

We encourage readers to download and experiment with the full source code. One interesting experiment would be to use the RMILookup utility class in an RMI server that is either a repository or proxy for other remote references to servers running outside the multicast range.

Ultimately, Jini is a better and more elegant solution, so we strongly advise readers who haven’t started to experiment with Jini to do so soon.

Finally, it is worth noting that multicast UDP generally won’t work on standalone machines not connected to a hub. Using a loopback adapter is an option; however, we encountered problems on Windows-based machine with that approach.

Many thanks to Dan Creswell at IntaMission for suggesting the RMI discovery-like solution.

Philip Bishop is an
independent Java consultant specializing in the design and
implementation of distributed Java systems.

Nigel Warren is director
of technology at IntaMission, creators of
IntaSpaces, the evolvable JavaSpace. Nigel Warren and Philip Bishop
are coauthors of Java in Practice (Addison Wesley, January
1999; ISBN: 0201360659).

Source: www.infoworld.com