Eve Application Development

Michael L Brereton - 06 March 2008, http://www.ewesoft.com/

 

Advanced Guide - Contents

>> Next: Eve Native Interface

Remote Method Invocation

The Essentials of RMI 1

Objects Passed as Reference. 2

The IRemoteProxyMaker Interface. 2

Server Setup. 2

Client Setup. 3

Testing RMI Interfaces Locally. 3

Example Application – Simple Chat System... 5

Byte Encodable Objects. 9

Important Design Considerations. 9

New Failure Modes. 9

Call Time Outs. 9

 

 

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 Essentials of RMI

 

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.

Objects Passed as Reference

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.

The IRemoteProxyMaker Interface

 

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.

Server Setup

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.

 

Client Setup

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.

Testing RMI Interfaces Locally

 

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

      }

}

 

Example Application – Simple Chat System

 

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.

Byte Encodable Objects

 

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

Important Design Considerations

 

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.

New Failure Modes

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.

Call Time Outs

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.