Client/Server Programming -- Week 3

Introduction

This week we'll look at using Java RMI to write client/server applications.

Note that the links from this page are to handouts that will be distributed the night of class.  Only print out those handouts if you could not attend class.

Main topics this week:

Assignment 2-1 Due
RMI
Finding Remote Objects
Designing a Remote Object
Throwing Exceptions from Server to Client
Passing Objects to Servers
Push or Pull?
Asynchronous Communication
Assignment 3-1 Assigned
Next Week

Assignment 2-1 Due

Your assignment 2-1 is due today.  You must turn in a printout of your code and your design for the assignment, in addition to submitting the code on einstein using ~shaffsta/bin/submit345 . 

You may turn this assignment in next week for a 10% penalty.

RMI

RMI stands for Remote Method Invocation.  RMI is one way of doing client/server programming, and is the way that you'll use in your assignment 3-1. 

The goal of RMI is to make the syntax for client/server programming as close as possible to that needed for non-client/server programming.  When using RMI, you are conceptually making a method call on an object.  However, the object is located on a server machine rather than on the local machine. 

An important principle that was adopted to make RMI work was, The behavior of a class and the implementation of that behavior are separate concepts.    This means that you can define what methods a class has in one place, and define the implementation of those methods in another place.  By now you should recognize that as a description of a Java interface.

So, a Java interface defines the methods that are available on a remote class.  A Java object in the server defines the implementation of those methods.  As far as the client is concerned, it is simply making a method call on an object.  Communication between the client and server is handled more or less transparently.

You know from CS 2 than a Java interface can be implemented by more than one class.   In RMI, there are two classes that implement the interface that defines the methods available on a server object.  One of those classes lives in the server and actually does the work.  The other class lives in the client, and provides the communications routines needed to contact the class in the server.  The class that lives in the client is called a proxy, or sometimes a stub. 

The stub or proxy in the client communicates with a class in the server called the skeleton.  The skeleton then makes the actual call to the server object and returns results back to the client.

(draw the architecture for this)

Finding Remote Objects

We've said that RMI tries to make the use of remote objects as transparent as possible for the client.  At some point, however, the client must say which remote object it wants, and where that remote object lives.  If the client knew a specific ip address of the server, all the clients would have to be changed if the server needed to be moved.

Instead, RMI uses a directory service.  A directory service is a server that knows how to convert from an object name to the location of the server that stores that object.   So a client could tell the directory service, "I want to use the object named Foo", and the directory service would know where to find that object. 

The advantage of using a directory service is that the server objects can move without changing the client.  You can also do things like load balancing, where client requests are spread among several servers, each of which has a copy of the desired object.

The directory service you'll use for your assignment 3-1 is the one that comes with RMI, called rmiregistry.  This is a simple program you can run on your home machine if you want.  It will listen on any port you choose, but the default port is 1099.  If you want to use the default port, simply type in "rmiregistry" to start it.  If you want to use a custom port, add the port number to the command line...for example, to tell rmiregistry to listen on port 4200, type "rmiregistry 4200".

A client can connect to a remote object using the class Naming.lookup method.   What you pass to the lookup method is a URL of the form, "//host:port/ObjectName".   What you get back is a reference to the remote object.  The host and port passed in is the host and port identifying where the rmiregistry program is running.   We'll see an example of this later.

A server can register a remote object with the directory service by using the Naming.rebind method.  What you pass to the rebind method is a URL just like the client passes, plus a reference to the object you want to register.  We'll see an example of this later, too.

(add the naming service to the architecture diagram)

Designing a Remote Object

So you have an object you want to put on a server.  The steps to doing that are:

  1. Create an interface with all the methods that you want to be able to call from the client.
  2. Write the implementation for the object in the server
  3. Create the stubs and skeletons
  4. Write a server program that will register the object you wrote in step 2 with the directory service
  5. Write a client program

After those five steps, the programs should be ready to run.  You run them by:

  1. Start the rmiregistry directory service
  2. Start the server
  3. Start the client(s)

Let's look at each of the steps in more detail.

Create an interface

You should have already designed the interface between client and server as part of your application design.  You now need to convert this into a Java interface suitable for use with RMI.  The interface is written just like other Java interfaces, except for two differences:

  1. The interface must extend the interface called Remote.  Remote is an interface provided by Java, in the java.rmi package.
  2. Every method on the interface must say that it throws RemoteException.  This is because any method call to a remote object may encounter communications problems that will cause the method call to fail.

Here's an example interface for a remote object:

import java.rmi.*;

public interface Foo extends Remote
{
    public void setData (int d) throws RemoteException;
    public int getData () throws RemoteException;
    public void randomData () throws RemoteException;
}

You compile this interface using javac like normal (e.g. "javac Foo.java").   Normal package rules for compiling apply if the interface is in a package (e.g. you may need to set the classpath appropriately).

Write the Implementation

We now need to write a class that implements the interface.  This will be the object that lives in the server.  This class must inherit from a Java class called UnicastRemoteObject, in the java.rmi.server package.  The constructor of the class must say that it throws RemoteException. 

UnicastRemoteObject has two constructors that will be of interest.  One of them is the default constructor.  When you use the default UnicastRemoteObject constructor, the server object will listen on a random port for connections from the client.  This is suitable if you are not running behind a firewall.  If you are running behind a firewall (such as on einstein), and need to know exactly which port the server object will be listening on, you can use the second constructor.  In the second constructor, you pass in a port for the server object to use.  We'll use the second constructor so we can specify a port.  Note this is the only place that the server port needs to appear.  Everywhere else we use a port, we will be using the rmiregistry port.

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

public class FooImpl extends UnicastRemoteObject implements Foo
{
    private int data;

    public FooImpl () throws RemoteException
    {
        super (4220);
    }

    public void setData (int d) throws RemoteException
    {
        data = d;
    }

    public int getData () throws RemoteException
    {
        return data;
    }

    public void randomData () throws RemoteException
    {
        data = (int) (Math.random () * 1000);
    }
}

You compile this the same way you compile any other class, again subject to normal package rules if the class is in a package.

Create stubs and skeletons

You use a different compiler to create the stubs and skeletons.  The compiler is called rmic, and takes as an argument the name of the server side implementation of the interface.  So, in our example, we'd use "rmic FooImpl".  This would create two .class files: Foo_Stub.class, and Foo_Skel.class.  The Foo_Stub.class file would be used by the client, while the Foo_Skel.class file and the Foo_Stub.class file would be used by the server.  You cannot run rmic until after you have compiled the server object, as rmic uses the .class file, not the .java file.

Note that if you're using packages, you need to pass in both a classpath and the full package name of the class (exactly the same parameters you would pass to the java command to run the class).  You will also want to run the rmic command from the root of the package directory structure to get the stub and skeletons in the right place (where ever you run the rmic command from, it will create package subdirectories there).   Depending on your use of packages, you may need to experiment with where you run rmic.

Write Server Program

To go along with the server object, you need a server program.  The server program will register the server object with the directory service.  Here's a simple example based on our FooImpl class:

import java.rmi.*;

public class FooServer
{
    public static void main (String [] args)
    {
        try
        {
            Foo f = new FooImpl ();

            Naming.rebind ("//localhost:4200/Foo", f);
        }
        catch (Exception e)
        {
            e.printStackTrace ();
        }
    }
}

You can get fancier with this server, by allowing the port for the directory service to be passed in, and even allowing the port for the server object to be passed in as well.

You compile this the same way you compile other Java classes, subject to the usual package rules.

Write a Client Program

Now we need to write a client program.  The client program will have a bit of code that uses Naming.lookup to get a reference to a proxy for the server object.  After that, calls to the server object can be made by calling method on the proxy.

public class FooClient
{
    public static void main (String [] args)
    {
        Foo f;

        try
        {
            f = (Foo) Naming.lookup ("//localhost:4200/Foo");

            f.randomData ();
            System.out.println (f.getData ());
            f.setData (50);
            System.out.println (f.getData ());
        }
        catch (Exception e)
        {
            e.printStackTrace ();
        }       
    }
}

Note that any calls to the proxy methods must be inside a try/catch block to handle possible RemoteExceptions. 

Running Everything

To run it all, we'll need three different console or command windows. 

In the first window, we run the rmiregistry service.  It is important that the classpath *not* be set when running rmiregistry.  On Windows, before you run rmiregistry, clear the classpath using "set CLASSPATH=".  On Unix, clear the classpath using "unset CLASSPATH".  If you get an exception when you try to run the server, it's probably because your classpath was set when you ran rmiregistry.  Also note that rmiregistry must be run from the root directory of the package.  On einstein, you should run rmiregistry with a port between 4200 and 4299.   Those ports are open to the outside world so you can run the server on Unix and the client on your home machine.  In the code examples above, we're assuming that rmiregistry is being run on port 4200.

In the second window, we run the server.  This is no different from running a normal Java program. You do set the classpath appropriately for whatever package your classes are in.

In the third window, we run the client.  This is no different from running a normal Java program. You do set the classpath appropriately for whatever package your classes are in.

Throwing Exceptions from Server to Client

Your server object probably has exceptions that it wants to throw for error conditions.   But the only exception you get to throw is a RemoteException.  So how do you throw other exceptions?

The answer is easy.  You define the interface as throwing any other exceptions you want to throw, and you define your server object as defining those same exceptions.   Then you simply throw the exception in the server, and it is caught in the client.   If the exception is a RuntimeException, you do not even need to say that it is thrown.   The process of converting the exception into data transmitted over the network, and then back into an exception again on the client, is handled for you by the skeletons and stubs. 

Passing Objects to Servers

When you pass an object to a server via RMI, the object is passed by value.  A copy is made in the server and any changes to the object are made to that copy.  The client will not see updates to that object.  All information coming back from the server should be done as return values.

Also note that in order to pass an object to the server or return it from the server, that object must be serializable.  Java uses serialization to transmit the object's data across the network.

Push or Pull?

Note that in an RMI program, a technical challenge is providing asynchronous communication from server to client.  This means information being transmitted from the server to the client when the client did not request the information.  The two basic approaches are pushing or pulling information.

In a pulling strategy, the client must request all information from the server.   So if the server may be sending information, the client must ask the server periodically if it has information to send (this is known as polling the server).   This may require multi-threading on the client if you also want to provide interactivity to the user at the same time. 

In a pushing strategy, the server is able to send information to the client even when the client isn't expecting it.  We'll look at how to do this next week.

Both strategies have advantages and disadvantages, but pushing data is generally regarded as a better choice since it limits the amount of network traffic (each request from the client consumes bandwidth, even those requests for which the server has no response).  In general, certain technologies lend themselves more to one technique or the other (for example, web-based solutions are generally pull technologies).

Asynchronous Communication

RMI is best suited for synchrounous communication...where the client sends requests to the server, and the server sends responses to those requests, but the server never sends anything else to the client.  However, many applications (such as your RMI assignment) that deal with communication between multiple clients, require the server to send information to the client.  You can choose to use a pull method, as discussed above, but that uses up quite a bit of bandwidth on the network. 

The push method, where the server sends data to the client even when the client has not asked for data, is more efficient in terms of network usage.  But, we'll need to see how to do that using RMI.  The basic idea is simple...you create not only an interface for calls from the client to the server, but you create a second interface for calls from the server to the client.  Note that using RMI to call into the client means that each call will be in its own thread, so the client will need synchronized appropriately on shared data.

Your text refers to this as "Bidirectional Messaging".

We'll look at converting a pull type client/server application into a push type client/server application.  The pull type application we'll convert is a simple chat server.  The pull chat server consists of these three files:

Chat server interface
Chat server implementation
Chat client

If you look over the files, you'll see that the server simply accepts requests from the clients...there is no communication from server to client except what happens as the return from a client-initiated method call.  For a chat server, this leads to some unacceptable behavior.  For example, the client does not see new messages from the server until the client checks for new messages from the server. 

Here are the basic steps to convert a pull type application into a push type application:

  1. Create the interface that represents server calls to the client.
  2. Write an implementation for the server to client interface.  We'll call this a callback instance.
  3. Compile the interface using rmic to generate the stubs and skeletons
  4. Create a method in the client to server interface that allows the client to send the server a callback instance
  5. In the client, pass the callback instance to the server
  6. In the server, when necessary, use methods on the callback instance to send data to the client

Let's look at each step in more detail.

Create the Interface

As part of your client/server design, you should have identified interface methods based on their direction of communication (e.g. from client to server, or from server to client).  You will write a Java interface for those methods that are server to client methods.  This interface follows the same rules as the client to server interface: it must extend the java.rmi.Remote interface, and every method must throw the java.rmi.RemoteException exception. 

Let's look in detail at the interface for the chat server.  The server will allow any number of clients to connect to it, and any text that a client sends will be sent to all clients.  Thus, the server simply needs to send a string of text to each client, and we could have an interface like this:

public interface ChatClient extends java.rmi.Remote
{
  public void send (String text) throws java.rmi.RemoteException;
}

This interface will be used by the server to send text to the client.

Write an implementation for the interface

Now we need to write an implementation for the server to client interface. 

public class ChatClientImpl extends UnicastRemoteObject implements ChatClient
{
  public ChatClientImpl ()
  {
    super (4221);
  }

  public void send (String text) throws java.rmi.RemoteException
  {
    System.out.println (text);
  }
}

Note that this instance is written just like the server instance we looked at last week.  It needs a port specified, but this is a port on the client machine, not the server machine.  If the machine the client is on is not using a firewall, you could simply call "super ()" without passing in a port number to use a random available port.

(Your book talks about using UnicastRemoteObject.exportObject...this is not necessary, the call to super is all you need.)

Compile the interface using rmic

You do this in the same way as you did last week for the client to server interface (e.g. "rmic ChatClientImpl").  This creates the stubs and skeletons.  Note that for this interface, the stub goes into the server and the stub and skeleton goes into the client.  This is the opposite of what we talked about last week, because the direction of communication is the opposite.

Create a method to pass callback instance to server

We need to get an instance of our interface to the server, so it can be used to send text back to the client.  We do this by adding or modifying a method to the client to server interface.  For our chat program, we'll probably add the callback to the connect method, making our chat server interface become:

public interface ChatServer extends Remote
{
    public void connect (String name, ChatClient client) throws RemoteException;

    public void send (String name, String text) throws RemoteException;
    public void disconnect (String name)throws RemoteException;
}

Now, when the client calls the connect method, it will also pass an instance of a class that implements ChatClient.  Note also that we removed the pull type method for getting messages, since those will come in through the ChatClient interface now.

In the client, pass callback instance to server

We need to make the client pass in a callback instance to the server.  In our chat client, we do this when we call connect:

    private static void connect (String name)
    {
        try
        {
            server.connect (name, new ChatClientImpl ());
        }
        catch (RemoteException e)
        {
            e.printStackTrace();
        }
    }

In the server, use the callback instance

The server must now keep track of all the client callbacks.  We'll add a linked list to keep track of these, and get rid of the linked list for keeping track of old messages.  When a client sends text to the server, the server will loop through all the client callbacks and send the text to each one.

Here are the files for the push version of the chat server:

Chat server interface
Chat server implementation
Chat client interface
Chat client implementation

One important note: if you synchronize in either the client or the server, be careful of deadlocks.  For example, if all your methods in your client are synchronized, and you make a call into the server, and that server call tries to make a call back into the client, you will get a deadlock situation. 

Another important note: if multiple threads in an application try to read input from the same stream (such as keyboard input), the threads will receive input in an unpredictable order.  This can most often happen in the client if you have some sort of main menu that requires the user to enter a selection, and the server can also send an asynchronous request to the client that requires user input.  A common cause of this is one client sending a message to the server that requires the server to get input from another client.  The following code displays this problem when you have two clients running and selection option 2...a message requiring input is sent from the server to the other client, which is already waiting for menu input:

Server Interface
Server Implementation
Client Interface
Client Implementation

To avoid this problem, you may need to use the ready () method in BufferedReader to check to see if there is input available first, before reading in your main thread.  An example of this is here:

Client Implementation

The disadvantage to this approach is that user input does not appear on the screen until after the user presses enter.  This is generally not a problem when working with graphical user interfaces, which is what you would really use for an application with this sort of communication between client and server.  Since this will be an issue with your assignment 6-1, you can either use the input blocking solution presented above, or you can use a graphical user interface and avoid the problem entirely.

Assignment 3-1 Assigned

Assignment 3-1 is assigned tonight.  This assignment is a client/server application using RMI as the client/server technology.  This assignment is due on week 5, however the design should be finished as soon as possible and sent to me for comments. 

This assignment can be done using either push or pull techniques.  You should be certain to synchronize appropriately on shared data.

Next Week

Next week we will continue looking at RMI, including reviewing multithreading and synchronization.