Michael L Brereton - 06 March 2008, http://www.ewesoft.com/
>>
Next: Eve Native Interface
Objects Passed as Reference. 2
The IRemoteProxyMaker Interface
Testing RMI Interfaces Locally
Example Application – Simple Chat System
Important Design Considerations
Remote Method Invocation (RMI) is the execution of methods on Java Objects that may exist on a separate virtual machine and possibly on a different computer. RMI can be used to provide complex distributed and Client/Server applications without the need to specify and implement any custom communication protocols – instead of formatting custom messages containing data for specific commands, the application simply invokes methods on remote objects as if it were invoking methods on a local object (with some prudent design decisions). The advantage is considerable ease of programming at the expense of more communication and processing overhead (which is implemented for the programmer) associated with RMI.
It is important to make “remote-aware” decisions when choosing the methods and interfaces that will be accessed remotely. Although your application will look as if it is running locally the fact that it is running over a distributed system introduces situations and potential errors that would not otherwise arise. This will be discussed in a later section.
The essential points of RMI are summarized below:
1.
RMI is usually implemented via a local Proxy object that
implements the same methods you wish to invoke on the remote object. A Proxy
object in Java is an automatically created anonymous Object that implements a
specific set of interfaces.
2.
The Proxy object formats all method calls made on it, along
with parameters, into a byte-encoded message (this process is known as
“marshalling”) that is then sent over some kind of communication channel to a
server object on the remote process.
3.
The server object then decodes the formatted method call and
either invokes the method on another object on the remote process or executes
the method itself.
4.
Once the method execution is complete a reply, along with any
return value or possibly any thrown exception, is sent back to the Proxy object
over the same channel.
5. The Proxy object decodes the reply and returns normally or throws an exception if necessary.
Some important characteristics and restrictions arise from these points:
1.
Only method calls are possible over the channel – modifying
fields in the local proxy will not affect fields in the remote Object.
In fact, local proxy objects are pure interface implementations and have no
fields to modify.
2.
Only data value that can be encoded into bytes can be sent as
parameters and can be received as return values. The types of values that can
be used for RMI in Eve will be discussed below.
3.
Arguments and return values are passed as copies and not
as references – except for interfaces that implement eve.sys.RemoteInterface
and for parameters or return values that are exactly the classes InputStream
or OutputStream. This means that if the remote Object modifies an object
parameter this will have no effect on the original object on the local side.
4. The Object on the remote side need not be a Java Object at all – any entity that can decode the marshaled parameters, execute the functionality (or at least appear to) and return a properly formatted reply will have acted successfully as a remote Object.
Here are some details for the RMI API in Eve. These details are for the simplified RMI interface and do not include details of the actual implementation.
1.
RMI is implemented at the Interface level. That is, you
define an Interface that specifies methods that you are going to invoke over
the remote connection.
2.
Method calls over RMI can throw an eve.sys.RemoteMethod
exception (which inherits from RuntimeException) so you should be prepared to
catch these exceptions.
3.
Security is implemented by securing the RMI connection
channel. There are several ways to do this and this will be discussed later.
+
4. Eve RMI does not specify or care how the application connects to the remote entity – the RMI API assumes a connection has been made and either Input/Output Streams are available or Input/Output BlockStreams are available.
It is assumed that you have an application that will run as some sort of server and an application that acts as a client. The server normally waits for incoming connections (usually on a socket) and normally can service more than one client at a time. You should set up the applications such that the server always implements a simple interface upon first connection. The client would use that interface to then possibly request further services as represented by additional interfaces (which must be “marked” as implementing eve.sys.RemoteMethod). The system will automatically create proxy objects as needed for these RemoteMethod objects.
As mentioned before objects passed as parameters or return values are normally passed as copies, such that operations on the received copies do not affect the original versions of the objects. There are two exceptions to this:
1. Where the type of the parameter or return values is an Interface and that Interface extends eve.sys.RemoteInterface, then a special reference is sent to the remote process which is converted to a Proxy object. That Proxy object fully implements the original Interface by sending remote calls back to the original implementing object that was specified as the parameter or return value.
2. Where the type of the parameter or return value is exactly java.io.InputStream or java.io.OutputStream, in which case a special reference is sent to the remote process which is converted to an object that is an InputStream or OutputStream. Reading or writing data from these received objects results in data being read from or written to the original InputStream or OutputStream objects that were specified as the parameter or return value. This allows direct streaming connections between the two remote entities.
This interface is in package eve.sys and it is meant to represent an object that can setup an RMI connection across an already connected channel. There is a concrete implementation of this interface: eve.io.block.RemoteProxy but under most circumstances you should not use it directly. Instead you should call eve.sys.Vm.getRemoteProxyMaker(Class interface, int options, ClassLoader specialLoader). Calling this method returns an object of type IRemoteProxyMaker that can create the RMI connection with the specified interface as the initial interface supported by the server. After calling this method you can call setTimeout(long millis) to specify the timeout for method calls (this is optional) and then you call either setConnection(Object input, Object output) and makeProxy() on the client, or you call makeServerObject(Object serverObject, Object input, Object output) on the server.
Here are the steps you would perform on the server.
1. Wait
for an incoming connection – usually on a ServerSocket but can be on any
channel that provides separate Input and Output streams.
2. Create
a new Thread to service the client and let the original Thread continue to wait
for connections.
3. In
the new client service Thread, apply any necessary validation and security. For
simple unsecured systems you may assume that as long as an entity connects to
the correct port then it is a valid connection. If it is determined that the
access is not authorized, you should close the connection.
4. Create
a server Object that implements the main server interface and which will
service the client. This should be a well know interface that can be used
either as the full server interface (for simple systems) or as a “gateway” for
the client to request further server interfaces (in more complex systems).
5. Call
Vm.getRemoteProxyMaker() specifying the main server interface to get an IRemoteProxyMaker.
On the returned IRemoteProxyMaker call makeServerObject(serverObject,
inputChannel, outputChannel) to “activate” the server – it will then be
able to receive remote calls from the client. It is not able to send calls to
the client unless the client sends a RemoteInterface object to the server as a
parameter in a method call.
6. The method makeServerObject() also returns a Handle that can be used to monitor the connection. When the connection is closed, the Stopped bit in the Handle will be set, or you can call stop() on the Handle to explicitly close the connection.
Here are the steps you would perform on the client.
1. Connect
to the server – usually to a socket on a known host and port.
2. Perform
any necessary validation and security authorization. For simple unsecured
systems this step will be omitted – you assume that the connection is to the
correct server.
3. Call
Vm.getRemoteProxyMaker() specifying the main server interface to get anIRemoteProxyMaker.
On the returned IRemoteProxyMaker call setConnection(inputChannel,
outputChannel) to specify the communication channel.
4. Call
createProxy() to get a Proxy Object that you then cast to the server
interface.
5. From that point, method calls to the Proxy object will be invoked over RMI on the server. If you can pass RemoteInterface Objects as parameters to the server, the server will be able to invoke methods on the local copies of those objects over the same RMI connection. Similarly if the server returns to the client RemoteInterface Objects, the client will be able to invoke methods on those Objects over the RMI connection.
The interface IRemoteProxyMaker also provides a simple system for testing RMI calls. The method createTestConnection(Object serverObject) does the following:
1. MemoryStream
objects are created to provide Input and Output streams which are connected to each
other through memory.
2. The
makeServerObject() method is used to activate the serverObject provided
(which must implement the interface provided in Vm.getRemoteProxyMaker()).
3. A client is setup and a Proxy object returned which also implements the server interface. The returned object is therefore the client interface to the server.
When the method returns all you need do to test the system is to make calls on the returned client object. These calls are converted to RMI calls, and sent over the internal stream to the server just as it would over any other communication channel.
Here is an example of using RMI over a local test connection. It also demonstrates the possibility of a method call timing out, which would not happen when called locally.
package evesamples.remote;
import java.io.IOException;
import eve.sys.IRemoteProxyMaker;
import eve.sys.Vm;
public class LocalRemoteTest {
//
// A simple
test interface.
//
public interface TestInterface {
public String process(String message);
}
//
// An
implementation of TestInterface.
//
static class testServer implements TestInterface{
public String process(String message) {
try{
Thread.sleep(2000); //Sleep for 2 seconds.
}catch(InterruptedException e){}
return message.toUpperCase();
}
}
//
// Used to
test the interface.
//
static void testTheInterface(TestInterface toTest)
{
try{
System.out.println("Calling...");
String ret = toTest.process("A Test Message!");
System.out.println("Received: "+ret);
}catch(Throwable t){
System.out.println("Error:");
t.printStackTrace();
}
}
//
public static void main(String[] args) throws IOException
{
Vm.startEve(args);
//
// First test
the local method call.
//
TestInterface test = new
testServer();
testTheInterface(test);
//
// Now test
over a local RMI connection.
//
IRemoteProxyMaker pm = Vm.getRemoteProxyMaker(TestInterface.class, 0, null);
test = (TestInterface)pm.createTestConnection(new
testServer());
testTheInterface(test);
//
// Now try
again, but with an impatient RMI connection.
//
pm = Vm.getRemoteProxyMaker(TestInterface.class, 0, null);
pm.setTimeOut(1000); // Will only wait for 1 second before timeout.
test = (TestInterface)pm.createTestConnection(new
testServer());
testTheInterface(test);
//
System.out.println("Done,
press ^C to exit.");
}
}
This will demonstrate how easy it is to set up a client/server application using RMI. Each client will be able to join chat sessions and once in a session all messages to the session gets sent to all clients. The system will work like this:
1. The
client connects to the server and uses the ChatServer interface for
authentication.
2. If
accepted the server sends the client a ChatLobby interface.
3. The
client uses the ChatLobby interface to list running sessions, start a
session and join a session.
4. Once
a session is joined/created the client is sent a ChatSession interface
that is used to send messages to the session and to exit the session.
5. Also on joining/creating a session the client sends to the server a ChatClient interface. This is an interface the server uses to send messages to the client. With this interface it is the server that makes RMI calls to the client.
First lets setup the interfaces starting with the ChatServer interface. Note that this interface does not have to implement RemoteInterface because it is the starting point for the interaction and upon connection the client explicitly creates a proxy to call it, while the server explicitly creates a server object to service it. This is the only case where an explicit proxy must be created; all other proxies are created automatically when the client and server send RemoteInterface objects to each other. Note that for brevity, the source code for the various exceptions are omitted.
package evesamples.remote.chat;
import
eve.data.PropertyList;
public
interface ChatServer {
public ChatLobby enterLobby(String
userName, String password, PropertyList additionalInformation)
throws
AuthenticationFailedException;
}
The next interface is ChatLobby. This represents the main interaction between the client the server. Note that it extends RemoteInterface so that when the server returns an actual implementation of a ChatLobby on its process, this is converted to a Proxy object on the remote process which implements ChatLobby by sending remote method calls to the server’s ChatLobby.
package evesamples.remote.chat;
import
eve.sys.RemoteInterface;
public
interface ChatLobby extends RemoteInterface {
public String[] listSessions();
public ChatSession startSession(String
sessionName, ChatClient client)
throws UnauthorizedException, SessionAlreadyRunningException;
public ChatSession joinSession(String
sessionName, ChatClient client)
throws UnauthorizedException,
SessionNotFoundException;
}
The ChatSession is used by the client to send messages to the session.
package evesamples.remote.chat;
import
eve.sys.RemoteInterface;
public
interface ChatSession extends RemoteInterface {
public void sendText(String text);
public void leave();
}
And the ChatClient is used to receive messages from the session.
package evesamples.remote.chat;
import
eve.sys.RemoteInterface;
public
interface ChatClient extends RemoteInterface {
public void keepAlive();
public void incomingMessage(String
text, String from);
}
The keepAlive() method is called by the server periodically to confirm that the connection is live. If a RemoteMethodException is thrown it knows that the connection has died and it will automatically remove the client from the session.
Now we can have provide a full implementation very easily.
package evesamples.remote.chat;
import
java.net.ServerSocket;
import
java.net.Socket;
import
java.util.Hashtable;
import
java.util.Iterator;
import
java.util.Map;
import
java.util.Vector;
import
eve.data.Property;
import
eve.data.PropertyList;
import
eve.net.RemoteConnection;
import
eve.sys.IRemoteProxyMaker;
import
eve.sys.RemoteMethodException;
import
eve.sys.Vm;
/**
* This implements ChatServer and all the other server interfaces via
* inner classes.
*/
public
class ChatServerObject implements ChatServer{
//
// Simple authentication, just only
allow three users and ignore passwords.
//
public ChatLobby enterLobby(String
userName, String password,
PropertyList
additionalInformation)
throws
AuthenticationFailedException {
if (!userName.equals("mike") &&
!userName.equals("ash") && !userName.equals("krystal"))
throw new
AuthenticationFailedException(userName);
return new ChatLobbyObject(userName);
}
//
// Active sessions are stored in here.
//
Map sessions = new Hashtable();
//...............................................................
class ChatLobbyObject implements ChatLobby {
//...............................................................
String user;
public ChatLobbyObject(String
user)
{
this.user = user;
}
//
public ChatSession
joinSession(String sessionName, ChatClient client)
throws UnauthorizedException,
SessionNotFoundException
{
synchronized(sessions){
ActiveSession as =
(ActiveSession)sessions.get(sessionName);
if (as == null) throw new
SessionNotFoundException(sessionName);
return as.join(user,client);
}
}
public String[] listSessions() {
Vector v = new Vector();
synchronized(sessions){
v.addAll(sessions.keySet());
}
String[] r = new String[v.size()];
v.copyInto(r);
return r;
}
public ChatSession
startSession(String sessionName,ChatClient client)
throws
SessionAlreadyRunningException, UnauthorizedException{
synchronized(sessions){
if (sessions.get(sessionName) != null)
throw new
SessionAlreadyRunningException(sessionName);
ActiveSession as = new ActiveSession(sessionName);
ChatSession ret =
as.join(user,client);
sessions.put(sessionName,as);
return ret;
}
}
}
//...............................................................
class ActiveSession {
//...............................................................
Map users = new Hashtable();
String sessionName;
ActiveSession(String name)
{
sessionName = name;
}
//
// A user is
joining this session. Return a ChatSession
// for them to
interact with this session.
//
ChatSession join(String
user,ChatClient client)
throws UnauthorizedException
{
return new clientSession(user,client);
}
//
// Implement a
ChatSession for this ActiveSession.
//
class clientSession implements ChatSession{
ChatClient client;
String user;
boolean left;
//
// Used for
sending messages to the client.
//
Vector dispatch = new Vector();
synchronized void sendToClient(String
message, String from)
{
dispatch.add(new Property(from,message));
notifyAll();
}
clientSession(String user, final ChatClient client)
{
this.user = user;
this.client = client;
users.put(user,this);
//
// This is the
Thread that sends messages to the
// ChatClient
and also checks the keepAlive().
// It runs
until left is false.
//
new Thread(){
public void run(){
while(!left){
try{
Property[] toGo = null;
synchronized(clientSession.this){
if (dispatch.size() != 0){
toGo
= new Property[dispatch.size()];
dispatch.copyInto(toGo);
dispatch.clear();
}
}
if (toGo != null){
for (int i = 0; i<toGo.length; i++)
client.incomingMessage((String)toGo[i].value, toGo[i].name);
continue;
}else{
client.keepAlive();
}
}catch(RemoteMethodException e){
// Error
occured with RMI connection.
leave();
return;
}
synchronized(clientSession.this){
try{
clientSession.this.wait(1000);
}catch(InterruptedException e){}
}
}
}
}.start();
}
//
public synchronized void leave() {
if (left) return;
System.out.println(user+" has
left.");
left = true;
users.remove(user);
if (users.size() == 0)
sessions.remove(sessionName);
notifyAll();
}
//
public void sendText(String text) {
for (Iterator e = users.values().iterator();
e.hasNext();){
clientSession cs =
(clientSession)e.next();
cs.sendToClient(text,
user);
}
}
}
}
This completely defines a simple ChatServer. To setup the server ready to accept any number of connections we do this:
public static void main(String[] args) throws Exception
{
Vm.startEve(args);
ServerSocket ss = new ServerSocket(2000);
System.out.println("ChatServer
running on port 2000");
ChatServerObject cso = new ChatServerObject();
while(true)
eve.net.Net.startRemoteServer(cso,
ChatServer.class, ss.accept(), 0);
}
The server listens to port 2000 for incoming connections. The Net.startRemoteServer() method is a convenience method that can be used to start a remote server on a connected socket. To connect the client does this:
static ChatLobby login(ChatServer cso,
String user) throws
AuthenticationFailedException, Exception
{
if (cso == null){
String name =
"localhost";
Socket s = new Socket(name,2000);
cso = (ChatServer)eve.net.Net.startClientProxy(ChatServer.class, s, 0);
}
return cso.enterLobby(user, "", null);
}
The Net.startClientProxy() method is another convenience method that can be used to create a local Proxy for a remote server over a connected socket.
Not any value can be sent as a parameter or returned from an RMI call. Here are the types of objects that can be encoded.
o All primitive values.
o Arrays of primitive values.
o Strings.
o Arrays of encodable values.
o Vectors containing encodable values.
o An object that implements eve.util.Encodable. This is a marker interface that indicates that the system should automatically attempt to encode all public fields in the object. Any field values that are not encodable will not be encoded.
o An object that implements ByteEncodable and ByteDecodable.
o An object that implements any of TextEncodable, Textable, Stringable all from the eve.util package.
o An object that implements eve.ui.data.LiveData (because it extends TextEncodable).
Although you may be tempted to program your client/server system in exactly the same way as you program a completely local system you should bear some important points in mind.
Methods invoked over RMI can throw a RemoteMethod exception that indicates that an error has occurred over the communication channel somehow. You should always be ready to catch such exceptions and handle them accordingly. How you recover from such errors is up to you. Usually you would try to reconnect to the server and reestablish whatever session is currently active. However you must realize that all Proxy objects associated with the old session would no longer be valid. Therefore the more complicated your interface, the more difficult it will be to recover from such an error.
Additionally, RMI assigns maximum length of time it will wait for a remote method call to complete before assuming an error and timing out with a RemoteMethod exception. The IRemoteProxyMaker used to make the initial client and server objects can be used to specify the time out in milliseconds that RMI will use over that connection. However it is safest that you attempt to make all client/server calls non-blocking. That is, calls to the server should return as quickly as possible. If a call is likely to take more than a few seconds, it is better that the client send the server an object that implements a RemoteInterface which allows the server to notify the client as to the progress of long operations. The client then makes the request and provides the server with the notify interface. The server then starts a new thread to handle the request and returns immediately.