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.
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.
The table below describes in more detail the interactions between the RMI client and RMI server.
Interactions between RMI client and RMI server
|
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:
- Set up a multicast UDP socket to listen for requests
- Check the protocol header when a packet arrives
- Parse the message packet
- Match the unique server name parameter
- Match the interface parameter
- 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:
- Thread per request: Create a new thread to handle each request
- 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“)
- 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 ClassNotFoundException
s 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:
- Listen for unicast responses from the server’s
RMILookup
- Send UDP packets to multicast address
- Read remote object from stream
- Stop sending multicast packets
- Stop listening on unicast socket
- 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.