NinjaRMI: A Free Java RMI
Introduction and Tutorial.

Matt Welsh
UC Berkeley Ninja Project

NinjaRMI v1.2 - Last modified 1 September 1999

Introduction.

NinjaRMI is a free, ground-up implementation of Java Remote Method Invocation, which allows Java code to invoke methods on objects running on remote machines using a network connection. NinjaRMI is an independent implementation of RMI which was developed for the UC Berkeley Ninja project.

Important note: While porting applications which use Sun's Java RMI to NinjaRMI is simple, NinjaRMI does not directly conform to Sun's Java RMI specification. I have made every effort to maintain "source-level" compatibility with Sun RMI applications, but NinjaRMI is not Java RMI per se. Additionally, NinjaRMI is not meant to be a rival RMI standard in any way. It was built to support the needs of the UC Berkeley Ninja project, which needed a flexible RMI system with several features not found in Sun's RMI. In some sense, "NinjaRMI" could stand for "NinjaRMI is not Java RMI!" I apologize for any confusion this may have caused.

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:

In addition, NinjaRMI performs as well as or better than Sun's JavaRMI over TCP connections, and since all of the source code is included with NinjaRMI, you are free to add additional features yourself.

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. 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:

Considering this, you will probably need to compile NinjaRMI on a UNIX system. NinjaRMI is written entirely in Java, so applications using NinjaRMI should run on any JDK1.1 system, including Windows.

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.

Changes since last release.

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.

Known bugs.

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.

Copyright.

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
All rights reserved.

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.

Compatibility.

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:

  1. In Sun's RMI implementation, remote objects must extend UnicastRemoteObject. With NinjaRMI, remote objects must extend NinjaRemoteObject instead. This is a one-line change to your code.
  2. NinjaRMI requires the use of the ninjarmic stub compiler, rather than Sun's rmic stub compiler.
  3. NinjaRMI comes with its own RMI registry, which should be used instead of Sun's. Simply run
        java ninja.rmi.NinjaRegistryImpl
    
    to 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.

Originally I planned to make a version of NinjaRMI for the GNU Classpath project which would be source, API, and wire-protocol compatible with Sun's version. Since I am now (1 September 1999) busy with other projects, it's doubtful I will have time to do this. However, it's not extremely hard to do with the NinjaRMI code base; please see this message for information on what's needed.

Compilation.

To compile NinjaRMI, please do the following:

  1. Place ninja/rmi/util on your PATH. This makes the ninjarmic program, used to compile NinjaRMI stubs, available to you. (Under UNIX, ninjarmic is a simple shell script; under Win32 systems, it's a batch file - ninjarmic.bat.)

  2. Be sure that the directory containing ninja is on your Java CLASSPATH. The code in NinjaRMI is part of the package ninja.rmi.

  3. Type: cd ninja/codegen

  4. Type: make

  5. Type: cd ../ninja/rmi

  6. Type: make

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.)

Running the examples.

In the ninja/rmi/example directory you will find an example client/server application. To run it, do the following:

  1. Edit the RUN-SERVER, RUN-CLIENT, and RUN-REGISTRY shell scripts and modify the CLASSPATH setting to point to the location of the ninja directory.

  2. Edit RmiServer.java and change the hostname "localhost" to the hostname of the machine where the registry will run (identical to the machine that the server will run, since they must run on the same machine).

  3. Edit RmiClient.java and change the hostname "localhost" to the hostname where the registry will run. (The client need not run on the same machine as the server and registry.)

  4. Type: make

  5. Type: ./RUN-REGISTRY & to run the registry.

  6. Type: ./RUN-SERVER to run the server.

  7. Type: ./RUN-CLIENT to run the client.

    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.)

Standalone client example.

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.)

  1. Place TheServiceImpl__RMIStub.class and TheService.class on an HTTP server. The former is needed so that clients (and the registry) can get the client-side stub for the object; the latter needed so that the registry can get the service interface (which is extended by the stub). It's assumed that the client has a copy of TheService.class already (as otherwise it would have been difficult for someone to write a client using TheService interface in the first place!)

  2. Start the registry in a directory OTHER THAN THE SERVER CODE DIRECTORY. This is because of a bug in Sun's classloader code which means that direct filesystem access to classes overrides the use of HTTP servers. The net result is that if the registry can get to the server code directly (via the filesystem) then it won't pass the server "codebase" URL to the client. You can still use the RUN-REGISTRY script as above to start the registry.

    Note that this bug appears using Sun's Java RMI implementation as well.

  3. When running the server, use the command
       java -Djava.rmi.server.codebase=http://URL-TO-STUBCLASS-DIRECTORY/ RmiServer
    
    Note 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
    

  4. When running the client, use the command
       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 NinjaRMI applications.

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:

  1. Write the Java interface for the remove object.

    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:

    The idea here is that the interface extends java.rmi.Remote so it is "tagged" as a remote interface; the java.rmi.Remote interface doesn't contain any methods. Each method must throw java.rmi.RemoteException since this is the exception type actually thrown by the RMI server code in case the remote object method itself throws an exception; this means the client (which uses this interface) must be prepared to catch the exception. And, all arguments and return values are serializable so they can be sent over the wire.

    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.

  2. Write the implementation of the remote object.

    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.

  3. Write the server class.

    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.)

  4. Implement the client code.

    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();
          }
        }
      }
    

  5. Compile the code.

    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 TheServiceImpl
    
    ninjarmic 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.

  6. Run the application.

    To test our you code, you follow the same procedure as in the NinjaRMI test cases above:

    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.

NinjaRMI special features.

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:

  1. Within a method being invoked remotely, cast the current thread (Thread.getCurrentThread()) to ninja.rmi.Reliable_ServerThread. This is the thread type used by TCP connections; see below for information on other transport types.
  2. Call the methods getClientHost, getClientPort, or getServerPort on the thread to obtain the information on the peer (or, in the case of getServerPort, the server itself) socket.

Client-side peer information: The client can also find out the information about the server socket from the remote object reference. Here's how:

  1. When the client obtains a reference to a remote object (a "stub"), say from the registry, that stub can be cast to ninja.rmi.NinjaRemoteStub. For example,
      service = (TheService)reg.lookup("service"); // Obtain stub from registry
      stub = (ninja.rmi.NinjaRemoteStub)service;   // Cast it
    
  2. Call the method getNinjaRemoteRef on the stub:
      ninja.rmi.NinjaRemoteRef ref = stub.getNinjaRemoteRef();
    
  3. Now the methods get_remotehost, get_remoteport, get_objid, and get_commtype can be invoked on the NinjaRemoteRef, giving the client information on the server connection.

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:

  1. Create an object which implements the ninja.rmi.NinjaServerCallbacks interface. This interface contains the methods socket_created and socket_destroyed.
  2. In the constructor of the remote object (that is, the object which extends NinjaRemoteObject), do the following:
      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.

Implementation 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.

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.

Further information.

Happy hacking!


M. Welsh
UC Berkeley Ninja Project