Remote Method Invocation (RMI)

COMP 310    Java Resources  Eclipse Resources

Remote Method Invocation is Java's "objectization" of the usual Remote Procedure Call capablilites of most modern server-capable operating systems such as Unix/Linux or Windows Server. RMI enables a client computer to access an object located in a remote server as if it were a locally instantiated object in the client.

Quick links:

 

RMI Overview

Features:

 Main Components:

  1. A "client" computer and a "server" computer: The remote object resides on the server and is accessed by the client. Note that the roles of client and server are not permanent and may change/reverse depending on which computer accesses a remote object stored on another computer. It is possible for a single computer to be both a client and a server.
  2. An interface that implements java.rmi.Remote: This is the view of the remote object by the client. To the client, the remote object on the server is a local object with the specified interface. All clients accessing the remote object must have a local copy of the same interface class unless the new dynamic loading schemes in Java 2 are utilized.
  3. An instantiation of a class that implements the same interface given in #2 above: This "RMI Server" object is the remotely accessible object that resides on the server.
  4. A "stub" object (automatically) created from the above remote object's class file: This "RMI Stub" object is instantiated by the same application that holds the above RMI Server object. The RMI Stub object is transmitted to the client whenever a reference to the remotely accessible object (RMI Server object) is established. This is the actual object that the client interacts with. The stub object simply delegates all methods call across the network to its corresponding skeleton object on the server.
  5. A "skeleton" object (automatically) created from the above remote object class file: This object resides on the server and delegates method requests received across the network from the stub object to the actual remote object.
  6. To make a stub, following the above recommendations:
  7. The RMI Registery: This is a program on the server called rmiregistry that must be running before any RMI connection is established. This program associates an instatiation of a remote object with a name that can accessed with a URL-type call. A name is "bound" into the RMI registry by using Java's built-in method.  There are many ways to do this, but if we already have a stub as from above, we can easily bind the stub to a name in the registry as such:
  8.  Finding the and downloading the stub from a remote server, as a RMI client would do, is easy enough:
  9. Dynamic Class Loadingsee below.

Stubs and Skeletons

Remote clients communicate with server objects via stubs and skeletons. These stubs and skeletons are automatically produced by the Java compiler on class files that extend UnicastRemoteObject and have an interface that extends Remote.   The stubs and skeleton are implementations of the Proxy Design Pattern, which is related to the Decorator Design Pattern.

RMI structure

 

Factories and RMI

Factories are one of the most useful techniques to use with RMI. Usually the server only binds out one object, a factory object, which the client uses to instantiate all the other objects it actually needs. This makes connecting much simpler, more flexible and more robust.

This technique is important enough that Sun even has a whole web page devoted to it: https://docs.oracle.com/javase/8/docs/technotes/guides/rmi/Factory.html

Remote Dynamic Class Loading

When an object is serialized, Java inserts both the class name and a URL value,  java.rmi.server.codebase, into the byte stream.   The Comp310 RMIUtils.startRMI(portNumber) call will set the  java.rmi.server.codebase value to refer to the class file server that it sets up running on your local (sending) machine on the portNumber port that can retrieve class files from your project's bin folder.

When an object is deserialized, the Java class loader looks at the imbedded class name and first checks whether or not the class is on the local (receiving) machine.  If the class is not found there, then the class loader uses the imbedded  java.rmi.server.codebase value to make an HTTP GET call (the same type of call a browser uses to retrieve a web page) to that URL to retrieve the class file.

The Java class loader is smart enough to realize that if an imbedded object uses other classes, then if those classes are not available locally, the class loader will then use the same java.rmi.server.codebase value to retrieve those classes as well from the remote location.

Sun (the originators of Java) has written a simple class file server that can be used for dynamic class loading. That code is provided here in a slightly modified version of Sun's class file server. It will work properly only if the java.rmi.server.codebase system property is as a URL that points to it. For example, http://foo.cs.rice.edu:2001. The server also needs to know the exact path to the default Java package directory on the server machine.

Note: If both the client and the server have classes with exactly the same name, errors (usually unmarshalling errors) can occur. It is highly recommended that the names of any serialized classes being sent across the RMI connection be "personalized" in some way--by appending one's initials to the classname for instance. This problem can be avoided by carefully setting up exactly what directories are visible to the RMIRegistry, class server and application when they are running, but that is beyond the scope of this web page. For this reason, Java has class naming conventions that, in a nutshell, are that any package create for Internet use should be their creator's URL backwards, e.g. edu.rice.cs.comp310.rmi

RMI Start-up Process

  1. Start the Java SecurityManager which controls how much of the system RMI can access.
  2. Configure Java system parameters:
  3. Start the class server for remote dynamic class loading before binding anything to the Registry
  4. Start the Registry on any machines where objects will be bound.
  5.  Make sure to start the RMI server end of  the system before the RMI client end tries to connect to it.

Resources

 

Tips and Traps

Do Not Serialize Anonymous Inner Classes

Since the serialization process is a deep-copy process, where object references are followed and any referenced objects are serialized alongside the host object, attempting to serialize an anonymous inner class will cause Java to attempt to serialize the enclosing object(s) that comprise the anonymous inner class's closure.    This is because all inner classes contain a hidden field that references the enclosing class(s) that form its closure.    However, the enclosing class may not be serializable or not desired.  For the most part, the enclosing class is either to big to serialize and transmit or simply not be serializable, thus throwing an error.

This is the #1 cause of serialization/marshalling/unmarshalling errors!

Work-arounds:

Comparing Deserialized Objects

Transient Fields in Classes

RMI executes void returns asynchronously:

UPDATE: As of 12/2015 (Java 8), there is evidence that void returns are not being executed asyncronously.  The Java RMI specification is a bit vague on this issue but there is documention that suggests that it must wait because non-declared run-time exceptions, e.g. IllegalArgumentException, must be passed back to the caller of a method on a stub (https://docs.oracle.com/javase/8/docs/platform/rmi/spec/rmi-stubs24.html).  

In light of this, it is highly recommended that in asychronous message-passing architectures, that all method calls to RMI stubs be handled by a dedicated thread that releases the caller from the waiting for the void return to complete.   For methods that are frequently used, a dedicated producer-consumer mechanism with a BlockingQueue is recommended.

RMI does not wait for the return of a remote void method call to return before proceeding on to the next statement. This can cause some strange behaviors if the code assumes a certain order of execution based on the assumption that the call to the void method waits for it to finish, as normally happens in a non-RMI, single-threaded situation. For instance, the following code can cause problems:

// in ObjectA, initially call method1() below. remoteObjectB is a RMI stub to a remote ObjectB instance.

void method1()
{
remoteObjectB.doIt();
System.out.println("the statement after the call to remoteObjectB");
}

void method2()
{
System.out.println("this might come out first or second!");
// This will mostly likely come out second due to network delays.
// In a non-RMI, single-threaded situation, this line would print first.
}

// The following is in class ObjectB which holds an RMI stub to the above ObjectA instance. 

void doIt()
{
remoteObjectA.method2();
}

Exporting a Stub Does NOT Prevent the Remote Object From Being Garbage-Collected

This issue may be the source of common "Object not in table" or "Connection Refused" errors.   Symptoms include inconsistent program behavior from execution to execution and operations that stop working after working initially when a program is run.

When one exports a Remote object to create a RMI stub, it is tempting to believe that the auto-generated skeleton object would maintain a reference to the original Remote object and thus prevent it from being garbage-collected but this is not so.

IMyRemoteClass stub = (IMyRemoteClass) UnicastRemoteObject.exportObject(myRemoteObject, aPort);

If for instance, 'myRemoteObject" above is an anonymous inner class defined right there, there is no reference to that object anywhere.   The generated RMI skeleton does not keep a reference either so at some point the anonymous inner class object will get garbage collected.   This means that an error will be generated the next time the stub is used because it has no more back-end object to actually perform the operations.

Solutions:

 

RMI Application May Run Better From Console Than From IDE

Since RMI relies on I/O ports, one's Integrated Development Environment ("IDE", e.g. Eclipse, DrJava, etc) may not properly manage the I/O resources as your RMI application starts and stops, resulting in erratic behavior such as intermittent connection failures.   Since any Java application is actually a process being run by the JVM, the host operating system is often not able to do the more complete "cleanup" that it does when a normal application terminates.    If the IDE is also using RMI, such as DrJava uses to communicate with its Interaction Pane, there is the possibility that your RMI  application may conflict with the RMI usage of the IDE.

Work-arounds:

 

Inadvertent Thread Blocking, Deadlocking or Other Problems

When remote calls to methods on RMI skeletons (UnicastRemoteObject) are called, the RMI system starts up a new thread to handle that call. It is important to realize this, as it may cause some strange and unexpected problems:

A pathological but not-that-uncommon scenario:

  1. From the calling machine, the user clicks a button whose code makes an remote RMI call to the their own machine (the caller is connected to itself) on a method that has a return value.

  2. The calling machine's GUI thread is now blocking, waiting for the return from the remote call.    The caller's GUI is currently non-operational because of this.

  3. The remote method needs user input, and so, attempts to use the GUI.   Properly, the remote method uses an SwingUtilities.invokeLater() call to place an event onto the GUI event queue.

  4. The remote method blocks deliberately because it is waiting for the user's input.      

  5. But since the remote method is actually running on the caller's machine, it is trying to use the blocked GUI from step 1 above.   The queued event from step 3, thus never executes.   Thus step 4 never unblocks.   Thus the remote method never returns.   Thus the GUI in step 2 never unblocks.

  6. The entire application has deadlocked and is frozen solid!  

Curious artifacts of the above problem:

A solution:  In step 1 above, spawn a new thread that will actually perform the remote call and

  1.  Allow the GUI thread to immediately return and not block.

  2. Asynchonously process the returned value.  Note that the spawned thread is the one that is blocking on the RMI call, not the GUI thread.

 

For more information, please see the Comp310 Java Resources page on Threads.

 

All Remote interfaces must be part of the shared API!

It seems that RMI does not handle dynamic class loading of Remote interfaces very well.   The problem arises when a client receives the stubs implementing the same Remote interface from multiple sources.  If the Remote interface is not part of the shared API, i.e. a common code base, then that interface must be dynamically class loaded when the stub is deserialized.    The problem is that when stubs implementing the same dynamically loaded Remote interface are received from multiple sources, RMI can't seem to tell that they are actually implementing the same interface because it sees the objects coming from different source computers.   RMI then tries to load the class file for that interface from each of the computers from which it receives stubs, uniquely identifying each class associated with the each of the downloaded class files. 

Then, under certain circumstances, RMI realizes that it cannot or rather, improperly identifies the class of a new incoming stub of that Remote interface.    The result is a ClassCastException as RMI tries to cast the stub to one of the classes it received from a different source.    This is a very confusing error because it appears to all aspects that RMI is unable to cast an object to its own class because everything about the multiply-downloaded classes all appear the same: fully-qualified name, methods, superclasses, implementing interfaces, i.e. everything except the hash codes of the associated Class objects (which is why RMI thinks they are different).  

Bottom line: The Remote interfaces used by the system MUST ALL be part of the shared, public API.

Thus, the use of implementation-specific stub interfaces will not work.

 

Address/port in use errors

The most common cause of this error is that an instance of the application is still running. Note that the Java RMI Registry and the class file server are technically separate processes and it is possible from them to keep running even if it looks like the main application has already shut down.

Always explicitly stop the RMI processes (IRMIUtils.stopRMI()) and notify any connections when the application exits.

To figure out if there there are still application processes running:

To see what processes are using a particular address/port:

If you know any of the commands or procedures for the missing items above, please let the course staff know right away so this page can be updated.

Unless you know exactly what the offending process is, it not recommended to outright kill it as this could cause data corruption problems. Try to exit the offending application gracefully or reboot one's computer if necessary.

Potentially problematic apps (please let the course staff know if you find any other apps so this list can be updated!):

 

 

© 2020 by Stephen Wong