Skip to main content

By clicking Submit, you agree to the developerWorks terms of use.

The first time you sign into developerWorks, a profile is created for you. Select information in your developerWorks profile is displayed to the public, but you may edit the information at any time. Your first name, last name (unless you choose to hide them), and display name will accompany the content that you post.

All information submitted is secure.

  • Close [x]

The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerworks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

By clicking Submit, you agree to the developerWorks terms of use.

All information submitted is secure.

  • Close [x]

Take command of your client/server apps

This command-based approach will boost parallel processing and performance

Barry Feigenbaum, Ph.D., Sr. Consulting IT Architect, IBM, Software Group
Dr. Feigenbaum has over 20 years of experience developing operating systems and complex applications. He has served as a consultant specializing in e-business enabling technologies such as Java, HTML, servlets, JSPs and EJBs. He is an expert in object-oriented analysis, design and development, and is well-versed on the latest trends in client/server and n-tier architectures. He has also published several books and articles and represented IBM at technical conferences (such as Sun's JavaOne) and trade shows (such as IBM's WebSphere).

Summary:  The Java programming environment offers several methods for you to implement client/server (C/S) conversational applications. This article describes an approach that lets you exchange message objects between the client and the server. The server interprets these messages and builds reply objects to send back to the client. Barry Feigenbaum developed this simple, lightweight approach that allows for the dynamic addition of new message types and provides for a high degree of parallel processing, enabling significant performance tuning.

Date:  12 Jun 2001
Level:  Intermediate

Activity:  4068 views
Comments:  

The Java language is an excellent choice for developing multitier client/server applications. It supports several approaches to managing client-to-server communication, such as queued messaging, RPC, RMI, CORBA, and EJB technology (see "Alternative client/server implementation techniques" for more information). There are times, however, when these techniques are overkill, and a simpler message-passing approach is more appropriate. The Java language, with its easy access to TCP/IP-based sockets and its ability to stream objects over them, makes it easy to exchange command messages between clients and their associated servers

In a remote-objects approach, you must invoke a method of a server object. In a message-passing approach, message (or command) objects are exchanged between the client and the server. The server then interprets the messages and, if required, builds reply objects to send back to the client. (See Figure 1.) In this context the server does not appear to the client as an object, but instead as a command interpreter of indeterminate form. The parameters of the command, if any, are the instance variables of the message object.


Figure 1. Client/server command relationship
Image: Client/Server Command Relationship

In this article, I will show you how to implement the message-passing approach. I'll explain how to construct command objects and describe how they are exchanged between the client and the server and subsequently processed by the server. Unlike the message-queueing approach, this approach is conversational or bi-directional (versus one-way). Conversation is synchronous (the reply comes immediately), while the alternative is asynchronous (the reply comes in the future, if at all).

Can I see your ID?

What is the nature of the command objects exchanged between the clients and servers? In general they consist of a message identifier (or command code) and some parameters.

The most important characteristic is the command identifier. Basically, you can implement command identification in two ways: 1) use a command code; or 2) use the type (or class) of the command object. In traditional message-passing systems, you use a code, often an integer value or a short string. With the Java language's ability to test the type of an object (the instanceof operator), the type can be used as well. I will show you how to use either approach. To facilitate this, you can expect each command object to contain at least the following instance variables:

abstract public class ServerCommand 
  implements java.io.Serializable,  java.lang.Cloneable {
 // messageId is redundant with type but faster to test than instanceof
  protected transient   int  uniqueId;            // unique number
  public                int  messageId;           // identifies message
  public                int  version = 0;         // for extension
  public                int  sequenceIn = 0;      // unique in a session
  public                int  sequenceOut = -1;    // used in replies
}

Use the uniqueId value, assigned by the constructor, to distinguish any command from another in a single Java virtual machine (similar to the object ID of the command but human readable). You can use the values to make the objects unique in any traces, but it's not needed to support the processing of messages.

The messageId is the command code. Each code has a corresponding message type. For the examples in this article the following codes, and thus message types, are defined:

public interface Messages {
  int           UNASSIGNED            = 0;
  int           MESSAGE_1             = (1  << 16);
  int           MESSAGE_2             = (2  << 16);
  int           MESSAGE_N             = (3  << 16);
  int           MESSAGE_N_PART1       = (3  << 16) + 1;
  int           MESSAGE_N_PART2       = (3  << 16) + 2;
  int           STOP                  = Integer.MAX_VALUE;
}

Use the version to facilitate future expansion. You can add more fields in future command versions. To coordinate commands with replies, use the sequenceIn and sequenceOut values. Any reply to a command has its sequenceOut set to the sequenceIn value of the command. A reply may be (and often is) of the same type as the command. Commands have a sequenceOut value of -1. Beyond this common content, each message may have content required to carry out its function. You define this content by subclassing ServerCommand and adding additional instance variables. For example, type Message1 adds an int value:

final public class Message1 extends ServerCommand {
  public int             index;                  // sequence value
}


Teaching your server processing tricks

After the server receives a command, how does it know how to process it? Command processing is most directly done by creating a method in the server that processes each command type. Traditionally, the server would use a switch statement indexed on the message identifier to select the desired method. This technique requires you to change the server constantly as you define new message types. A better approach would to separate the top-level command reception and buffering support from the particular command processing.

You can achieve this separation by using a registered command listener with a callback approach. This technique is similar to the Java 1.1 AWT event model. Each command processor is a method of a class that implements a listener interface defined for that command type. For example, for the listener interface:

public interface MessageNListener extends CommandListener {
   public       void            messageN(MessageN c);
   public       void            messageNPart1(MessageNPart1 c);
   public       void            messageNPart2(MessageNPart2 c);
}

a possible command processor is:

class MessageHandler2 implements MessageNListener {
   public       void            messageN(MessageN c)
   { ... }
   public       void            messageNPart1(MessageNPart1 c)
   { ... }
   public       void            messageNPart2(MessageNPart2 c)
   { ... }
}

When the server receives a command, say a MessageN, it determines that its matching listener is MessageNListener and it finds all instances of registered listeners for it (for example, MessageHandler2). Then it invokes the appropriate callback (for example, messageN(...)) in that class to process the command. If the server finds multiple listener classes, the command is multicast to each of them.

This registration is all done dynamically. In fact, it is possible to install new types of command listeners and handlers while the server is running (by using Class.forName()). This would be quite difficult if the server employed a switch-based approach to command dispatching.


Nice threads

The server itself consists of two Java threads: one to accept commands from a client and another to send responses back to the client. The server code of these examples serves only a single client but you can easily extend it to support an indefinite number of clients. The client is represented below by an input stream of predefined commands and an output stream to accept replies.

The input thread repetitively calls processInboundMessage. The output thread repetitively calls processOutboundMessage. These loops end when you shut down the server.

public void processInboundMessage() throws Exception {
   ServerCommand   c = (ServerCommand)_inputStream.readObject();
   Class           clc = c.getListenerClass(); // who listens for it
   Enumeration     e = _commandQueues.elements(); // queues for this server
   while(e.hasMoreElements()) {                // for each queue in turn
        ServerCommandQueue cq = (ServerCommandQueue)e.nextElement();
        if(cq.listensTo(clc))          // this queue wants the command
          cq.put(_copyMode ? (ServerCommand)c.clone() : c);
   }
}
public void processOutboundMessage() throws Exception {
   ServerCommandQueue     cq =
      (ServerCommandQueue)_commandQueues.elementAt(_outboundIndex);
   ServerCommand          c = cq.get();              // get next command
   _outputStream.writeObject(c); _outputStream.flush();  // send the image
}


Taking your queues

The server processes commands by placing them into ServerCommandQueues. The code in these examples supports multiple input queues and a single output queue. You can implement additional output queues, and you can clone commands by placing them into the inbound queues, a useful approach when you're multicasting and when you're changing the command while processing it. For input commands the server searches all known command queues looking for the ones that accept commands for that listener type:

public synchronized boolean listensTo(Class c) {
    boolean            result = false;      // assume not found
    Enumeration        e = _listeners.elements(); // listeners for this queue
    while(!result && e.hasMoreElements()) {  // try c against all listeners
      CommandListener cl = (CommandListener)e.nextElement();
      if(cl != null && c.isInstance(cl))  // listener is correct for command
        result = true;
    }
    return result;
}

To determine which command queue to place the command into, each ServerCommand class is responsible for returning the listener class that responds to it. This class is used to associate the message with the queue (as shown above). For example, the MessageN class provides this information:

public Class getListenerClass() throws ClassNotFoundException {
    return Class.forName("commanddispatcher.MessageNListener");
}

It is the job of the server command queue to deliver the command to the appropriate server callback. Each queue has a thread that removes entries placed on it. The input queues dispatch the commands:

protected void dispatchListeners(ServerCommand c) throws Exception {
  if(c != null && hasListeners()) { // some command to process
    Class          clc = c.getListenerClass(); // whom listens
    if(clc != null) { // some defined listener
       Enumeration    e = _listeners.elements(); // registered listeners
       while(e.hasMoreElements()) {  // try each listener (ie multicast)
          CommandListener cl = (CommandListener)e.nextElement();
          if(cl != null && clc.isInstance(cl)) { // listener is correct
            ServerCommand tc = _copyMode ? (ServerCommand)c.clone() : c;
            tc.invoke(cl); // go do call back
          }
      }
    }
  }
}

You invoke the callback by the invoke method of the received command class. The invoke method then delegates to the appropriate implementation method. For example, class MessageN has the method:

public void invoke(CommandListener cl) {
    ((MessageNListener)cl).messageN(this);
}

Because the command itself provides the code to invoke the callback, the base server and command queue code is ignorant of the command types. This is how you can add more commands to the system dynamically.


Getting your priorities straight

Multiple command queues allow you to process different command types at different priorities, allowing you to implement priority categories such as idle, normal, and critical. The different threads of the server and the command queues can run at different priorities, which gives you a very fine control on the relative rates of receiving, processing, and responding to commands.

Consider the example of command handlers (inner classes of ServerDemo) shown in Listing 1.

The delay method just spins, eating valuable CPU time. This represents the time used by a true command processor in a real server. The shutdown, message1, and messageNPart2 callbacks send responses.

The involved interactions described above are condensed in Figure 2. The sequence is:

  1. A command comes in from the client and is handled by the server's input thread.

  2. The command is placed in a inbound command queue.

  3. The command queue's processing thread invokes the command's registered callback.

  4. The callback generates a response, places it in the outbound command queue, and returns.

  5. The server's output thread gets the response from the outbound queue.

  6. The server responds to the client.

Figure 2. Command processing flow
Image: Command Processing Flow

Each command queue has its own direction, capacity, and priority. The server has flow-tracing code to indicate where a message (or action) comes from (such as the "main" lines in the trace shown in Listing 3). This trace is driven by the example command stream shown in this code snippet, which, when executed with the server configuration shown in Listing 2, outputs the trace shown in Listing 3.

In the tracing output:

  • The "inbound xxxx" lines indicate the client has generated a command.
  • The "read inbound command" lines indicate the server is waiting for the next command to arrive.
  • The "process command: xxxx" lines indicate that the server input thread has received the command and will deliver it to a command queue.
  • The "xxxx event" lines indicate the beginning of the processing of a command (that is, the callback is running).
  • The corresponding "xxxx done" lines indicate the server has processed the command.
  • The "write command: xxxx" lines indicate the server is sending a response.
  • The "get output command" lines indicate the server is waiting for the next reply to send.
  • The "outbound xxxx" lines indicate the client has received a response.

I have defined an example client (in the main method of class ServerDemo) with the following instance variables and code sequence:

ServerCommand[]             commands = {                    // test commands
                              new Message1(1),     new Message2(),
                              new MessageN(),      new MessageNPart1(),
                              new MessageNPart2(), new MessageN(),
                              new Message2(),      new Message1(8),
                              new Message2(),      new Message1(10)
                                       };
for(int j = 0; j < 3; j++) {              // do several times
    try { Thread.sleep(j * 10); } catch(InterruptedException ie) {} // delay
    for(int i = 0; i < commands.length; i++) {
       send(commands[i]);  // fake a client generated command
       try { Thread.sleep((commands.length - i) * (j + 1)); } // delay
       catch(InterruptedException ie) {}
    }
    send(new Stop());                     // end it all
}

Note that all the client-generated commands are not processed. When the system receives the Stop command, it shuts down and thus ignores subsequent commands in progress or those not yet received. Also note that the servers don't always process commands in the order they are received and that multiple commands are executed concurrently. This is the result of having multiple command queues with different priorities. Servers that receive commands in a single command queue process them in arrival order. The responses have the sequenceOut value set to the sequenceIn value of the corresponding command.

Going beyond the Java language

The solution demonstrated in this article is only for clients and servers based on the Java language. To support other languages, you must use the interface (versus Serializable). You can create externalized objects in any form you desire. To represent the commands when serializing them, you must develop a public protocol (for example, a data stream). The easiest way to design the protocol is to make sure the commands do not reference other object types (beyond simple forms such as String and Date, which are easily exported). Thus, you can transmit the commands in a Java language-independent way, similar in fashion to using Interface Definition Language (IDL) under CORBA.

The server would then need a Map (for example, a Hashtable) structure to convert message codes to the appropriate server command class to process them (MESSAGE_N -> MessageN). Each such class would have a method (for example, ServerCommand makeFrom(byte[] image)) that converts a command image to a command object. A reverse method (for example, byte[] flatten()) is required in each command class to send replies back to the client.


Conclusion

In this article, I have described an approach to developing a command-based server that allows for the separation of the command-processing logic from the base server support. This separation allows for the dynamic addition of new command types and removes the concerns of command buffering and priority handling from the command processors themselves.

The server can consist of a single input and output command queue. This form processes all received commands sequentially. Alternatively, the server can have multiple command queues, each possibly running at different priorities, which allows for priority-based parallel command processing.

You can adjust the buffering capacity of the system. With single-entry queues, the transmission and processing of commands is lock step. With larger queues, the transmission and processing can overlap, helping you generally achieve greater command throughput. Larger queues also help when messages occasionally arrive faster than the server can process them.

You can have multiple client capability by adding multiple Server objects, each with their own input and output thread pairs, one per client. You can start these additional servers with an "accepting" thread (versus the static code in the example) that waits for connections to be established.

Up to this point, I have discussed the benefits of this approach. However, there are a few disadvantages:

  • The use of multiple threads and the command queue mechanism adds processing time overhead, especially if a lock-step (single queue) configuration is used. The overhead can be eliminated if you extract the processInboundMessage and dispatchListeners code from these methods and call them directly from the server code.

  • Using message codes requires that you change the Messages interface when you add new message types, which can reduce flexibility. Thus, I recommend the use of type-based message selection (see the message codes discussion above).

  • Extending messages (ServerCommand subclasses) with more instance variables can cause difficulties. If any extension occurs, you'll need to upgrade the message's .class file for both the client and server and then restart them. To avoid this, I recommend that you create a single message class, such as shown below:

    import java.util.*;
    abstract public class GeneralMessage extends ServerCommand {
        protected Vector      indexedItems;  // holds value indexed by an int
        protected Hashtable   namedItems;    // holds named field value
    }
    

    This class would be extended by a class of the following form:

    public class Message1 extends GeneralMessage {
        static final long serialVersionUID = ???L; // from serialver command
        protected static final String INDEX_KEY = "index";
        public         Message1(int index) {
            namedItems = new HashTable();
            setIndex(index);
        }
        public int     getIndex() {
            return ((Integer)namedItems.get(INDEX_KEY)).intValue();
        }
        public void    setIndex(int index) {
            namedItems.put(INDEX_KEY, new Integer(index));
        }
    }
    

    The serialVersionUID field is set to ensure that any future method additions do not invalidate the serialized form of the message. Use the serialver command to create this statement. Add a similar serialVersionUID line to each newly created subclass of GeneralMessage.

    Indexed types use the indexedItems field while non-indexed items use the namedItems field. You don't need to add new fields as you're adding more values, and thus this solution does not break version compatibility. If necessary, you can extend the message class with more values such as:

    public class Message1x extends Message1 {
        protected static final String NAME_KEY = "name";
        public         Message1x(int index, String name) {
            super(index);
            setName(name);
        }
        public String     getNamex() {
            return (String)namedItems.get(NAME_KEY);
        }
        public void    setName(String name) {
            namedItems.put(NAME_KEY, name);
        }
    }
    


Resources

About the author

Dr. Feigenbaum has over 20 years of experience developing operating systems and complex applications. He has served as a consultant specializing in e-business enabling technologies such as Java, HTML, servlets, JSPs and EJBs. He is an expert in object-oriented analysis, design and development, and is well-versed on the latest trends in client/server and n-tier architectures. He has also published several books and articles and represented IBM at technical conferences (such as Sun's JavaOne) and trade shows (such as IBM's WebSphere).

Report abuse help

Report abuse

Thank you. This entry has been flagged for moderator attention.


Report abuse help

Report abuse

Report abuse submission failed. Please try again later.


developerWorks: Sign in


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Select information in your developerWorks profile is displayed to the public, but you may edit the information at any time. Your first name, last name (unless you choose to hide them), and display name will accompany the content that you post.

Choose your display name

The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


Rate this article

Comments

Help: Update or add to My dW interests

What's this?

This little timesaver lets you update your My developerWorks profile with just one click! The general subject of this content (AIX and UNIX, Information Management, Lotus, Rational, Tivoli, WebSphere, Java, Linux, Open source, SOA and Web services, Web development, or XML) will be added to the interests section of your profile, if it's not there already. You only need to be logged in to My developerWorks.

And what's the point of adding your interests to your profile? That's how you find other users with the same interests as yours, and see what they're reading and contributing to the community. Your interests also help us recommend relevant developerWorks content to you.

View your My developerWorks profile

Return from help

Help: Remove from My dW interests

What's this?

Removing this interest does not alter your profile, but rather removes this piece of content from a list of all content for which you've indicated interest. In a future enhancement to My developerWorks, you'll be able to see a record of that content.

View your My developerWorks profile

Return from help

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Java technology
ArticleID=10548
ArticleTitle=Take command of your client/server apps
publish-date=06122001
author1-email=feigenba@us.ibm.com
author1-email-cc=

Tags

Help
Use the search field to find all types of content in My developerWorks with that tag.

Use the slider bar to see more or fewer tags.

For articles in technology zones (such as Java technology, Linux, Open source, XML), Popular tags shows the top tags for all technology zones. For articles in product zones (such as Info Mgmt, Rational, WebSphere), Popular tags shows the top tags for just that product zone.

For articles in technology zones (such as Java technology, Linux, Open source, XML), My tags shows your tags for all technology zones. For articles in product zones (such as Info Mgmt, Rational, WebSphere), My tags shows your tags for just that product zone.

Use the search field to find all types of content in My developerWorks with that tag. Popular tags shows the top tags for this particular content zone (for example, Java technology, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere).

Special offers