Previous Contents Next

RMI Example - RMIChat

Chapter 8: RMI Example - RMIChat

Just as with sockets, we're going to build a moderately sized example, that illustrates some of the things we have seen so far - we are also going to briefly look at some other RMI issues, to do with building rather more industrial-scale examples. The example we're going to build is more or less exactly the same as the socket example - and a lot of the code will actually look pretty familiar.

8.1. Overview

We're going to build a callback-based chat program, that will use two interfaces - one for the server functionality, and one for the callbacks. The first version will have a number of issues - it's a bit clumsy and inefficient. We're doing this to illustrate some of the techniques we can use to speed things up and make them a bit more scaleable.

Unlike the socket example, we're not going to try to encorporate all versions in one application - life's too short to do that twice.

8.2. Interfaces

Here's the server interface:



import java.rmi.*;

public interface ChatInterface extends Remote {
    public void join(Notify n) throws RemoteException;
    
    public void talk(Notify n, String s) throws RemoteException;
    
    public void leave(Notify n) throws RemoteException;
}

We can do three things: join, talk and leave. The Notify object (actually interface) is the callback link to the client:



import java.rmi.*;

public interface Notify extends Remote {

	public void joinMessage(String name) throws RemoteException;
	
    public void sendMessage(String name, String message) throws RemoteException;
    
    public void exitMessage(String name) throws RemoteException;
    
    public String getName() throws RemoteException;
    
    //Not called remotely
    public void setName(String s) throws RemoteException;
}

The server can invoke one of five methods on the client - it can tell it a client has joined (joinMessage), sent a message (sendMessage), or left (exitMessage); it can also set and get the client's name. In this version, the server never calls setName.

8.3. Client Code

The client looks the same as the socket version and so much of the GUI code is shared (and left out again). There's no need for a separate thread to handle arriving messages however - that's all handled by the callback object.


import javax.swing.*;
import java.awt.event.*;
import java.awt.*;
import java.rmi.*;
import java.rmi.server.*;

public class ChatClient extends javax.swing.JFrame {
   
    ...
    
    Notify displayChat;
    ChatInterface chatServer;
     
    public ChatClient() {
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                initComponents();
            }
        });
        try {
            Remote remoteObject = Naming.lookup("rmichat");

			if (remoteObject instanceof ChatInterface) {
				chatServer = (ChatInterface)remoteObject ;
				displayChat = new
				DisplayMessage(otherText);
			} else {
				System.out.println("Not a Chat Server.");
				System.exit(0);
			}
        }
        catch(Exception e){
            System.out.println("RMI Lookup Exception");
            System.exit(0);
        };    
    	   	
    	frame.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
          	  try {
          	      chatServer.leave(displayChat);
          	  }
          	  catch (Exception ex) {
          	      otherText.append("Exit failed.");
          	  }
          	  System.exit(0);
            }
          });
    }
   

    private void initComponents() {
    	...

        myText.addKeyListener(new java.awt.event.KeyAdapter() {
            public void keyTyped(java.awt.event.KeyEvent evt) {
                textTyped(evt);
            }
        });

        frame.getContentPane().add(myTextScroll,
            java.awt.BorderLayout.NORTH);
        
        ...
    }

    private void textTyped(java.awt.event.KeyEvent evt) {
        char c = evt.getKeyChar();
        if (c == '\n'){
        	   try {
        		   if (firstMessage) { 
        		       displayChat.setName(textString);
        		       chatServer.join(displayChat);
        			   firstMessage = false;
        		   } else {
        		       chatServer.talk(displayChat, textString);
        		   }
        	   }
        	   catch (Exception ie) {
        		   otherText.append("Failed to send message.");
        	   }
            textString = "";
        } else {
            textString = textString + c;
        }
    }
    

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

}

The registry name ('rmichat') is hard-coded, which is not so good.

The main method just calls the constructor, which sets up the GUI, the key listener (to call the textTyped method), and runs the RMI launch code. The launch code is as follows:


try {
            Remote remoteObject = Naming.lookup("rmichat");

			if (remoteObject instanceof ChatInterface) {
				chatServer = (ChatInterface)remoteObject ;
				displayChat = new
				    DisplayMessage(otherText);
			} else {
				System.out.println("Not a Chat Server.");
				System.exit(0);
			}
        }
        catch(Exception e){
  			...
        };    

We get an instance of the server, check it's actually the correct class (interface), and then cast it appropriately and set up the callback object (displayChat).

The textTyped method handles most of the communication:


    private void textTyped(java.awt.event.KeyEvent evt) {
        char c = evt.getKeyChar();
        if (c == '\n'){
        	   try {
        		   if (firstMessage) { 
        		       displayChat.setName(textString);
        		       chatServer.join(displayChat);
        			   firstMessage = false;
        		   } else {
        		       chatServer.talk(displayChat, textString);
        		   }
        	   }
        	   catch (Exception ie) {
        		   otherText.append("Failed to send message.");
        	   }
            textString = "";
        } else {
            textString = textString + c;
        }
    }

We collect text until we get a newline, and then call the server - if this is the first line of text, we set the name (displayChat.setName(...)) and join (chatServer.talk(...)). In both cases, we pass the callback object (displayChat), which might seem a bit inefficient - we'll get back to this and some other issues later. The last bit of interesting code is:


    	frame.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
          	  try {
          	      chatServer.leave(displayChat);
          	  }
          	  catch (Exception ex) {
          	      otherText.append("Exit failed.");
          	  }
          	  System.exit(0);
            }
          });

Most of the details of this are related to the Swing GUI - the important line is chatServer.leave(...) - that is, when the application closes we tell the server we have left.

8.4. The Callback Class

The Notify interface is implemented by DisplayMessage:



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

public class DisplayMessage extends UnicastRemoteObject implements Notify {

	private javax.swing.JTextArea textArea;
	private String name;

    public DisplayMessage(javax.swing.JTextArea ta)
        throws RemoteException {
    	    textArea = ta;
    }
    
    public void joinMessage(String name)
        throws RemoteException
    {
        try {
    	    textArea.append(name + " has joined\n");
    	}
        catch(Exception e){
            System.out.println("Message Failure");
            e.printStackTrace();
        };
    }
    
    public void sendMessage(String name, String message) throws RemoteException
    {
        try {
    	    textArea.append(name + " says: " + message + "\n");
    	}
        catch(Exception e){
            System.out.println("Message Failure");
            e.printStackTrace();
        };
    }
    
    public void exitMessage(String name) throws RemoteException {
        try {
    	    textArea.append(name + " has left the building.\n");
    	}
        catch(Exception e){
            System.out.println("Message Failure");
        };
    }
    
    //Notice this one not called remotely
    public void setName(String name) throws RemoteException {
    	this.name = name;
    }
    
    public String getName() {
    	return name;
    }
}

This is all pretty obvious - most of the methods append the appropriate text to the output text display in the GUI. They are so similar you might wonder if we need them all - we'll get back to that later as well.

8.5. The Server

Here is the server:



import java.rmi.*;
import java.rmi.server.*;
import java.lang.*;
import java.util.*;


public class Main extends UnicastRemoteObject implements ChatInterface {
    
    private Collection<Notify> threadList = new ArrayList<Notify>();
    
    public Main() throws RemoteException {
    }
    
    public synchronized void join(Notify n) throws RemoteException {
		threadList.add(n);
		for (Iterator i = threadList.iterator();
				 i.hasNext();) {
			Notify client = (Notify)i.next();
			client.joinMessage(n.getName());
		}
    }
    
    public synchronized void talk(Notify n, String s)
            throws RemoteException {
		for (Iterator i = threadList.iterator();
				 i.hasNext();) {
			Notify client = (Notify)i.next();
			client.sendMessage(n.getName(),s);
		}
    }
    
    public synchronized void leave(Notify n) throws RemoteException {
    	threadList.remove(n);
    	for (Iterator i = threadList.iterator();
				 i.hasNext();) {
			Notify client = (Notify)i.next();
			client.exitMessage(n.getName());
		}
    }

    public static void main(String[] args) {
         	try {
			
			Main server = new Main();

			Naming.rebind("rmichat", server);
		
		}
		catch (java.net.MalformedURLException e) {
			System.out.println("Malformed URL "
			    + e.toString());
		}
		catch (RemoteException e) {
			System.out.println("Communication error " +
			    e.toString());
		}
    }
    
}

This is pretty simple - especially when you realize that the three principle methods are very similar (we'll address this too). All the methods are synchronized to control access to the thread list.

8.5.1. Some Synchronization Comments

As an aside, in this case, the entire methods have been synchronized - rather than just synchronizing a block on the thread list object. That is:


    public synchronized void join(Notify n) throws RemoteException {
		threadList.add(n);
		for (Iterator i = threadList.iterator();
				 i.hasNext();) {
			Notify client = (Notify)i.next();
			client.joinMessage(n.getName());
		}

rather than:


     public void join(Notify n) throws RemoteException {
     	synchronized(threadList) {
		threadList.add(n);
			for (Iterator i = threadList.iterator();
					 i.hasNext();) {
				Notify client = (Notify)i.next();
				client.joinMessage(n.getName());
			}
		}
    }

The reason we did it the second way in the socket chat example is that the thread list object in that case is static - that is, class-wide. So just synchronizing the methods would not lock the thread list, which belongs to the class - and not the server object which is an instance of the class. We could have used either the first or the second methods in the case of the RMI version, but only the second method is correct for our implementation of the socket version.

8.6. Improving the Implementation

Although the version we have will work, there are a few obvious ways to improve it. Note that like the socket version, we have to be careful - it's very easy to turn a working but inefficient program into a broken one.

As with the socket version, we are going to focus solely on the server. This is because whatever changes we make to the client, they will only speed up a single client - changes to the server affect every connected client.

The first obvious change to make is the same one we made for the socket example - allow reads to proceed in parallel, but only permit a single write (i.e. new client joining, old one leaving) at a time, and only permit a write when there are no ongoing reads. The code for this will be more or less identical to the socket version.

The next obvious change comes from the observation that we are calling getName on every loop iteration whenever the server sends anything to the clients. This is an RMI call, so it will be quite slow. Furthermore, it's in a synchronized method, so not a lot else can happen while it's happening. In general, it's not good practice to make further RMI calls to other servers (even callbacks) in synchronized blocks. We could simply change the code (e.g. for join):


     public void join(Notify n) throws RemoteException {
     	String name = n.getName();
     	synchronized(threadList) {
		threadList.add(n);
			for (Iterator i = threadList.iterator();
					 i.hasNext();) {
				Notify client = (Notify)i.next();
				client.joinMessage(name);
			}
		}

But in this case we can do away with the call altogether - we can simply pass the name as an argument when we call talk. This also means we do not need to pass the callback object in this case, which is likely to lead to shorter RMI messages (we're now only sending a string instead of the callback object).

One further change I thought about making was to simplify the callback interface to only one method. This would mean that the various server messages ('x has joined', 'x has left the building') would be constructed entirely on the server, and just sent as strings to the client. This would get rid of the (slightly unsatisfactory) repeated code on the server. However, it would also mean that the messages sent back to clients would be longer. In general, the less data you send, the better. There are still ways to rewrite the server to eliminate the repeated code - but, personally, I think they're even worse.

8.7. The New Server Version

Here is the new server interface:



public interface ChatInterface extends Remote {
    public void join(Notify n, String name) throws RemoteException;
    
    public void talk(String name, String s) throws RemoteException;
    
    public void leave(Notify n, String name) throws RemoteException;
}

Here is the new callback interface:



public interface Notify extends Remote {

	public void joinMessage(String name) throws RemoteException;
	
    public void sendMessage(String name, String message) throws RemoteException;
    
    public void exitMessage(String name) throws RemoteException;
    
}

Here is the new client:



public class ChatClient extends javax.swing.JFrame {
   
	...
    private static String name = null;
	...
     
    public ChatClient() {
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                initComponents();
            }
        });
        try {
            Remote remoteObject = Naming.lookup("rmichat");

			if (remoteObject instanceof ChatInterface) {
				chatServer = (ChatInterface)remoteObject ;
				displayChat = new DisplayMessage(otherText);
			} else {
				...
			}
        }
        catch(Exception e){
            ...
        };    
    	   	
    	frame.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
          	  try {
          	  	  if (name != null) {
          	          chatServer.leave(displayChat, name);
          	      }
          	  }
          	  	...
            }
          });
    }
   

    private void initComponents() {
    	...
    }

    private void textTyped(java.awt.event.KeyEvent evt) {
        char c = evt.getKeyChar();
        if (c == '\n'){
        	   try {
        		   if (firstMessage) { 
        		   	   name = textString;
        		       chatServer.join(displayChat,name);
        			   firstMessage = false;
        		   } else {
        		       chatServer.talk(name, textString);
        		   }
        	   }
        	   catch (Exception ie) {
        		   ...
        	   }
            textString = "";
        } else {
            textString = textString + c;
        }
    }
    

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

}

If you look closely, you'll notice some other minor changes as well as those discussed above. Basically, these stop the client calling the leave method if it hasn't joined yet.

Here is the new server list object:


class ServerList {
	
	private Collection<Notify> threadList =
	            new ArrayList<Notify>();
	private int counter = 0;
	
	public synchronized void add(Notify item) {
		try {
			while (counter > 0) {
				wait();
			}
			threadList.add(item);
	    }
	    catch (InterruptedException e) {
	    	System.out.println("Addition interrupted.");
	    }
	    finally{
	        notifyAll();
	    }
	}

	public synchronized void remove(Notify item) {
	    try {
			while (counter > 0) {
				wait();
			}
			threadList.remove(item);
		}
	    catch (InterruptedException e) {
	    	System.out.println("Removal interrupted.");
	    }
	    finally {
	        notifyAll();
	    }
	}

	public synchronized void incCounter() {
		counter++;
		notifyAll();
	}
	
	public synchronized void decCounter() {
		counter--;
		notifyAll();
	}

	public Collection getCollection() {
		return threadList;
	}
}

Notice it's more or less identical to the socket version (except we haven't used interfaces and abstract classes here).

Finally, here is the server:



public class Main extends UnicastRemoteObject implements ChatInterface {
    
    private ServerList serverList = new ServerList();
    
    public Main() throws RemoteException {
    }
    
    public void join(Notify n, String name) throws RemoteException {
		serverList.add(n);
		
		serverList.incCounter();
		for (Iterator i = serverList.getCollection().iterator();
				 i.hasNext();) {
			Notify client = (Notify)i.next();
			client.joinMessage(name);
		}
		serverList.decCounter();
    }
    
    public void talk(String name, String s)
            throws RemoteException {
		serverList.incCounter();
		for (Iterator i = serverList.getCollection().iterator();
				 i.hasNext();) {
			Notify client = (Notify)i.next();
			client.sendMessage(name,s);
		}
		serverList.decCounter();
    }
    
    public synchronized void leave(Notify n, String name) throws
    		RemoteException   {
    		... Very similar to join
    }

    public static void main(String[] args) {
         try {
			
			Main server = new Main();

			Naming.rebind("rmichat", server);
		
		}
		...
    
}

As we've pointed out above, there are significant similarities with the socket version.

8.8. More Advanced Changes

Although we can reasonably expect the changes we made to the second version will improve its efficiency, we still can't expect it to behave well with very large numbers of clients. To some extent, our example is pretty unrealistic when we talk about scaling to that extent - currently, everyone talks to everyone: if there were lots of clients you would have to expand the application to include some kind of `room' concept. However, we're not going to build any actual code: just talk about what you would need to do. (Note though the code to do this is not actually that complex - probably 3-4 times the length of our second RMI example. However, I thought that by now you probably feel you've looked at enough RMI code.) What's more, we're going to talk in rather general terms, about non-specific RMI servers dealing with large numbers of clients.

In addition to the changes we made in version 2, we might want to consider thread pools - collections of shared, reusuable, threads - and server farms - multiple machines.

8.8.1. Thread Pools

The idea of a thread pool - commonly used in, for example, web servers - is that the cost of creating and destroying threads (which costs resources), we would maintain a `pool' of running threads. When a client joins, rather than creating a new thread, it is assigned an already-running idle one (assuming there is an idle one of course); when it leaves, it returns the thread to the pool, rather than destroying it.

This concept entails using explicit threads - like the socket example, and not implicit ones - like all our RMI examples so far. However, this is not a particularly difficult change to make. To modify our chat example, we would change the join method. Instead of just adding a callback object to some collection object (e.g. an ArrayList), it creates a new thread, stores that in the collection, and returns a reference to it back to the client. This thread then handles all communication with that client. There would, of course, be some other changes to the code but they're not that major.

To actually implement a thread pool, we would need to do something like this:

The idea of using four threads for this - one to hand them out to clients, register requests for new ones, and register return requests; one to actually create new threads; one to actually return threads; and one to remove surplus, unused, ones - might seem excessive. While you can make a thread pool work with less, you need to remember the point: to maximize the responsiveness of the main thread dealing with clients requests. By minimizing the work done in this thread, and handing less urgent tasks off to other threads, we can do this.

8.8.2. Server Farms and Activation

The next stage in scaling an application is to run it on multiple servers simultaneously. The process is in some ways similar to the thread pool case: the initial request from the client is handled by an intermediate server, that hands off the request. The only real obvious difference is that in the case of a server farm, the server that ultimately gets to handle the client is on some other machine. There are a number of practical issues, that we're not going into in any detail. One of which is: we don't really want lots of servers sitting idle on multiple machines. Instead, we want them started on demand. On the face of it, RMI can't do that. So far, we've only looked at the case where we start a server and then wait for incoming requests. Even in the case when the initial client contact is handled by some intermediate server, it would seem that the actual server would need to be running all the time to process requests passed from the intermediate server.

There is, however, a mechanism for handling this known as Activation, which enables RMI to start up remote servers - in fact, complete JVMs. Instead of extending UnicastRemoteObject, activatable servers extend the Activatable class - although as with simple RMI, there are alternatives. They also have substantially more complex launch code, and we're not going to discuss the details here.

Previous Contents Next