Statement of purpose.
The purpose of this tutorial is to demonstrate how RMI may be used for
basic distributed computing using the Java programming
language. Through this tutorial, following issues will be
demonstrated:
Disclaimer.
The API presented by this tutorial is just a narrow selection of primitives from
(java.rmi.*
). The examples are developed for educational purpose thus
aim for clarity rather than exploiting "nifty details" of the language or achieving
outstanding performance of the applications.
A basic understanding of the Java programming language is assumed, thus basic
concepts of the language (object oriented programming, exception handling etc.) is
not described into details.
Some of the programs included as examples or exercises in this tutorial accept
incoming connections thus they are "servers". Running such servers on a permanent
basis may compromise security of the departmental computer network. Furthermore,
running servers and thereby offering "network services" without prior written
permission from the system administration is violation of "responsible usage of the
computer systems at cs.aau.dk". Temporary experimenting with e.g. the examples and
exercises from this tutorial does however not require special permission.
Introduction to Java RMI.
The Java RMI (Remote Method Invocation) is a package for writing and executing
distributed Java programs. The Java RMI provides a framework for developing and
running servers (server objects). The services (methods) provided by those server
objects can be accessed by clients in a way similar to method invocation. I.e. Java
RMI hides almost all aspects of the distribution and provides a uniform way by which
objects (distributed or not) may be accessed.
Writing Java programs using RMI can be described by the following steps:
Executing distributed Java programs using RMI can be described by the following
steps:
The remainder of this tutorial will detail and exemplify those steps for writing and
executing RMI programs.
The example.
The example, which will be used throughout this tutorial, will be rather simple. A
server (RMIServer.java) will provide the methods String getString()
and
void setString(String s)
. A client (RMIClient.java) may use those two
methods for retrieving and storing a string in the server, i.e. the client may
modify and inspect the local state of the server object.
The following sections will develop this server and a corresponding client.
The server interface.
The server interface is used by the stub/skeleton compiler when generating the client
stub and the server skeleton files. This interface thus defines the methods in the
server, which may be invoked by the clients.
There are two issues to remember when writing such an interface. First, the
interface has to be written as extending the java.rmi.Remote
interface. Second, all methods in the interface must throw
java.rmt.RemoteException
. This exception must thus be caught when the
clients are invoking any of the servers methods, thus the clients may have a way to
determine if a method invocation was successful.
The complete source code for the server interface (ServerInterface.java) is included
below.
package examples.rmi;
import java.rmi.*;
public interface ServerInterface extends Remote
{
public String getString() throws RemoteException;
public void setString(String s) throws RemoteException;
}
The interface is compiled by the javac compiler to generate the file
ServerInterface.class
. The source code for the interface may be
downloaded here.
Writing the server object.
The server must be written as a "regular" Java program, i.e. a program with a method
public static void main(String argv[])
. Through this main method,
server objects may be instantiated and registered with the rmi registry.
Each distributed object is identified by a string, specifying the object name. This
string is registered with the rmi registry and is used by the clients when
requesting a reference to the server object. The following lines of code indicates
how an instance of RMIServer
can be registered with the rmi registry
under the string name "RMIServer". The following code would typically appear in the
main method of the server:
String name = "RMIServer";
RMIServer theServer = new RMIServer();
Naming.rebind(name,theServer);
Server objects must - of course - implement the defined interfaces and in addition
typically extend java.rmi.server.UnicastRemoteObject
. At the abstract
level, this should potentially enable various kinds of distribution schemes
(e.g. replication of objects, multicast groups of objects etc.) by extending
different classes. However in current implementations of Java, only
java.rmi.server.UnicastRemoteObject
is available. The only practical
advantage of inheriting from java.rmi.sever.UnicastRemoteObject
is that
it will preserve the usual semantics of the methods hashCode()
,
equals()
and toString()
in a distributed environment.
The source code for the RMIServer.java is included below and may furthermore be
downloaded from here.
package examples.rmi;
import java.rmi.server.UnicastRemoteObject;
import java.rmi.*;
public class RMIServer extends UnicastRemoteObject implements ServerInterface
{
private String myString = "";
// The default constructor
public RMIServer() throws RemoteException
{
super();
}
// Implement the methods from the ServerInterface
public void setString(String s) throws RemoteException
{
this.myString = s;
}
public String getString() throws RemoteException
{
return myString;
}
// The main method: instantiate and register an instance of the
// RMIServer with the rmi registry.
public static void main(String argv[])
{
try
{
String name = "RMIServer";
System.out.println("Registering as: \""+name+"\"");
RMIServer theServer = new RMIServer();
Naming.rebind(name,theServer);
System.out.println(name+" ready...");
}
catch(Exception e)
{
System.out.println("Exception while registering: "+e);
}
}
}
The RMIServer.java is compiled using the default javac to generate the file
RMIServer.class.
Generating skeleton and stub.
Based on the compiled ServerInterface and RMIServer files, a client stub and a
server skeleton can be generated. The server skeleton acts as interface between the
rmi registry and the server objects residing on a host. Likewise, the client stub of
the server is returned to the client when a reference to the remote object is
requested. The exact details (using the skeleton and stub) are all taken care of by
the runtime environment.
To generate the skeleton and the stub, the rmic compiler is used. Like the Java
virtual machine, the rmic compiler requires fully qualified class names, i.e. to
generate the server skeleton and the client stub for the RMIServer, the following
command must be invoked:
rmic examples.rmi.RMIServer
This generates the file RMIServer_Skel.class
and
RMIServer_Stub.class
.
UPDATE: rmic
only generates RMIServer_Stub.class
UPDATE: as of java 1.6 no stub need to be generated - all done
through reflection now. Normally no need for rmic unless you need to
generate stubs for old version or IIOP. But be sure that jre and jdk are both java 1.6
or higher/compatible.
Check java -version
and javac
-version
Writing the client implementation.
When writing a client implementation, three things must be done. First, a "security
manager" must be installed. Such a security manager specifies the security policy,
i.e. decides which constraints are imposed on the server stubs. An appropriate
security manager for these examples can be installed by the following code:
System.setSecurityManager(new java.rmi.RMISecurityManager());
(Java security is a world of its own and definitely worth investigating when writing
distributed applications. [6] is dedicated to describing security aspects of the
Java programming language and is highly recommended).
Second, a reference to the remote object must be requested. To do so, the hostname
of the host where the object resides as well as the string name, under which the
object is registered, is required.
In this example, the object was registered with the string name "RMIServer".
Assuming that the server was started on the host "objecthost.domain.com", the
following line of code may be used to get a reference to the object:
String name = "rmi://objecthost.domain.com/RMIServer"
server = (ServerInterface)Naming.lookup(name);
The code above contacts the rmi registry at "objecthost.domain.com" and asks for the
stub for the object, registered under the name "RMIServer".
Thus in reality, Naming.lookup()
returns an instance of the
RMIServer_stub
. However the available methods in the server object (and
thus in the stub) are defined by ServerInterface
. Thus whatever
Naming.lookup()
returns is typecast into a
ServerInterface
.
The complete code for the RMIClient is included below and may furthermore be
downloaded here:
package examples.rmi;
import java.rmi.server.*;
import java.rmi.*;
public class RMIClient
{
public static void main (String argv[])
{
// Parse the commandline to get the hostname where
// the server object resides
String host = "";
if (argv.length == 1)
{
host = argv[0];
}
else
{
System.out.println("Usage: RMIClient server");
System.exit(1);
}
// Install a security manager.
System.setSecurityManager(new RMISecurityManager());
// Request a reference to the server object
String name = "rmi://"+host+"/RMIServer";
System.out.println("Looking up: "+name);
ServerInterface server = null;
try
{
// In reality, Naming.lookup() will return an instance of
// examples.rmi.RMIServer_stub.
// This is typecast into the ServerInterface, which is what
// specifies the available server methods.
server = (ServerInterface)Naming.lookup(name);
}
catch(Exception e)
{
System.out.println("Exception " +e);
System.exit(1);
}
// Given a reference to the server object, it is now
// possible to invoke methods as usual:
try
{
server.setString("Foobar");
System.out.println("String in server: "
+server.getString());
}
catch(Exception e)
{
System.out.println("Exception " +e);
System.exit(1);
}
}
}
Running the example.
The first thing to do when running Java programs using RMI is to start the rmi
registry:
install:~> rmiregistry &
[1] 1449
install:~>
Next, the server must be started:
install:~> java examples.rmi.RMIServer
Registering as: "RMIServer"
RMIServer ready...
Finally, the client may be started and the setup tested:
install:~> java examples.rmi.RMIClient install.cs.aau.dk
Looking up: rmi://install.cs.aau.dk/RMIServer
String in server: Foobar
install:~>
(where install is the name of the machine you are using)
In case you are getting security errors, the following small hack should be
able to deal with it. Put the following peice of text in a file called Grant.java and place it in the working directory:
grant
{
permission java.security.AllPermission;
};
And use it in the following way:
java -Djava.security.policy=Grant.java
examples.rmi.RMIClient install.cs.aau.dk
Exercise 1.
Develop a simple, RMI-based server (CatServer), which will allow a client to read a
text-file line by line. I.e. the server provides the following methods:
Develop a client (CatClient), which can connect to the server. The client works like
a "remote cat", i.e. it requests a file to be opened by the server and reads the file
line-by-line (through nextLine()
). The client sends the lines
one-by-one to std. out while reading from the server.
The following code may be useful when opening a file for reading:
try
{
File myFile = new File(file);
if (myFile.canRead() && myFile.isFile())
{
BufferedReader mrf = new BufferedReader(new FileReader
(myFile));
}
}
catch (Exception e)
{
// In case of an exception, handle it...
// and the server continiues
}
Reading line-by-line from a BufferedReader:
String s = mrf.readLine();
Bibliography / further reading