RMI, Inheritance, Call-Back Pattern

Job Download - Running Example

The following running example illustrates a simple Client-Server application passing a Java object through a Remote Method Invocation.
This example shows a simple server that would have a queue of jobs to be performed by processor (client) machines (so-called dispatching mechanism).
The client application would run on the processing machines, and contact the server to download a Job object to be processed.
This design could be used to farmout large collections of jobs to a collection of client machines, such as individual rendering jobs for an animation sequence.
 

Job.java :

This class represents a simple object that does some processing. In order to be passed in an RMI call objects must implement java.io.Serializable. Serializable is an empty interface that simply allows you to control which objects may be passed across the network and which objects may not.
public class Job implements java.io.Serializable{
        String msg;
        public Job(){
                //Just to distinguish one job from the next
                msg = Long.toString(System.currentTimeMillis());
        }
        public void process(){
                //Put complex algorithm here
                System.out.println("Job generated on server at: "+msg);
        }
}

JobQueue.java :

This interface represents the methods that may be remotely invoked on the server. Every RMI server must implement at least one interface that extends java.rmi.Remote.
public interface JobQueue extends java.rmi.Remote{
        //Get the next job on the queue
        Job getJob() throws java.rmi.RemoteException;
}

JobQueueImpl.java :

This class is one possible implementation of the remote interface above. 
import java.rmi.*;
import java.rmi.server.*;

public class JobQueueImpl extends UnicastRemoteObject 
                        implements JobQueue {

        public JobQueueImpl() throws RemoteException {
                super();
                try {
                        Naming.bind("rmi://localhost/JobQueue",this);
                        System.out.println("Server Ready");
                } catch (Exception e){
                        System.err.println("Failed to start RMI server: "
                                + e.getMessage());
                }
        }

        //The method that will be invoked remotely by clients
        public Job getJob(){
                return new Job();
        }

        public static void main(String[] args){
                try {
                        new JobQueueImpl();
                } catch (RemoteException e){
                        System.err.println("Failed to start RMI server: "
                                + e.getMessage());
                }
        }
}

Worker.java :

This class is a simple client that connects to the remote server and requests a Job. It then invokes the process method on the Job to perform the work.
import java.rmi.*;

public class Worker {
        public Worker(){
                try {
                        //Get a reference to the remote server on this machine
                        JobQueue queue = (JobQueue)Naming.lookup(
                                "rmi:///JobQueue");

                        //Request a job from the remote server
                        Job myJob = queue.getJob();

                        //Start processing the Job.
                        myJob.process();
                } catch(Exception e){
                        System.err.println("Error connecting to server: "
                                +e.getMessage());
                }
        }

        public static void main(String[] args){
                new Worker();
        }
}


Inheritance

This example is an extension on the previous example. This example assumes that you may have more than one type of Job that you want processed by client machines.
It illustrates how both local and remote objects can be extended. By creating a new server object that extends from the previous JobQueueImpl we can re-use code from the old server.
By extending the new Job types from the old Job type we don't need to modify any of the client code.

Matrix.java :

This class would be used to perform some sort of matrix manipulation. It is based on Job.java so it does not need to explicitly implement Serializable to be passed as an argument in a remote method invocation.
public class Matrix extends Job {
        public void process(){
                System.out.println("Would be processing a matrix");
        }
}

Prime.java :

This class is just another class that extends from Job.java.
public class Prime extends Job {
        public void process(){
                System.out.println("Would be computing primes");
        }
}

PolyJobQueueImpl.java :

This server is derived from the original JobQueueImpl class and re-uses the rmiregistry binding portion of the code. Main is re-written to make sure that an instance of this object is created instead of the old JobQueueImpl object. The getJob method is also modified to return the new types of Jobs.
import java.rmi.*;
import java.rmi.server.*;

public class PolyJobQueueImpl extends JobQueueImpl {
        int index = 1;
        Job[] list = { new Matrix() , new Prime() };

        public PolyJobQueueImpl() throws RemoteException {
                //JobQueueImpl takes care of the binding for us
                super();
        }
        
        //Polymorphically extend getJob to return the new Job types
        public Job getJob(){
                index = (index+1) % list.length; //Walk the index
                return list[index];
        }

        public static void main(String[] args){
                try {
                        new PolyJobQueueImpl();
                } catch (RemoteException e){
                        System.err.println("Failed to start RMI server: "
                                + e.getMessage());
                }
        }
}
To run the above example, simply place the three new files into the same directory as the first example, then run: You'll notice that the old Client, Job, and JobQueue files were not recompiled nor was the RMI compiler re-run.
Due to the power of object orientation and RMIs close alignment with the Java object model these files don't need to be re-compiled because they did not change.
Polymorphism and runtime binding take care of the changes for you. If the client software had already been installed on remote client machines this change to the server software would not require an upgrade of the clients in any way.

Callback Pattern in RMI

In the previous examples the clients were actively making requests for jobs from the passive server. In this example the roles are reversed.
The client makes an initial connection to the server to register as a worker and then become passive. The server actively pushes jobs to any registered workers.
This setup would allow a collection of clients to all register with a server and wait for jobs to become available.
As the server collects jobs it would push them out to whichever clients are idle (call the client back !).

For simplicity sake this example only allows a single client to register but could be easily modified to store a collection of clients.
As this is a fairly drastic change in the design of this example, most of the code has changed. The only code that remains the same is the Job class.
 

CallbackJobQueue.java :

public interface CallbackJobQueue extends java.rmi.Remote{
        void register(CallbackWorker aClient) throws java.rmi.RemoteException;
}

CallbackJobQueueImpl.java :

import java.rmi.*;
import java.rmi.server.*;

public class CallbackJobQueueImpl extends UnicastRemoteObject 
                        implements CallbackJobQueue {

        CallbackWorker client;

        public CallbackJobQueueImpl() throws RemoteException {
                super();
                try {
                        //Bind to the registry running on this machine
                        Naming.bind("rmi://lpc1.itec.uni-klu.ac.at/JobQueue",this);
                        System.out.println("Server Ready");
                } catch (Exception e){
                        System.err.println("Failed to start RMI server: "
                                + e.getMessage());
                }
                //Loop forever and give out jobs (better use wait/notify)
                while(true){
                        if (client != null){
                                client.processJob(new Job());
                        }
                        try {
                                Thread.sleep(1000); //Sleep for a sec.
                        } catch(InterruptedException e){}
                }
        }

        public void register(CallbackWorker aClient){
                client = aClient;
        }

        public static void main(String[] args){
                try {
                        new CallbackJobQueueImpl();
                } catch (RemoteException e){
                        System.err.println("Failure with RMI server: "
                                + e.getMessage());
                }
        }
}

CallbackWorker.java :

This is the remote Interface that the CallbackWorkerImpl must implement to be an RMI server. This allows the CallbackJobQueueImpl server to be able to remotely invoke methods on the CallbackWorkerImpl client.
public interface CallbackWorker extends java.rmi.Remote{
        public void processJob(Job newJob) throws java.rmi.RemoteException;
}

CallbackWorkerImpl.java :

This class is the implementation of the CallbackWorker client. This client object also functions as an RMI server to allow callbacks to take place.
import java.rmi.*;
import java.rmi.server.*;

public class CallbackWorkerImpl implements CallbackWorker{
        public CallbackWorkerImpl(){
                try {
                        UnicastRemoteObject.exportObject(this);
                        CallbackJobQueue server = 
                                (CallbackJobQueue)Naming.lookup(
                                        "rmi:///JobQueue");
                        //Register with the server, then just wait for jobs
                        server.register(this); 
                } catch (Exception e){
                        System.err.println("Failed to setup for RMI");
                }
        }

        public void processJob(Job newJob){
                newJob.process();
        }

        public static void main(String[] args){
                new CallbackWorkerImpl();
        }
}
To run the above example, place all of the code into the directory with the previous examples and run the following commands: The careful reader should have noticed a few subtle errors in the design of this example. The CallbackJobQueueImpl constructor never finishes. This object should probably start a secondary thread to sit in the infinite loop passing out jobs. Another subtle problem is the processJob method of the client will block until the Job finishes processing. This would be undesirable if the server is trying to have multiple clients working at the same time. To resolve this deficiency the server could start one thread per client, or the Job object could start a secondary thread to perform the processing on the client side.


harald.kosch@itec.uni-klu.ac.at - Department's HomePage
Last updated 30/04/2002.