HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

We set out to build a generic framework for creating Java client/server relationships. Our hope was to encapsulate all of the messy details of the relationship, allowing developers writing a client or a server to focus just on their particular application. This would allow our team to swiftly create client/server relationships relying on robust, fully debugged classes to handle communications. We wanted a framework that was simple and easy to use, but flexible enough to handle multiple communications methods.

Motivation
It has generally been difficult to set up the communications for a new client/ server architecture. The first hurdle is to decide which of various methods to use: custom-parsed data on a socket, serialization, Remote Method Invocation (RMI), JavaSpaces, a proprietary middleware package, and so forth.

The next issue to be tackled is connection management. This involves specifying connection details such as host names and port numbers, restarting the server and reconnecting clients, graceful disconnection, reporting errors, logging and more. Much of the time these complex problems are solved in a way that's tailored to the specific client/server pair under consideration.

We had several criteria for an ideal solution to our situation. First of all, we wanted to pass serialized objects rather than some format we had to parse ourselves. This would allow us to rely on Sun's developing and maintaining our parsing code. We would also have access to books, online documentation, newsgroup help and outside experts whenever we needed assistance.

We wanted to use Java's listener event model, as it would allow for a consistent event and communications model for a program. Using this familiar model would significantly decrease the time it took a user of our framework to become familiar with it.

Support for multiple lower-level communication mechanisms was also a requirement. We already knew that our first server had to connect to Oracle, to an existing socket-based price server and to RMI-based calculation servers. We could envision other connection types ­ such as telnet, HTTP or SMTP ­ coming down the pike.

We needed our architecture to be simple and robust. We wanted a narrow API that would be easy to understand and maintain. As much as possible of the connection and event dispatching code should be in library classes so it could be coded and debugged once, then reused many times.

We also want the application itself to have enough control so it could handle errors, logging and connection operations when it needed to. At other times it could ignore these issues and let the lower-level classes handle them. This would allow our architecture to flexibly extend to future projects.

Applicability
The architecture we came up with suits our purposes quite nicely. We're developing for an equity-trading firm, and frequently create new clients for use by our traders and new servers to supply the client programs with calculation engines, data caches, trading models and so on. Subsecond response to queries is a requirement in many of our applications, since trading opportunities can appear and disappear very quickly. Servers with extensive data caching and high-performance calculation servers are also a must in our environment. Communication between these servers and our clients must be speedy as well. And we need to develop new client/server applications in a hurry to take advantage of new market conditions.

Our design is not suitable for our highest throughput jobs. For instance, we wouldn't contemplate rewriting our master price server, which handles hundreds of price updates per second, using this architecture. The overhead of serializing and deserializing objects would be too high to meet these requirements.

It's also not appropriate for multilingual situations. By this we don't mean that the United Nations couldn't use it ­ only that it doesn't talk anything but Java. It's possible, of course, to write code in another language that serializes or deserializes a Java object. But the large investment necessary to do so would negate the advantages of this architecture.

We decided that the initial trial of our architecture would be to use it to create a client/server pair, which would give our traders up-to-the-second information on stock options. We'll refer to this project several times below.

Design
Bruce Eckel convinced us of the superiority of the Java 1.1 event model in his excellent book Thinking in Java. In the paradigmatic use of the model, event handlers don't need conditional branching to determine what to do with a particular event because they rely on the Java-type system to do so.

Because we weren't handling a GUI, however, our model primary dispatch isn't on the recipient of an event. In a GUI, dispatching on the recipient makes sense ­ creating a listener to get mouse events over a button, for instance. But in our situation the source for events at the client is always the server, and on the server it's one of the clients. A "switch" on this source would make little sense. Instead, we register listeners for particular types of incoming objects. The classes that flow between the client and server become message types as well as possibly being significant application elements in their own right.

To express interest in a particular class of object, the server and the clients create listener classes and call a registration method in the library.

Error messages and protocol-level events (e.g., a new client has connected, or the server has gone down) are also represented as classes, and are received and handled in the same way as application-level objects.

Because of our use of the listener pattern, neither our library nor its users need conditional code or large switch statements to process communications. Listeners are registered with the library and placed as a value in a hashtable, a value for which the class they receive is the key.

Our architecture permits asynchronous calling of the listeners. The server can send out objects the client hasn't requested, such as updates to an object the client is viewing, in the same manner in which it sends out requested objects.

Our architecture also supports a generic monitoring program that the server can tailor to its own needs. It's easy to create both GUI and command-line monitors. They include built-in statistics gathering on the number of each class sent and received, as well as connection information such as application name, hostname and so on. To allow any particular server to tailor the monitor to its needs, the server can pass a list of monitoring commands to the monitor (see CommandList below). The monitor then puts them in a menu for easy access. Examples of commands we've implemented include shutdown, kill clients, turn server statistics on and turn them off. The monitor then periodically sends out a request for status object and receives back information on server performance and statistics. Because these monitoring facilities are built into the Channel class, you can worry about application design and not about building monitoring protocols. You can monitor your application in its early stages and not have to hack in monitoring later, as too often occurs.

Implementation
The classes we created to implement this architecture are as follows.

  • Channel: There's one Channel object per logical service provided by a server. For example, all clients of our option server clients connect to it on the same channel. The Channel is the main class through which applications connect, read and write objects, handle errors and disconnect. The Channel therefore provides methods such as connect(), send() and goodbye(). There's no read() method because all objects are received through the ReceiveListener interface. This interface has a single method called recv(ChannelMsg). For an application to receive objects it simply implements a ReceiveListener and then calls Channel.registerListener(Class,ReceiveListener). The first argument is the class type of the objects that will be passed to the listener. Example:

    public class FooListener implements ReceiveListener
    {
    public void recv(ChannelMsg msg)
    {
    Foo f = (Foo)msg.getData();
    System.out.println("Got a Foo: " + f.toString());
    }
    }

    channel.registerListener(Foo.class,
    new FooListener());

  • Device: Device is an abstract class. There's one Device per connected client, and therefore many per Channel. For example, the Channel in the option server has multiple Devices, each of which represents a connection to a different client.

Creating new lower-level mechanisms is simply a matter of implementing new Device classes. Specific applications don't care about how the communications are carried out ­ they just send and receive objects. A descendant of the Device class could even implement a disk-file­based communication mechanism. The Channel calls device methods to do the actual sending and receiving of objects, as well as the connection/disconnection tasks. The Device class uses the common open/close/read/write model. Therefore, the most important Device methods are:

public abstract void open() throws IOException;
public abstract void close();
public abstract void write(Object o) throws IOException;
public abstract Object read() throws IOException, ClassNotFoundException;

The Device abstract class doesn't define a constructor ­ the constructor will probably be different for each concrete Device, allowing parameters for that particular transport method. For example, the SockDevice uses sockets as its mechanism. Therefore the constructor takes hostname and port number parameters. (Figure 1 shows the relationship of Channel and Device objects.)

Figure 1
Figure 1:
  • SockDevice: This is the first concrete implementation of Device we created. It handles TCP/IP sockets as a transport mechanism. It uses the Java Socket classes and simply reads and writes serialized objects to and from the stream associated with the socket:

    out = getWriter(sock.getOutputStream());
    out.writeObject(o);
    out.flush();
    out.reset();

The reset() call is included because serialized objects are cached by Java. If you send the same object twice, Java won't serialize it a second time. This is problematic if you change some fields in the class between the first and second call. The second call will use the cached class and your changes won't be sent. In our Device class the call to the reset() method can be toggled off or on. If you're sending the same class and aren't changing it, it will be more efficient not to call reset().

If some parts of an object intended for serialization are useful only to the server or client, they can be declared transient and never instantiated on the side that isn't interested in them. For example, the server may have a database connection for some objects, allowing it to fill in their fields. Since they're filled in before moving to the client, the client doesn't need the connection. Declare it transient, and you get the behavior you want.

Event Classes
A small number of simple event classes for managing and monitoring connections are built into our library. When certain events occur (e.g., a new client connects to a server), and if the application has supplied a listener, the application's listener will be called when the event occurs. Some of those classes are:

  • ClientConnect: This is sent to a server application whenever a new client connects to it. The class contains an AppInfo object that lists information about the connecting client.
  • RemoteGone: This is sent to the application (client or server) whenever a remote connection disappears.
  • ServerConnected: Received by Channel clients when they have successfully connected to a server.

  • Error: Sent into the application on any kind of general error. Contains error message and exception information.
  • Goodbye: This is sent by a client to a server in an attempt to disconnect gracefully. If a client disconnects and the server doesn't receive a Goodbye object, it assumes the client has crashed and emits an Error object.
  • AppInfo: Objects of this class are exchanged when a client and server first connect. It contains information about the sender such as hostname and user name. It may also contain device-specific info in a general info field. For instance, the SockDevice stores the port number there.
  • CommandList: A server may send a CommandList to a monitoring client. This allows the monitor the ability to control the server.
  • ChannelMsg: A ChannelMsg is passed to the recv() method of all ReceiveListeners. It provides access to the object that was sent. It also has handles to the Channel in use and the current Device over which the object arrived. These are most often used to reply to the sender of the object. For example:

    recv(ChannelMsg msg)
    {
    msg.getChannel().send(msg.getDevice(), "Hi");
    }

See Figure 2 for a diagram of the foregoing classes.

Figure 2
Figure 2:

A Small Application
The ease of setting up connections is shown in the small code sample in Listing 1.

A Note on Threading
The waitForClients() call in the example server code above spawns off a thread to listen for new clients. Normally the application shouldn't return from this call.

When a new client connects, a thread for the new client is created. This new-client thread will be the one that calls the ReceiveListeners. Applications are responsible for synchronizing their domain-specific data where needed.

The client side of the Channel also uses a separate thread to listen for objects returned from the server. Listing 2 is an example client application that first waits for a server, then calls Channel.waitForMsgs(), which launches the receive object thread. The listing provides the code to send a message to the server as well as some code to reconnect if there are any errors.

Consequences
The result of our efforts has been an extremely simple, flexible architecture for Java client/server relationships. A simple client/server pair can be created in a matter of minutes. In the specific application for our trading floor we were able to create a sophisticated and useful new client/server pair, working from an existing stand-alone client, in a couple of weeks. Our architecture can handle hundreds of thousands of price updates in a day. Our server caches stock and option information for several hundred securities and relays them to multiple clients in close to real time. A sign of the success of our design is that we've had to spend very little time on the classes described above once they were completed and debugged. This simple set of methods has provided the ability to create major applications with little attention to communications issues.

Author's Bios
Gene Callahan, president of St. George Technologies, designs and implements Internet projects. He has written articles for several national and international industry publications.
He can be reached at: [email protected].

Rob Dodson is a software developer who writes options-trading software in Java and C++ for OTA Limited Partnership. Previous projects include weather analysis software, tactical programs for Navy submarines and code for electronic shelf labels.
He can be reached at: [email protected].

	

Listing 1:

Channel channel = new Channel(new SockDevice(hostname, port));
public class MyErrorListener
{
    public void recv(ChannelMsg msg)
    {
         Error e = (Error)msg.getData();
         System.err.println(e.getErrorString());
        // do something with the error!
    }
}
Channel.registerListener(Error.class, new MyErrorListener());
channel.waitForClients(true);

Listing 2:

while (true)
{
    // Wait for a server
    while (channel.getCurrentDevice().isDeviceOk() == false)
    {
        try
        {
            channel.waitForMsgs(false);
        }
        catch (Exception e)
        {
            Log.println("server not there");
            try { Thread.sleep(2000); } catch (Exception ee) {}
        }
    }
    try
    {
        channel.send(new Command("status"));
    }
    catch (Exception e)
    {
     // Problems? Go back and wait for server
        break;
    }
    if (channel.getCurrentDevice().isDeviceOk() == false) break;
}


 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.