Introduction
Java Reflective Broker (JRB - pronounced jerby) is a distributed object model for the Java language that retains the semantics of the Java object model, making distributed objects easy to implement and to use. The system uses two important frameworks of the JDK1.1, namely Java Serialization and Java Reflection.
Rationale
Distributed systems require that computations running in different address spaces, potentially on different hosts, be able to communicate. For a basic communication mechanism, the Java language supports sockets, which require the client and server to engage in applications-level protocols to encode and decode messages for exchange, and the design of such protocols is cumbersome and error-prone.
An alternative to sockets are Object Request Brokers (ORBs) that abstract away the communication interface to the level of a remote method invocation. Instead of working directly with sockets, the programmer has the illusion of calling a local method, when in fact the arguments of the call are packaged up and shipped off to the remote target of the call. Traditional ORBs, such as CORBA and DCOM, are designed to work across languages, operating systems, and hardware platforms, thereby falling short of seamless integration with the specific features of a certain environment. CORBA users know that mapping IDL constructs into programming languages, such as C, C++, Smalltalk or Java, can be clumsy and unnatural.
The new Java Development Kit 1.1 supports Java RMI (Remote Method Invocation) which assumes the homogeneous environment of the Java Virtual Machine and can therefore take advantage of the Java object model whenever possible. Though Java RMI is far simpler than CORBA, some drawbacks still remain:
- Apparently Microsoft will not support Java RMI in the forthcoming version 4 of Internet Explorer. After the recent announcement by Apple Computers it is clear that Internet Explorer cannot be neglected by any applications that uses the browser metaphor for the client interface. Even Netscape seem to campaign for CORBA IIOP and the extent of their support to Java RMI is not clear at the moment.
- Java RMI is a heavy communication system. Its classes amount to 175KBs and the performances are poor due to the complex architecture and the distributed garbage collection mechanism that introduces a considerable overhead on the wire. The processing may be overwhelmed by the RMI system and this may be an unacceptable bottleneck for some applications.
- Though simpler than CORBA, RMI still introduces a number of steps and rules to remote-enable a Java class. Remote services are specified by inheriting from the Remote interface; server classes should inherit from the UnicastRemoteObject class; stub and skeleton classes have to be generated through compilation, etc. The Java RMI Specification itself amounts to 90 pages and the Java classes and interfaces that represents the API of the framework are 23. Some applications may be happier with an ORB that abstracts away the low-level communication model of sockets but at the same time offers a simple and straightforward architecture.
- By looking through the JDK1.1 it emerges that the several extensions have been worked out independently of one another. This is quite natural as JavaSoft was wrestling against time, but the result was that some frameworks had to build from scratch their foundations while they might have benefited from the support of other frameworks. This is the case for Java RMI whose design did not take into account the important features offered by Java Reflection.
On account of the above we have developed JRB. JRB is not a competitor of Java RMI but a niche product targeted for those applications that care for the aforementioned issues, in particular JRB:
- can be used with both Microsoft Explorer 4 and Netscape Communicator;
- is very lightweight. JRB is an orblet, meaning that it migrates into the browsers along with applets and for this reason it needed to be very small. The size of the whole JRB package is about 12 KBs which includes both the client and server side. If an applet only behaves as a client of a remote invocation JRB takes less than 5 KBs. JRB doesnt support a distributed garbage collection mechanism. Besides being costly in term of performance, distributed garbage collection is not guaranteed to work correctly due to the inherent complexity of the distributed environment. If any failures occur in the distributed environment the garbage collection mechanism may be unable to reclaim the allocated objects.
- is perhaps the simplest way to remote-enable any Java class without any modifications. Any Java class can be made available for remote invocations, including all the legacy Java classes that were not built with distribution in mind. For instance, the String class, the Hashtable class, and whatever Java class you may fancy of can be remote-enable with JRB. Also, when using JRB the programmer doesnt need to generate stubs and skeletons thereby greatly simplifying the development process.
- fully builds upon the features of Java Reflection and that is the reason way the whole package is very small. In addition, by exploiting the reflection API stub and skeleton generation can be replaced by a dynamic invocation model akin to the CORBA Dynamic Invocation Interface or the Java Reflection API itself, but this time applied to a distributed scenario.
JRB Architecture
The architecture of the JRB essentially builds upon two primary classes, the ObjectRef class and the Server class that represents respectively the client and server side of the JRB architecture.
ObjectRef class
When writing an applet or an application that uses remote objects, the programmer needs to be aware of the JRB client visible interfaces. JRB client functions are provided by the cselt.jrb.ObjectRef class used to:
- connect the client with the remote object. The constructor of the ObjectRef class accepts a URL specification to bind the remote object. URLs are provided using the Java URL class or in string format: \[protocol\]://\<host\>:\[port\]/\<objectName\>, if the protocol is omitted the default will be http.
- getHost, getPort, and getObjectName methods return respectively the remote host name, port number and the name of the remote object to which this reference is bound to.
- invoke a remote method. Once an instance of the ObjectRef class has been created that refers to a remote object, any public methods of the remote object can be invoked through the invoke method of the ObjectRef class. The invoke method uses a dynamic invocation model akin to CORBA Dynamic Invocation Interface and the Java Reflection API.
package cselt.jrb;
import java.io.*;
import java.lang.reflect.*;
public class ObjectRef implements Serializable {
public static String getLoaderName()
{...}
public static void
setLoaderName(String name) {...}
public ObjectRef(URL url) {...}
public ObjectRef(String url) throws
MalformedURLException {...}
public String getHost() {...}
public int getPort() {...}
public String getObjectName() {...}
public String toString() {...}
public Object invoke(String
methodName, Object args[])
throws Exception {...}
}
Server class
JRB server functions are provided by the cselt.jrb.Server class. This class provides support for point-to-point active object references using Java Serialization as on the wire protocol. The Server class implements a remote server object with the following characteristics:
- references are valid only for, at most, the life of the process that creates and exports the remote object.
- an http connection-based transport is used.
- invocations, arguments, and results use Java Serialization as stream protocol for communicating between client and server.
The Server class provides only static methods and is used to:
- export an object, i.e. to make the object available to invocation from remote clients. The export method is called with the object to be exported. Optionally, a name can be associated with the exported object: this is the name by which the object will be referred to by its clients. If no name is given in the export method, the JRB will assign a unique name within the Java Virtual Machine. The export method returns an instance of ObjectRef which is the object reference for the exported object. Object references can be passed as arguments or return values of any remote invocations.
- retract an exported object. As already said, JRB doesnt support a distributed garbage collection mechanism. On the contrary, servers maintain control on the exported objects: an exported object will not be garbage collected till the server explicitly retracts it.
- lookup an exported object by name.
- set and get the port number on which the server is listening. If no port number is specified the server will be listening on an anonymous port. If the setPort method is called with no argument than the server will be listening on the default port 8888. Once an object has been exported an exception is thrown on any attempts to change the port number.
package cselt.jrb;
import java.io.*;
import java.lang.reflect.*;
public class Server {
public static void setPort(int port)
throws Exception {...}
public static int getPort() {...}
public static ObjectRef export(Object
obj) {...}
public static ObjectRef export(String
objectName, Object obj) {...}
public static void retract(String
objectName) {...}
public static void retract(ObjectRef
ref) {...}
public static Object lookup(String
objectName) {...}
}
The NetClassLoader class
The cselt.jrb.NetClassLoader is a utility class that can be used by applications to load classes via a URL.
package cselt.jrb;
import java.io.*;
import java.net.*;
public class NetClassLoader extends ClassLoader {
public static void setURL(URL url)
{...}
public static void setURL(String url)
throws MalformedURLException {...}
public static URL getURL() {...}
public Class loadClass(String name)
throws ClassNotFoundException;
}
The loadClass method loads the specified class name via the URL defined by the setURL method. The class is loaded, defined, and returned. The JRB runtime may create instances of this class to load classes of arguments received in remote invocations if such classes cannot be found locally by the default class loader. Server processes must declare the location of the classes (arguments/returns) by setting the URL, using the normal protocols, such as http and ftp, with the setURL method of the NetClassLoader class, e.g.
NetClassLoader.setURL("http://loom.cselt.it/~ennio/java/codebase/");
Although the JRB system provides the NetClassLoader class, the programmer is given the chance to customize the behaviour of class loading, for example by providing a class loader that loads class with an alternative mechanism to the URL-based model. All the programmer has to do is define a class loader, e.g. by inheriting from cselt.jrb.NetClassLoader or from java.lang.ClassLoader, and then let the JRB runtime know of this new class. For example, suppose your class loader is mypackage.MyClassLoader, then you simply call:
ObjectRef.setLoaderName("mypackage.MyClassLoader");
before any remote invocation is performed.
Naming class
For a client to be able to invoke a method on a remote object, that client must first obtain an object reference. Object references are created by exporting an object with the JRB runtime system through the export operation of the Server class. Most of the time the client will obtain a reference to a remote object as a parameter to, or a return value from, another remote method call. For bootstrapping, the client must be able to obtain the first reference to a remote object. The simplest way is for the server to listen on a well known port and export an object with a name also known by the client. The client can use a URL-based naming scheme of the form host:port/objectName to create an object reference, where objectName is a simple string name used in the export operation:
ObjectRef ref = new ObjectRef("loom.cselt.it:9999/MyObjectName");
If starting a server on a well known port is considered restraining, the JRB system provides a Naming Server. The cselt.jrb.Naming class provides methods for lookup, binding, rebinding, unbinding, and listing the contents of the Naming Server using a URL-based naming model. Being Naming a remote object, clients use the JRB to invoke the methods of the Naming class. This can be done either through the invoke method of the generic object reference ObjectRef, e.g.
ObjectRef.invoke("bind", "MyObjectName", objRef);
or through the method of the specialized
object reference
NamingRef
generated by the stub generator, e.g.
NamingRef.bind("MyObjectName", objRef);
package cselt.jrb;
import java.io.*;
import java.net.*;
public class Naming {
public static int DEFAULT_PORT = 6666;
public ObjectRef lookup(String name)
throws IOException {...}
public void bind(String name, ObjectRef objRef)
throws IOException {...}
public void rebind(String name, ObjectRef objRef) {...}
public void unbind(String name)
throws IOException {...}
public String[] list() {...}
}
- The DEFAULT_PORT is the default port of the Naming Server.
- The lookup method returns the object reference bound to the specified name. Clients can cast the generic object reference to a specialized object reference. (This cast can fail in the usual ways that casts can fail in the Java language).
- The bind method associates the name with the object reference, objRef. If the name is already bound to an object an IOExcepton is thrown.
- The rebind method associates the name with the object reference, objRef. Any previous binding of the name is discarded.
- The unbind method removes the binding between the name and the object reference, objRef. If the name is not already bound to an object an IOException is thrown.
- The list method returns an array of Strings containing a snapshot of the names bound in the Naming Server.
Stub generator
Normally, the JRB will be used without stubs. The invoke operation of the ObjectRef class follows the principle of the Java Reflection API that dynamically creates an invocation and dispatches the call to the remote object. If, for any reasons, the programmer prefers to use the static invocation model, like the one supported by Java RMI, JRB provides a stub generator for creating specialized object references. A specialized object reference class is a subclass of ObjectRef that provides the same public method of the exported remote object. For example, the Naming Server of the JRB is made up of the Naming class. By applying the stub generator to this class a NamingRef class is created that provides the same methods of the Naming class and is used at the client side for remote invocations to the Naming object.
The compiler itself is written in Java and must be launched with:
> java cselt.jrb.Stub \<flags\> \<className\>
\<className\> is the package qualified class name of the remote object class. The class must have previously been compiled successfully with javac. \<flags\> are all the flags applicable to the javac compiler plus the optional flag -package \<packageName\>. If this flag is given, the package name of the specialized object reference class will be \<packageName\>, otherwise the package will correspond to the remote object classs package. This option is useful if you need to remote-enable a library class. For instance to remote-enable the Vector class of the java.util package you should not create a specialized object reference in java.util, rather, you may call:
> java cselt.jrb.Stub -package examples.hello java.util.Vector
this creates a VectorRef class in package examples.hello.
JRB through Firewalls
Many intranets have firewalls that do not allow opening direct sockets to hosts on the Internet. Therefore JRB uses http as the default transport layer to enable a client behind a firewall to invoke a method on a remote object which resides outside the firewall. The JRB transport layer embeds serialized objects in the body of an http POST request and response. The JRB runtime on the target machine listens with a server socket that is capable of understanding and decoding JRB invocations inside http POST requests.
If the target object resides on a machine that is not visible by the client, JRB provides a proxy servlet to forward an invocation to the target JRB object. Servlets are the dynamic Java extensions to web servers as applets are to web browsers. Suppose that JRB object, say ObjectServer, listens on machine loom.cselt.it on port 9999 which is inside an intranet and is not visible by a remote client in the Internet. However, that intranet is connected with the Internet through a web server on machine andromeda.cselt.it. If the web server supports Java servlets, the client can indeed invoke ObjectServer by construing an object reference of the kind:
ObjectRef ref = new ObjectRef("http://andromeda.cselt.it/servlet/
cselt.jrb.ProxyServlet?http://loom.cselt.it:9999/ObjectServer");
This URL is delivered to the web server listening on andromeda.cselt.it causing the dynamic loading and invocation of JRB's ProxyServlet servlet. This servlet uses the query argument to determine the target object and deliver the invocation to ObjectServer on loom.cselt.it. Objviously this approach only works if the web server supports the servlet extension. A similar approach, though more time consuming, can be realized using CGI scripts.
JRB Event Service
The standard JRB architecture supports the synchronous, point-to-point, connection-oriented communication model. In this model the client is explicitly aware of which target object it is invoking. The JRB Event Service supports an asynchronous, multicast, connection-less communication model where a sender object, the talker, sends a message and is unaware of the identity and number of recipients, the listeners, of the message. Consequently any messages is uni-directional, from one or more talkers to one or more listeners. In general, M talkers can issue messages via the same message stream to N listeners, without any of the talkers and listeners having explicit knowledge of each other. An advantage of this approach is that new talkers and listeners can be added easily.
Topic names
In Java Beans event model, listener objects interested in certain events must know the identities of the source objects of those events and register directly with them, which, in turn, have to maintain a list of the interested listeners to forward the events. The JRB Event Service follows the standard Java Beans event model but now listeners do not have to know the identity of the talkers and talkers do not have to maintain a list of listeners to forward the events. Events are identified by topic names, and listeners specify events of interest by informing the Event Service of topics interest to them. Topic names are organized into a hierarchical tree structure, like a file system. When a talker sends an event, the topic name of the event identifies a branch of a tree, e.g. "/cselt/teleconf/10:00am". All listeners that have registered for topic names along this branch will be notified of the event. In the example listeners that have registered for: "", "/cselt", "/cselt/teleconf", "/cselt/teleconf/10:00am" are all notified. The further down the tree hierarchy, the more specific the event registration becomes.
Talker class
The Talker class provides support for sending either singlecast or multicast events depending on the IP address specified in the constructor. If the address denotes an IP mulitcast group (Class D Internet address in range 224.0.0.0 to 239.255.255.255), all listeners that have joined that multicast group will receive the event, possibly on several different Virtual Machines. If, on the other hand, the address identifies a certain host-name/port-number, the talker will send the event only to the listeners in the Virtual Machine identified by that address. The cselt.jrb.Talker class is used to:
- create a talker. The default constructor creates a multicast talker for the multicast address 225.0.0.0 and port 4444. However, both the address and port number can be specified in the constructor of the Talker class. To create a talker for a specific Virtual Machine simply specify host-name and port-number in the constructor, e.g.
- Talker talker = new Talker("loom.cselt.it:6666");
- getAddress, and getPort methods return respectively the IP address and port number to which this talker refers.
- send an event with a certain topic name. The send method sends an event for a certain topic topicName to all listeners interested in this topic. The sender can pass an object as an argument of the send and this object will be delivered to all interested listeners. Depending on whether the talker uses a multicast address or a singlecast address the event will be dispatched to all Virtual Machines or a given Virtual Machine.
package cselt.jrb;
import java.io.*;
public class Talker implements Serializable {
public Talker() {...}
public Talker(String group) {...}
public Talker(String group, int port)
{...}
public String getAddress() {...}
public int getPort() {...}
public String toString() {...}
public void send(String topicName,
Object arg)
throws Exception {...}
}
Due to browsers' security policies, applets are not usually allowed to use IP multicast addresses. However, an applet is allowed to listen to events sent by the host whence the applet was downloaded.
Listener interface
Listeners must implement the cselt.jrb.Listener interface. This interface provides a single operation, receive, which is invoked to notify the listener of the occurrence of the event.
package cselt.jrb;
import java.io.*;
public interface Listener extend java.util.EventListener {
public void receive(String topicName,
Object arg);
}
ProxyListener class
The cselt.jrb.ProxyListener class provides only static methods and is used to:
- add a listener. The addListener method is called with the topic name of interest and the identity of the listener, which must support the Listener interface.
- remove a listener. The removeListener method is called to remove a listener for a certain topic name.
- set and get the port number and IP address the ProxyListener will be listening to. By default the ProxyListener will receive only events directly sent to it. With setMulticastAddress the programmer can specify a multicast address in the range 224.0.0.0 to 239.255.255.255 that the ProxyListener will join. Once the first listener has been registered an exception is thrown on any attempts to change the port number or IP address.
- the static variable packetSize specifies the maximum message's length. Since the Event Service uses UDP, which is neither reliable nor order preserving, the programmer must take into account the possibility of fragmentation and packet loss if messages are too large.
- the static variable multiThread specify whether the ProxyListener should fork a new thread for each incoming event or if all events are to be handled by the same thread. Forking new threads is more expensive but guarantees that all events are caught. If multiThread is set to false and an event arrives while another event is being served, the incoming event will be lost.
package cselt.jrb;
import java.io.*;
public class ProxyListener {
public static int packetSize = 4096;
public static boolean multiThread =
true;
public static void
setMulticastAddress() {...}
public static void
setMulticastAddress(String address)
throws Exception {...}
public String getAddress();
public static void setPort(int port)
throws Exception {...}
public static int getPort() {...}
public static void addListener(String
topicName, Listener l) {...}
public static void
removeListener(String topicName, Listener l) {...}
}
JRB and RMI comparison
In the following we provide a brief comparison between Java RMI and JRB.
Remote-enabling a class
With RMI, the following steps are required to enable an existing class for remote method invocation:
- Create an interface that extends Remote and redeclare every public non-static method.
- Add RemoteException to every method signature.
- Modify the existing class to implement the interface and extend UnicastRemoteObject.
- Add throws RemoteException to every method implementation.
- Modify each method that takes or returns an implementation to use a remote interface instead.
Note that instances of classes defined as Remote cannot be passed by value. Therefore, if you define a RemoteVector class and pass it as an argument, it will always pass a remote reference to the vector rather than a copy of the vector. This is not always the desired effect.
With JRB, to enable an existing class for remote method invocation, simply export an instance of the class with the export method of the Server class. You do not have to modify the original class in any way, not even access to its source code. This means that you can effortlessly enable any third-party library for remote method invocation. For example, to remote enable an instance of the java.util.Vector class simply export it:
...
Vector v = new Vector(); Server.export(v);
Exporting a Named Remote Object
Both RMI and JRB provides a Naming Server, called registry in RMI, for binding an object to a name, then retrieving the object via its name. To create an object in a server and then export the object for use by remote clients, the following steps are required:
- with RMI start an rmiregistry process, with JRB start the Naming Server: java cselt.jrb.Naming in the server.
- create the object in the server.
- bind the object to a name.
In addition, JRB includes an integrated URL-based naming scheme that allows you to create an object reference from a URL name without the need for a separate Naming Server. For example, you can export an instance of java.util.Vector with name "MyVector" on a server listening on port 9999 on host loom.cselt.it:
Server.setPort(9999);
Server.export("MyVector", new Vector());
then clients can bind the remote object
by creating an object reference:
VectorRef vector = new VectorRef("loom.cselt.it:9999/MyVector");
Garbage Collection
RMI has a lease-based garbage collection system. When a client obtains a reference to a remote object, the client is granted a lease that must be periodically renewed in order to prevent the remote object from being garbage collected. A remote object is garbage collected when all of its leases have expired. This garbage collection mechanism may severely affect the performance of the application due to the considerable overhead on the wire. Instead of relying on a distributed garbage collection mechanism, in JRB servers maintain control on the exported objects. Objects are not garbage collected till they are explicitly retracted.
Product Size
RMI's total class file size is 175KBs uncompressed. This includes all primary RMI .class files, the registry, and the distributed garbage collection system but excludes the http firewall support. JRB's total class file size is less than 20KBs uncompressed, an order of magnitude less than RMI!
Stubs and Skeletons
RMI uses the rmic utility to generate stubs and skeletons for remote method invocation. If using the dynamic invocation model, JRB doesnt need stubs and skeleton. If the programmer prefers to use a static invocation model JRB provides a stub generator. This section shows the code that each system generates for the twoInts method of the following interface:
public interface MyInterface extends Remote {
public int twoInts(int a, int b) throws RemoteException;
}
As the code examples below demonstrate, JRB typically emits 90% less support code than RMI. The stub code for twoInts with RMI:
public int twoInts(int $_int_1,
int $_int_2) throws java.rmi.RemoteException {
int opnum = 1;
java.rmi.server.RemoteRef sub = ref;
java.rmi.server.RemoteCall call =
sub.newCall((java.rmi.server.RemoteObject)this, operations,
opnum, interfaceHash);
try {
java.io.ObjectOutput out = call.getOutputStream();
out.writeInt($_int_1);
out.writeInt($_int_2);
} catch (java.io.IOException ex) {
throw new java.rmi.MarshalException("Error marshaling arguments", ex);
};
try {
sub.invoke(call);
} catch (java.rmi.RemoteException ex) {
throw ex;
} catch (java.lang.Exception ex) {
throw new java.rmi.UnexpectedException("Unexpected exception", ex);
};
int $result;
try {
java.io.ObjectInput in = call.getInputStream();
$result = in.readInt();
} catch (java.io.IOException ex) {
throw new java.rmi.UnmarshalException("Error unmarshaling return",
ex);
} catch (Exception ex) {
throw new java.rmi.UnexpectedException("Unexpected exception", ex);
} finally {
sub.done(call);
}
return $result;
}
The skeleton code for twoInts with RMI:
case 0: { // twoInts
int $_int_1;
int $_int_2;
try {
java.io.ObjectInput in = call.getInputStream();
$_int_1 = in.readInt();
$_int_2 = in.readInt();
} catch (java.io.IOException ex) {
throw new java.rmi.UnmarshalException("Error unmarshaling arguments",
ex);
} finally {
call.releaseInputStream();
};
int $result = server.twoInts($_int_1,
$_int_2);
try {
java.io.ObjectOutput out = call.getResultStream(true);
out.writeInt($result);
} catch (java.io.IOException ex) {
throw new java.rmi.MarshalException("Error marshaling return", ex);
};
break;
}
The stub code for twoInts with JRB:
public int twoInts(int p1, int
p2) throws Exception {
Object o[] = {new Integer(p1), new
Integer(p2)};
return
((Integer)invoke("twoInts", o)).intValue();
}
There is no skeleton code because JRB does not require skeleton code on the server side.
Ennio Grasso appartiene alla linea di Tecnologie Softeware di CSELT. Occupandosi di object-oriented distributed computing da quando è entrato in CSELT nel 1992, ha maturato una certa esperienza sia progettuale che implementativa. È rappresentante CSELT in OMG e segue lo standard CORBA sin dalle prime fasi. Autore del modello transazionale dell'architettura TINA, ha studiato l'applicazione di concetti transazionali rilassati per sistemi distribuiti dove il concetto di "transazionalità" deve essere adattato al dominio applicativo. Ultimamente la sua area di competenza si è concentrata sugli aspetti architetturali di Java, in particolare per gli aspetti di distrubuted computing, migrazione di codice e mobile agents.
