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.
The following are some of the main issues that I encountered when getting the RMI to work:
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.
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.
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.
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:
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.
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.
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.
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.