NinjaRMI: A Free Java RMI
Introduction and Tutorial.
Matt Welsh, mdw@cs.berkeley.edu
UC Berkeley Ninja Project
NinjaRMI v1.2 - Last modified 23 November 1998
NinjaRMI is a free, ground-up reimplementation of Sun's Java Remote Method Invocation specification, which allows Java code to invoke methods on objects running on remote machines using a network connection. NinjaRMI is a completely free and independent implementation of RMI which was developed for the UC Berkeley Ninja project.
The main NinjaRMI web page is http://www.cs.berkeley.edu/~mdw/proj/ninja/, where future updates will be posted.
NinjaRMI v1.2 (Released 24 November, 1998) is now available for download.
NinjaRMI includes a number of enhancements over the standard Java RMI implementations released by Sun:
NinjaRMI was developed to give us maximum flexibility in features and performance as needed for this project. Since the code is stable and performs well, we thought it would be a good idea to release it to the Java community at large. NinjaRMI is maintained by Matt Welsh, mdw@cs.berkeley.edu. I will be coordinating future releases of this code. If you have bugfixes, new features, or other contributions to this package please e-mail me. All other feedback is also welcome!
NinjaRMI requires:
I do not support Microsoft platforms, so please do not ask me any questions regarding NinjaRMI usage under Windows systems. I know that NinjaRMI does work on Windows NT with Sun JDK 1.1.x. Instructions (where they differ from UNIX) are included below.
v1.2, 23 November 1998: It turns out that the RMISecurityManager and AppletSecurityManager hacks are no longer necessary in NinjaRMI. This means that you don't need to bother with creating special security managers in the NinjaRMI server, client, or registry code. These have been cleaned out of the examples.
v1.1, 29 September 1998: Added the ninja/rmi/compiler package, which includes an all-Java implementation of the NinjaRMIC RMI stub compiler (a replacement for Sun's rmic). This means that Perl is no longer needed - ninjarmic is simply a shell (or batch) script which executes java ninja.rmi.compiler.NinjaRMIC.
Also added the ninja/codegen package, which includes utilities used by NinjaRMIC.
Here at Berkeley we have support for authenticated/encrypted RMI working, however, due to uncertainty about export limitations it hasn't been included in the NinjaRMI release at this time. You'll notice references in the code to the authenticated RMI implementation, so we simply commented out the relevant hooks in the NinjaRMI release. We hope to be able to release this soon.
ninjarmic will cause the static initializer (if any) of the class being compiled to be invoked. This means if your service implementation contains a static { ... } code block, it will be executed when ninjarmic is run on it. This is because ninjarmic uses Class.forName() to introspect on the class, which (for some reason) invokes the class's static initializer. This is not a problem unless your static initializer causes an unwanted side-effect.
NinjaRMI is covered under a standard copyright from the University of California which allows free redistribution and derived work, as long as the copyright itself remains intact. Here is the fine print:
Copyright (c) 1998 by The Regents of the University of California
Permission to use, copy, modify, and distribute this software and its
documentation for any purpose, without fee, and without written agreement is
hereby granted, provided that the above copyright notice and the following
two paragraphs appear in all copies of this software.
IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR
DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT
OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE UNIVERSITY OF
CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATION TO
PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
All rights reserved.
NinjaRMI is mostly source-compatible with Sun's RMI implementation (at least in JDK1.1). What this means is that making a few small modifications to existing code which uses Sun's RMI will allow the code to use NinjaRMI instead. In particular:
java ninja.rmi.NinjaRegistryImplto start the NinjaRMI registry.
These are the only changes required to use NinjaRMI. If you want to use the special features added by NinjaRMI, however, you'll have to make a few additional changes (discussed below). Fortunately these are very simple.
NinjaRMI is not compatible with Sun's RMI at a "wire" protocol level. This means that NinjaRMI objects cannot communicate with Sun RMI objects and vice versa. The issues here are complex and varied, and basically it is not one of NinjaRMI's goals to be protocol-compatible with Sun RMI. I have opted for new features and functionality rather than backwards-compatibility with Sun.
Note that as of 23 November 1998 I intend to make a version of NinjaRMI for the GNU Classpath project which will be source, API, and wire-protocol compatible with Sun's version. Contact me if you're interested.
To compile NinjaRMI, please do the following:
This will automatically configure and compile the NinjaRMI code, including the examples, registry, and documentation. (You can access the NinjaRMI javadoc API documentation here after compiling.)
In the ninja/rmi/example directory you will find an example client/server application. To run it, do the following:
This should do 100 RMIs from the client to the server and print how long each RMI takes. (Since the server method is more than a "null RMI" note that the results are not necessarily commensurate with Ninja RMI performance. To get real performance numbers, edit TheServiceImpl.java and take out the extra work inside of the SomeFunction() method.)
Note that the above example expects that the client has direct filesystem access to the "__RMIStub.class" and "__RMISkel.class" files generated by compiling the example application. If this is not the case (for example, if you're trying to use RMI from a Web page applet remotely) then you need to make a couple of changes to the code. (These changes have already been made to the code in ninja/rmi/example/standalone-client. They're mentioned here so that you understand what's necessary.)
Note that this bug appears using Sun's Java RMI implementation as well.
java -Djava.rmi.server.codebase=http://URL-TO-STUBCLASS-DIRECTORY/ RmiServerNote that the URL (which names the directory that the stub class is in, not the stub class file itself) needs to have a trailing slash. For example, if I placed the stubclass in http://foobar.cs.berkeley.edu/mdw/, I would use the command
java -Djava.rmi.server.codebase=http://foobar.cs.berkeley.edu/mdw/ RmiServer
java RmiClient
An example of "client only" code is in ninja/rmi/example/standalone-client. The RUN-CLIENT script there contains the correct command for starting the client, and RUN-SERVER-CODEBASE contains the command to run the server specifying the codebase property.
Writing code which uses NinjaRMI is largely like writing code for Sun's Java RMI. You should check out Sun's RMI pages for details. (Note that NinjaRMI looks more like JDK1.1 RMI, and does not have some of the features in JDK1.2.)
The best way to get started is to read the code in the example directory. This consists of several files:
Here's a step-by-step on how to write a NinjaRMI application:
The Java interface class defines the methods exported by the object which you wish to make remotely accessible. This interface can contain any methods you like, as long as:
An example of a simple remote object interface is TheService.java in the examples directory. It looks like:
public interface TheService extends java.rmi.Remote { public void someFunction() throws java.rmi.RemoteException; }Obviously this is simplistic as there are no arguments or return values, but you get the idea.
Your remotely-accesible object can implement many interfaces, as well as multiple interfaces which extend java.rmi.Remote. The idea is that the client will obtain a handle to the remote object which implements one (or more) remote interfaces; by specifying the remote interface you are specifying which methods of the remote object can be called by the client.
This is a class which implements (naturally) the remote interface you wrote above. In addition, the remote object (which runs on the server) must extend ninja.rmi.NinjaRemoteObject, which is the base class for all remotely-accesible objects. NinjaRemoteObject's special magic is that its constructor arranges for the object to be "exported" for remote access.
Obviously the remote object implementation must implement the remote interface(s) you specified above. As such it's pretty straightforward to write, and example it is in example/TheServiceImpl.java. The code shown below is a simplified version of this code, which doesn't include any special embellishments:
import java.rmi.*; import java.io.*; import ninja.rmi.*; public class TheServiceImpl extends NinjaRemoteObject implements TheService { public TheServiceImpl() throws RemoteException { System.out.println("Constructor called\n"); } public void someFunction() throws RemoteException { System.out.println("TheServiceImpl being called!"); } }
The actual example code contains some special API calls which show off how to do nice things in NinjaRMI such as get the hostname of the client calling the remote object, and so forth. The above is just a minimal example.
Note that the above class is not synchronized which means that (potentially) several clients could be calling methods on TheServiceImpl at once. If you want to manage concurrent use by multiple clients you can use the usual Java synchronized feature. If you want to control access to your remote object (say, by only allowing particular client hosts to call methods on it), you can use special calls in the Ninja API, which are documented here.
The server class in NinjaRMI simply instantiates one or more remotely-accesible objects, which makes them available for client access. In addition it's responsible for registering the remotely-accessible objects with the Registry, which is a utility which gives client machines the ability to look up remotely-accessible objects on the server machine.
example/RmiServer.java contains a simple RMI server example:
import java.rmi.*; import java.rmi.server.*; import java.rmi.registry.*; import ninja.rmi.*; import ninja.rmi.registry.*; class RmiServer { public static void main(String args[]) { TheServiceImpl service; // The remotely-accessible object Registry reg; // Handle to the registry try { // First get a handle to the local Registry (must be on the same // machine!) String hostname = "bar.foo.com"; reg = (Registry)NinjaLocateRegistry.getRegistry(hostname, 1099); // Instantiate the remotely-accesible object service = new TheServiceImpl(); // Bind it in the Registry reg.rebind("servicename", service); } catch (Exception e) { System.out.println("RmiServer error: " +e.getMessage()); e.printStackTrace(); } } }
Note that the server class doesn't sleep or wait at the end of the main method. This is because the NinjaRMI code spawns a thread within TheServiceImpl to listen for incoming requests, which keeps the JVM running even after main returns. (You could capture the client socket open/close events using the NinjaServerCallbacks feature to make a decision to quit the JVM when the last client has disconnected.)
The RMI client code needs to contact the Registry to obtain a handle to the remote object and then invoke methods on the object. What's really going on here is that the Registry is giving the client a "stub" object which converts method calls on itself into network messages to the server, which cause the remote object to be invoked. Here's a simple client (a more advanced one is in example/RmiClient.java):
import java.rmi.*; import java.util.*; import java.io.*; import java.rmi.server.*; import java.rmi.registry.*; import ninja.rmi.*; import ninja.rmi.registry.*; class RmiClient { public static void main(String args[]) { TheService service; // Handle to the remote object Registry reg; // Handle to the Registry try { String hostname = "bar.foo.com"; // Host name of the server // Get a handle to the registry reg = (Registry)NinjaLocateRegistry.getRegistry(hostname, 1099); // Lookup the service service = (TheService)reg.lookup("servicename"); // Invoke a method on it service.someFunction(); } catch (Exception e) { System.out.println("RmiClient exception: " + e.getMessage()); e.printStackTrace(); } } }
To compile the application, simply invoke javac on each of the four Java source files (TheService.java, TheServiceImpl.java, RmiServer.java, and RmiClient.java) which you wrote above. Then, run ninjarmic on the remote object implementation class, as so:
ninjarmic TheServiceImplninjarmic generates two new classes: TheServiceImpl__RMISkel.class and TheServiceImpl__RMIStub.class, which implement the server-side "skeleton" and client-side "stub" for NinjaRMI. The skeleton and stub convert Java method calls into network messages and vice versa.
To test our you code, you follow the same procedure as in the NinjaRMI test cases above:
java ninja.rmi.registry.NinjaRegistryImpl
java RmiServer
java RmiClient
That's it! You now have a complete NinjaRMI application.
If you have an application previously written for Sun's Java RMI, you can simply replace the usage of UnicastRemoteObject with NinjaRemoteObject in the remote object implementation, and use ninjarmic instead of rmic to genrate the skeletons and stubs.
ninjarmic supports a number of command-line options which are compatible with Sun's rmic. Look at the code in compiler/NinjaRMIC.jc for details.
The example code demonstrates most of NinjaRMI's special features. We'll discuss these in turn. I know that this documentation is a bit sketchy, but the API documentation and reading the source helps a lot.
Server-side peer information: The server code (meaning, a method being invoked from a remote client) can determine the hostname and port number for the socket connection being used by the client. Among other things this can be used to disambiguate clients from one another (as NinjaRMI does not currently "multiplex" sockets between multiple clients, as Sun's Java RMI does). Here's how it works:
Client-side peer information: The client can also find out the information about the server socket from the remote object reference. Here's how:
service = (TheService)reg.lookup("service"); // Obtain stub from registry stub = (ninja.rmi.NinjaRemoteStub)service; // Cast it
ninja.rmi.NinjaRemoteRef ref = stub.getNinjaRemoteRef();
Server-side callbacks: The server can can register callbacks with the RMI code which are invoked when certain events occur. Currently, socket creation and destruction (for TCP connections) are implemented as callbacks, but others could be added. To use them, do this:
public TheServiceImpl() throws RemoteException { super(null); NinjaExportData data = new NinjaExportData(); data.callbacks = new MyCallbacks(); // Replace with your callbacks object this.exportObject(data); }
Multiple communication types: NinjaRMI allows communication semantics other than reliable TCP connections to be used. Currently implemented are "unreliable, one-way" (UDP) and "unreliable, one-way, multicast" (multicast UDP) connection types; other types are easy to add (read the code for details).
The NinjaExportData structure is created by the server code when instantiating a remote object, as seen above in the callbacks example. If no such structure is explicity created and filled, the superclass constructor (for NinjaRemoteObject) will create a default one which specifies that the object is to be exported on an anonymous TCP socket. If you create your own NinjaExportData and fill it in, you can control certain things, such as the communications type to use, the port number to listen on, and so forth. Read the API documentation on NinjaExportData for details.
I could go into much detail about the implementation of NinjaRMI here, but because you have the source code before you, it's almost easier just to read it. The best place to start reading the code is with NinjaRemoteObject.jc and NinjaExportData.jc. Also be sure to use ninjarmic -keepgenerated which will keep the generated .java files for the generated "RMIStub" and "RMISkel" classes; these are very instructive as to how the system works.
If you have specific implementation questions please send me mail at mdw@cs.berkeley.edu.
NinjaRMI's performance should be equivalent to or better than Sun's implementation, at least for TCP connections. UDP and multicast suffer a little bit as you can't re-use the same "socket" structures for each call; for now I don't think this is a serious problem. On a Pentium II-300MHz running Linux 2.0.34 with JDK 1.1.6 and the TYA 0.9 JIT, I measure NinjaRMI round-trip performance at network time plus 1 ms for a "null" RMI (void args, void return). Adding an int arg and int return adds 0.2 ms. This translates to about 1.9 ms round-trip over a local TCP socket.
Currently, the NinjaRMI server code can scale up to 248 open TCP sockets, which basically translates into 248 clients holding references to remote objects on the server. This is a kernel issue as a UNIX process can only have so many open sockets at a time. The obvious solution here is to close and recycle sockets using LRU; this hasn't been implemented yet.
When there are more than 150 open sockets on the server, performance starts to lag a bit. This seems to be due to either the Java threading model or the select() call being done by the JVM to simultaneously poll the open sockets.
I realize that there are a number of performance enhancements possible with NinjaRMI, and that Sun's RMI might have some advantages in terms of scalability. I plan to make some of these optimizations soon, however, the priority right now is to finish implementing the features needed for the project.
If you're really unhappy with NinjaRMI's performance, you have two options: (a) Don't use it, or (b) Contribute! All of the code is there, so you're empowered to add whatever features you need. Please send any contributions to me via e-mail. If there's a lot of interest in this code, we'll establish a more structured development effort based on it.
For further information on NinjaRMI and related projects, see http://www.cs.berkeley.edu/~mdw/proj/ninja.
Happy hacking!