Approach

I developed this application initially as standalone application, with separate classes for the client and the server portions. This ensured that I had a sound architecture, and permitted me to completely develop and thoroughly test the application without worrying about the potential RMI difficulties that I expected to encounter.

Deciding how to divide the application into client and server portions was fairly straightforward. The server class contained the methods for deciding what the next move should be (the meat of the application) and for creating and populating a new board. The user interface was handled by the client. The question was where to put the code to enforce the rules of the game (such as requiring that at least one bar be taken, etc.), and it became clear that putting them in the client would avoid a lot of extra crosstalk between the client and server. The final design generates very limited client/server communication; only when the game begins and when it's the computer's turn to make a move.

This server can actually serve many client applications, which would be especially appropriate if the servers' computations were more difficult or less deterministic, such as those required if the game were chess, checkers, othello, etc.

When the testing of the standalone application was finished and I started to work on the RMI portion of the task, the first thing I did was to create separate client and server .jars and put them on different machines. I didn't want to mask any issues that I might have been hidden by running from the same directory on a single machine.

Issues

The following are some of the main issues that I encountered when getting the RMI to work:

  1. throws RemoteException

    I guess it didn't sink in when I listened to the lectures, but the Impl class has to be changed to throw RemoteException (or at least just say that it does) and the client side has to catch it. Fortunately this wasn't too hard to deal with.

  2. rmiregistry access to stub and interface classes

    I think this was the most confusing and unexpected part of the whole RMI process. In my initial pass at getting RMI to work, I really wasn't interested in having the client application use dynamic loading of class stubs. I figured that I might want to try that feature after I got into it a little bit, but that in most scenarios in which I could foresee using RMI, it would be reasonable to expect the client to already have the stub classes locally. Thus, I didn't really expect to need to setup things to have this feature.

    Well, I had a rude awakening. Forget the client for minute. It turns out that you cannot even get the server to bind to the rmiregistry unless you have the server inform the rmiregistry where it can find those stubs. I expected that I wouldn't need to provide them to the rmiregistry since the client wasn't going to be requesting them, but noooo you can't even start up your server until you pony up and tell the rmiregistry where they are. Failing to do this results in all sorts of painful exceptions when the server calls rebind, including ClassNotFoundException, UnmarshalException, and so forth. And it's not enough to simply have the classes in the same directory with the server -- you have to specify a codebase property in the server as a URL where the rmiregistry can pick up these classes. The best way to avoid confusion is to place these classes in separate directory (from the directory which contains the server classes). And it's not just the stub class, but also the interface class, as well as any other classes that are passed between the client and server.

    Ultimately I omitted the stubs from the client jar file and use dynamic stub downloading, since RMI automatically downloads them as needed, and since I already went through the pain of providing them to rmiregistry.

    As a further test, I tried using a 4-host deployment, where each of the following is on a separate host: server, rmiregistry, stubs, and client. This failed, of course, because RMI requires that the server must run on the same host as the rmiregistry. Oh, well.

  3. Security and policies

    After struggling with access control exceptions when trying to start the server, I found a tip in the Core Java vol. 2 book (p. 342) that indicates that using a security manager in the server is neither necessary nor beneficial, and just compounds the problems in configuring RMI. This alleviated some of the access problems I was getting in starting the server.

    On the clinet side, the RMISecurityManager by default does not have the appropriate settings to be able to contact the rmiregistry, so you are required to supply a policy file for this. Unfortunately, I could find no way to make Java read the policy file from the client.jar file, so there is an extra step required in the deployment to extract the policy file into the filesystem from the client.jar. Sun should have at least made the default security policy of an the RMISecurityManager have the ability to connect to the rmiregistry running on its default port, 1099.

Summary

In hindsight, I think my development approach was sound (do standalone first, RMI second), and on another project of this scale I would do it this way again. On a larger project with multiple people or teams of people it probably wouldn't work to defer all of this work to the end of the project.

It was good to have hands-on experience creating and actually using the interface classes, stub classes, and Impl classes that are used in RMI. For several months I worked on a project that used CORBA to communicate between a Java client and a set of C++ servers, using a similar set of classes as RMI uses, but since I never had to work directly on those parts of the program, they were always a bit of black magic to me. This project gave me the chance to understand these pieces more fully.

After this experience, I think I would tend to avoid using RMI in any real client/server application for the following reasons:

  1. Complexity

    While the designers of Java did a good job of making it simple for developers to use network sockets, they didn't do so well with RMI. It is much easier to get client/server communication working by using sockets, especially when the protocol is simple. As I mentioned above, there are lots of things that can and do go wrong with RMI.

  2. Language-specific

    Since RMI requires both the client and server to be written in Java, its usefulness is limited in heterogenous computing environments. CORBA is not much more complex that RMI, but offers bindings to almost every language.

  3. Administration

    In a typical client/server environment, the system administrators need to administer both the client and server applications. With RMI, there are now several extra things to administer and keep running: rmiregistry, policy files, a web server, and a stub class repository.

  4. Existing code must be modified

    One thing I don't like is that you have to re-write all of the server methods to throw RemoteException. This somewhat contradicts the RMI propaganda that you can transparantly use RMI with existing interfaces. on the contrary, you have to either modify the existing code to throws RemoteException, or you have to write another class that sits between the existing class and the interface class in order to add the throws clause. Yuck. Transparent RMI seems to be a step in the right direction.