Book Home Java Distributed Computing Search this book

6.4. Fixed Protocols

If we recall our definition of a message as an identifier followed by a set of arguments, we can break down the possible message protocols into fixed and adaptable types. In this section we'll discuss fixed protocols, where the set of possible identifiers and the arguments for each type of message are known beforehand and don't change during a communication session. Adaptable protocols have variable argument lists on messages, or variable sets of message types, or both.

Let's return to the chess-player agents that we mentioned earlier and define a fixed protocol that they could use to engage in a game of chess. We'll define a protocol that will let them pass moves back and forth, confirm each other's moves, and concede a game. Then we'll implement this message protocol using our BasicMessage and BasicMsgHandler classes.

Figure 6-1 shows the architecture of the chess-playing system we'll be building in the following sections. On each player's host computer, a ChessPlayer object keeps track of the current board layout and comes up with the player's next move. A ChessServer object handles all of the communication with the remote opponent; it packages up moves from the local player into messages, and ships them off to the opponent's ChessServer. (It also takes messages from the remote opponent and calls the required methods on the local ChessPlayer.)

figure

Figure 6-1. Chess system architecture

Before we define the protocol that the distributed chess system will use, let's put together the application-level classes that act as our chess players. The ChessPlayer class in Example 6-3 demonstrates the interface to our chess player agents. The ChessPlayer maintains the state of the chess board internally. (We don't show the details of the data structures here, since they're not directly relevant to the topic at hand.) The methods defined on the ChessPlayer interface provide the means for telling the chess player the opposing player's moves, and asking the chess player for its moves.

The acceptMove() method is called on a ChessPlayer when a move from the opposing player has been received. The requested move is given to the chess player to be confirmed as valid against the current state of the board. Game moves are represented as a "from" position, a "to" position, and a flag indicating whether the move results in a "check," a "checkmate," or neither. The "from" and "to" positions are represented as strings, such as "K3" for "King's 3," "R4" for "Rook's 4," etc. The nextMove() method asks the chess player for its next move based on the current board position. The move is returned as the value of the reference arguments. The generated move will not be applied to the game board until the moveAccepted() method is called. This indicates that the opposing player has accepted the move and applied it to its copy of the game board. These three methods are used by the two players to submit and confirm each other's moves during a game. Calling a player's concede() method tells it that the opponent has conceded the game.

We've designed this application object independently of the fact that we're planning on using message passing. We could be using any communication scheme at all to pass moves between two ChessPlayers. We could even create two ChessPlayer objects within one process and engage in a game by calling methods on each of them in turn.

Example 6-3. A Chess Player Agent

package dcj.examples.messageV1;

public class ChessPlayer {
  // Data structures for maintaining chess board state
  // ...            

  public static final int CHECK = 0;
  public static final int CHECKMATE = 1;
  public static final int NO_CHECK = 2;

  public ChessPlayer() {
    // Initialize chess board
  }

  public boolean acceptMove(String from, String to,
                            int checkOrMate) {
    // Check validity of requested move.
    // If valid, apply to chess board and return true.
    // If invalid, return false.
    // ...            
    return true;
  }

  public boolean nextMove(String from, String to,
                          int checkOrMate) {
    // Generate our next move based on the current 
    // state of the game board...
    // ...            
    return true;
  }

  public void moveAccepted(String from, String to,
                           int checkOrMate) {
    // Our move was accepted as valid, apply it to the 
    // game board...
    // ...            
  }

  public void conceded() {
    // We've won!
  }
}

Now that we've defined the agents that will be playing the chess game, we can define the message protocol they'll use. First, they'll need a message to pass moves back and forth:

  1. movefromtocheckOrMate

    The message identifier is "move," and it contains three arguments. The first is the "from" position (a string), the second is the "to" position (a string), and the third is the integer flag indicating check, checkmate, or neither.

Next, they will need messages to confirm or reject each other's moves:

  1. confirmfromtocheckFlag

    The arguments indicate the particular move that was accepted by the opponent.

  2. reject

    The last move was rejected as invalid by the opponent.

Finally, they need a message to use when a game is being conceded:

  1. concede

    The opposing player is conceding the game.

The next step is to define the link from these messages to our chess player agents and their corresponding methods. Using our BasicMessage and BasicMsgHandler classes, we first need to define subclasses of BasicMessage corresponding to each of the message types in our protocol. Then we need to extend the BasicMsgHandler class to implement a chess server, which will convert incoming messages into corresponding BasicMessage subclasses and call their Do() methods.

Example 6-4 shows the subclasses of BasicMessage corresponding to the message types in our chess-playing protocol. Each of the message objects will need access to the local chess player object in order to translate an incoming message into the appropriate method call on the chess player agent. The ChessMessage class acts as a base class for our message objects, providing a reference to a ChessPlayer. Now we derive a class for each type of message in our chess protocol. Each of the message objects shown in Example 6-4 can be used for both processing an incoming message of a given type and generating an outgoing message of the same type. Each has a pair of constructors: one accepts a ChessPlayer object and a list of arguments for the message, and one just accepts a list of the message's arguments. The former is used when an incoming message is being processed, and a call to a method on the ChessPlayer will be required. The latter is used for generating outgoing messages, where the local ChessPlayer object is not necessary.

Example 6-4. Messages in a Chess Protocol

package dcj.examples.messageV1;

import java.io.*;

abstract class ChessMessage extends BasicMessage {
  protected ChessPlayer player;

  public ChessMessage(ChessPlayer p) { player = p; }
  public ChessMessage()              { player = null; }
}

class MoveMessage extends ChessMessage {
  public MoveMessage(ChessPlayer p) {
    super(p);
    setId("move");
  }

  public MoveMessage(String from, String to, int checkFlag) {
    setId("move");
    addArg(from);
    addArg(to);
    addArg(Integer.toString(checkFlag));
  }

  public boolean Do() {
    boolean success = true;
    BasicMsgHandler handler = BasicMsgHandler.current;

    String from = (String)argList.elementAt(0);
    String to = (String)argList.elementAt(1);
    String checkStr = (String)argList.elementAt(2);
    int checkFlag = Integer.valueOf(checkStr).intValue();

    try {
      if (!player.acceptMove(from, to, checkFlag)) {
        handler.sendMsg(new RejectMoveMessage());
      }
      else {
        ConfirmMoveMessage cmmsg =
          new ConfirmMoveMessage(from, to, checkFlag);
        handler.sendMsg(cmmsg);

        // We accepted the opponent's move, now send them
        // our countermove, unless they just mated us...
        if (checkFlag == ChessPlayer.CHECKMATE) {
          ConcedeMessage cmsg = new ConcedeMessage();
          handler.sendMsg(cmsg);
        }
        else {
          player.nextMove(from, to, checkFlag);
          MoveMessage mmsg = new MoveMessage(from, to, checkFlag);
          handler.sendMsg(mmsg);
        }
      }
    }
    catch (IOException e) {
      success = false;
    }
    return success;
  }
}

class ConfirmMoveMessage extends ChessMessage {
  public ConfirmMoveMessage(String from, String to, int checkFlag) {
    setId("confirm");
    addArg(from);
    addArg(to);
    addArg(Integer.toString(checkFlag));
  }

  public ConfirmMoveMessage(ChessPlayer p) {
    super(p);
    setId("confirm");
  }

  public boolean Do() {
    boolean success = true;

    // Opponent accepted our last move, so record it on our
    // copy of the game board.
    String from = (String)argList.elementAt(0);
    String to = (String)argList.elementAt(1);
    String cmateStr = (String)argList.elementAt(2);
    int checkOrMate = Integer.valueOf(cmateStr).intValue();
    player.moveAccepted(from, to, checkOrMate);
    return success;
  }
}

class RejectMoveMessage extends ChessMessage {
  public RejectMoveMessage() {
    setId("reject");
  }

  public RejectMoveMessage(ChessPlayer p) {
    super(p);
    setId("reject");
  }

  public boolean Do() {
    boolean success = true;
    String newFrom = "";
    String newTo = "";
    int newCheckFlag = ChessPlayer.NO_CHECK;
    BasicMsgHandler handler = BasicMsgHandler.current;

    try {
      if (player.nextMove(newFrom, newTo, newCheckFlag)) {
        MoveMessage mmsg =
          new MoveMessage(newFrom, newTo, newCheckFlag);
        handler.sendMsg(mmsg);
      }
      else {
        // Our player didn't come up with another move, so
        // concede the game
        handler.sendMsg(new ConcedeMessage());
      }
    }
    catch (IOException e) {
      success = false;
    }

    return success;
  }
}

class ConcedeMessage extends ChessMessage {
  public ConcedeMessage() {
    setId("concede");
  }

  public ConcedeMessage(ChessPlayer p) {
    super(p);
    setId("concede");
  }

  public boolean Do() {
    player.conceded();
    return true;
  }
}

These message classes are used by the ChessServer class in Example 6-5 to convert incoming messages into method calls on the local ChessPlayer, and to generate outgoing messages for the remote chess player. A ChessServer is constructed with an input and output stream connecting it to the remote opponent. The ChessServer makes a new ChessPlayer in its constructor to act as the local player. The ChessServer's buildMessage() method checks each incoming message identifier and constructs the appropriate message object for each, passing the local ChessPlayer reference into each constructor. When the message's Do() method is called in the implementation of run() inherited from BasicMsgHandler, the message arguments, if any, will be parsed, and the appropriate method will be called on the local ChessPlayer.

Example 6-5. A Chess Server

package dcj.examples.messageV1;

import java.io.*;

public class ChessServer extends BasicMsgHandler
{
  ChessPlayer player;

  public ChessServer(InputStream in, OutputStream out) {
    super(in, out);
    player = new ChessPlayer();
  }

  public ChessPlayer getPlayer() {
    return player;
  }

  protected BasicMessage buildMessage(String msgId) {
    BasicMessage msg = null;

    System.out.println("Got message type \"" + msgId + "\"");

    if (msgId.compareTo("move") == 0) {
      msg = new MoveMessage(player);
    }
    else if (msgId.compareTo("confirm") == 0) {
      msg = new ConfirmMoveMessage(player);
    }
    else if (msgId.compareTo("reject") == 0) {
      msg = new RejectMoveMessage(player);
    }
    else if (msgId.compareTo("concede") == 0) {
      msg = new ConcedeMessage(player);
    }

    return msg;
  }
}

To see the chess message protocol in action, let's walk through a hypothetical game played between two players on the network. First, the two processes containing the player objects need to establish a socket connection with corresponding input/output streams. (We won't show the details of this, since we've already seen some examples of creating socket connections, and there's nothing new or exciting about this one.) Once the socket connection is made, each player process passes the input and output streams from the socket to the constructor for a ChessServer. The ChessServer constructor passes the input and output streams to the BasicMsgHandler constructor, then creates a local ChessPlayer object. One of the player processes (let's call it the "white" player) starts the game by requesting a move from the ChessPlayer, wraps the move into a MoveMessage object, and tells the ChessServer to send the message by calling its sendMsg() method with the message object. A section of code like the following is used:

// Create the server and get the local player object
ChessServer server = new ChessServer(ins, outs);
ChessPlayer player = server.getPlayer();

// Get the player's first move, and generate a move message
String from, to;
int checkFlag;
player.nextMove(from, to, checkFlag);
MoveMessage mmsg = new MoveMessage(from, to, checkFlag);

// Send the move message to the opponent
server.sendMsg(mmsg);

The opponent player process (the "black" player) can start off by wrapping its ChessServer in a Thread and calling its run() method, causing it to enter its message-reading loop. The black player receives the move message from the white player, converts the message into a MoveMessage object, then calls the Do() method on the message object. The Do() method on MoveMessage takes the move from the white player and passes it to the black ChessPlayer through its acceptMove() method. If the black ChessPlayer accepts the move, then a ConfirmMoveMessage is constructed and returned to the white player to signal that the move has been accepted. The white player's ChessServer will receive the confirmation message, and the Do() method on the ConfirmMoveMessage object will tell the white player that its last move was accepted. If the white player's move was a checkmate, then a ConcedeMessage is also constructed and sent to the white player. If not, then the black player is asked for its countermove, and it's sent as a MoveMessage to the white player. The black player's ChessServer then waits for a confirmation or rejection of the move from the white player.

If the white player's first move was not accepted by the black player, then a RejectMoveMessage is constructed and sent to the white player. The white player's ChessServer receives the rejection message, converts it into a local RejectMoveMessage object, and the message's Do() method asks the white player for another move. If a new move is given, it is wrapped in a MoveMessage object and sent back to the black player. If not, this is taken as a concession of the game, and a ConcedeMessage object is constructed to send a concede message to the black player.

This message passing continues until one of the players receives and accepts a checkmate move and concedes the game, or until one of the players fails to generate a countermove, which acts as a forfeit of the game.

6.4.1. Heterogeneous Argument Lists

This message-passing example was kept simple by avoiding some of the common issues that arise even with fixed message protocols. The messages in the chess protocol consist only of string tokens, delimited by a set of special characters. This allowed us to define a single readMsg() method on our BasicMsgHandler class that we could reuse in our chess game example. It also allowed us to represent all message arguments using a list of strings in our BasicMessage class. If we know that every message is a sequence of strings ending with a special "end-of-message" string, then we can read and store each message from the input stream in the same way, without knowing what type of message was being read. This is just what the readMsg() method does--after checking the message identifier in the buildMessage() method to see which message object to create, readMsg() reads each message from the input stream the same way:

while (!msgEnd) {
    token = din.readUTF();
    if (token.compareTo(msgEndToken) == 0)
        msgEnd = true;
    else {
        msg.addArg(token);
    }
}

Typically, we can't be this simplistic, since messages may need to contain data of various types. Actually, we didn't completely escape this issue; notice that the "move" message has an argument (the "checkFlag" argument) that is supposed to be an integer, not a string. We got around the limitation of our message-passing facility by converting the integer to a string on sending the message, and then converting back to an integer on the receiving end.

In order to add the ability to send and receive heterogeneous argument lists on messages, we would need to update our message-passing facility so that each message class reads and converts its own arguments from the input stream. Another option would be to have BasicMsgHandler convert the arguments to their proper types in its readMsg() method. This could be done by having the readMsg() method know the format of all of the message types the BasicMsg-Handler supports. This would put the entire protocol definition in the BasicMsgHandler class, which makes updating the message protocol more difficult. Overall, having the message objects parse their own arguments leaves our message-passing facility more flexible for future changes.

Example 6-6 shows a new version of the BasicMessage class that handles heterogeneous argument lists. The argument list is still implemented using a Vector, but the Vector now contains references to Objects rather than Strings. Message arguments are offered and accepted by the new BasicMessage class as Objects as well.

Example 6-6. Updated Basic Message Class

package dcj.examples.messageV2;

import java.util.Vector;
import java.io.*;

abstract class BasicMessage
{
  protected String id;
  protected Vector argList;
  String endToken = "END";

  public BasicMessage() {
    argList = new Vector();
  }

  public BasicMessage(String mid) {
    id = mid;
    argList = new Vector();
  }

  protected void setId(String mid) {
    id = mid;
  }

  public void addArg(Object arg) {
    argList.addElement(arg);
  }

  public String messageID() {
    return id;
  }

  public Vector argList() {
    Vector listCopy = (Vector)argList.clone();
    return listCopy;
  }

  public boolean readArgs(InputStream ins) {
    boolean success = true;
    DataInputStream din = new DataInputStream(ins);

    // Read tokens until the "end-of-message" token is seen.
    try {
      String token = din.readUTF();
      while (token.compareTo(endToken) != 0) {
        addArg(token);
        token = din.readUTF();
      }
    }
    catch (IOException e) {
      // Failed to read complete argument list.
      success = false;
    }
    return success;
  }

  public boolean writeArgs(OutputStream outs) {
    int len = argList.size();
    boolean success = true;
    DataOutputStream dout = new DataOutputStream(outs);

    // Write each argument in order
    try {
      for (int i = 0; i < len; i++) {
        String arg = (String)argList.elementAt(i);
        dout.writeUTF(arg);
      }

      // Finish with the end-of-message token
      dout.writeUTF(endToken);
    }
    catch (IOException e) {
      success = false;
    }
    return success;
  }

  public abstract String Do();
}

To allow message objects to parse their own arguments, BasicMessage has two additional methods: readArgs() and writeArgs(). The readArgs() method takes an InputStream as its only argument, and is meant to read the arguments for the message from the InputStream. The default implementation of readArgs() provided on the BasicMessage class is stolen from the readMsg() method from the original BasicMsgHandler class; it treats the incoming message as a sequence of string tokens ending with a known "end-of-message" token. The writeArgs() method takes an OutputStream as an argument, and writes the message arguments to the output stream. The default implementation is copied from the sendMsg() method from the original BasicMsgHandler; it converts each argument to a String and writes it to the output stream. The "end-of-message" token is sent after the message arguments to mark the end of the message.

The message classes for our chess protocol need to be updated to match the new BasicMessage class. The most significant changes are to the MoveMessage and ConfirmMoveMessage classes, since they now need to provide their own implementations of the readArgs() and writeArgs() methods. Example 6-7 shows the updated MoveMessage class. Its readArgs() method reads the arguments defining the chess move ( from and to strings, and a check/checkmate flag) from the InputStream, and its writeArgs() method writes the same arguments to the OutputStream.

Example 6-7. Updated MoveMessage Class

class MoveMessage extends ChessMessage {
  public MoveMessage(ChessPlayer p) {
    super(p);
    setId("move");
  }

  public MoveMessage(String from, String to, int checkFlag) {
    setId("move");
    addArg(from);
    addArg(to);
    addArg(new Integer(checkFlag));
  }

  public boolean Do() {
    boolean success = true;
    BasicMsgHandler handler = BasicMsgHandler.current;

    String from = (String)argList.elementAt(0);
    String to = (String)argList.elementAt(1);
    Integer checkInt = (Integer)argList.elementAt(2);
    int checkFlag = checkInt.intValue();

    try {
      if (!player.acceptMove(from, to, checkFlag)) {
        handler.sendMsg(new RejectMoveMessage());
      }
      else {
        ConfirmMoveMessage ccmsg =
          new ConfirmMoveMessage(from, to, checkFlag);
        handler.sendMsg(ccmsg);

        // We accepted the opponent's move, now send them
        // our countermove, unless they just mated us...
        if (checkFlag == ChessPlayer.CHECKMATE) {
          ConcedeMessage cmsg = new ConcedeMessage();
          handler.sendMsg(cmsg);
        }
        else {
          player.nextMove(from, to, checkFlag);
          MoveMessage mmsg = new MoveMessage(from, to, checkFlag);
          handler.sendMsg(mmsg);
        }
      }
    }
    catch (IOException e) {
      success = false;
    }
    return success;
  }

  public boolean readArgs(InputStream ins) {
    boolean success = true;

    DataInputStream din = new DataInputStream(ins);

    try {
      String from = din.readUTF();
      addArg(from);
      String to = din.readUTF();
      addArg(to);
      int checkFlag = din.readInt();
      addArg(new Integer(checkFlag));

      // Got all of our arguments, now watch for the
      // end-of-message token
      String temp = din.readUTF();
      while (temp.compareTo(endToken) != 0) {
        temp = din.readUTF();
      }
    }
    catch (Exception e) {
      success = false;
    }

    return success;
  }

  public boolean writeArgs(OutputStream outs) {
    boolean success = true;
    DataOutputStream dout = new DataOutputStream(outs);

    String from = (String) argList.elementAt(0);
    String to = (String)argList.elementAt(1);
    Integer tmpInt = (Integer)argList.elementAt(2);
    int checkFlag = tmpInt.intValue();

    try {
      dout.writeUTF(from);
      dout.writeUTF(to);
      dout.writeInt(checkFlag);
      dout.writeUTF(endToken);
    }
    catch (IOException e) {
      success = false;
    }
    return success;
  }
}

The only change required to the BasicMsgHandler class is to update its readMsg() and sendMsg() methods to delegate the reading and writing of arguments to the message objects they create:

public BasicMessage readMsg() throws IOException {
    BasicMessage msg;
    String token;
    DataInputStream din = new DataInputStream(msgIn);

    // Get message ID and build corresponding BasicMessage
    token = din.readUTF();
    msg = buildMessage(token);

    // Tell message to read its args
    if (msg != null && msg.readArgs(msgIn))
      return msg;
    else
      return null;
}
public void sendMsg(BasicMessage msg) throws IOException {
    boolean success = true;
    DataOutputStream dout = new DataOutputStream(msgOut);
    // Send message ID
    dout.writeUTF(msg.messageID());
    // Tell message to send its arguments
    msg.writeArgs(msgOut);
}

6.4.2. Objects as Message Arguments

With this new version of our message-passing facility, we can define message types that have arguments of any data type that can be transmitted over an I/O stream. We can even use the object serialization facility built into the Java I/O package to use objects as message arguments.

Suppose we define an object to represent a chess move in our chess protocol example. Example 6-8 shows a ChessMove class that encapsulates in a single object the from, to, and checkFlag arguments corresponding to a chess move. We can easily alter our MoveMessage and ConfirmMoveMessage classes to use a ChessMove object as its single message argument. The updated MoveMessage class is shown in Example 6-9. The readArgs() and writeArgs() methods now use ObjectInputStreams and ObjectOutputStreams to read and write the ChessMove argument over the network.

Example 6-8. A ChessMove Class

package dcj.examples.messageV2;

class ChessMove {
  String fromPos;
  String toPos;
  int checkFlag;

  public ChessMove(String from, String to, int ckFlag) {
    fromPos = from;
    toPos = to;
    checkFlag = ckFlag;
  }

  public String from() {
    return fromPos;
  }

  public String to() {
    return toPos;
  }
 
  public int checkFlag() {
    return checkFlag;
  }
}

Example 6-9. A MoveMessage Class with Object Argument

class MoveMessage extends ChessMessage {
  public MoveMessage(ChessPlayer p) {
    super(p);
    setId("move");
  }

  public MoveMessage(String from, String to, int checkFlag) {
    setId("move");
    ChessMove move = new ChessMove(from, to, checkFlag);
    addArg(move);
  }

  public String Do() {
    BasicMsgHandler handler = BasicMsgHandler.current;
    ChessMove move = (ChessMove)argList.elementAt(0);

    if (!player.acceptMove(move.from(), move.to(), move.checkFlag())) {
      handler.sendMsg(new RejectMoveMessage());
    }
    else {
      ConfirmMoveMessage ccmsg =
        new ConfirmMoveMessage(move.from(), move.to(), move.checkFlag());
      handler.sendMsg(ccmsg);

      // We accepted the opponent's move, now send them
      // our countermove, unless they just mated us...
      if (checkFlag == ChessPlayer.CHECKMATE) {
        ConcedeMessage cmsg = new ConcedeMessage();
        handler.sendMsg(cmsg);
      }
      else {
        String from, to;
        int checkFlag;
        player.nextMove(from, to, checkFlag);
        MoveMessage mmsg = new MoveMessage(from, to, checkFlag);
        handler.sendMsg(mmsg);
      }
    }
  }

  public boolean readArgs(InputStream ins) {
    boolean success = true;

    DataInputStream din = new DataInputStream(ins);
    ObjectInputStream oin = new ObjectInputStream(ins);

    try {
      ChessMove move = (ChessMove)oin.readObject();
      addArg(move);

      // Got all of our arguments, now watch for the
      // end-of-message token
      String temp = din.readUTF();
      while (temp.compareTo(endToken) != 0) {
        temp = din.readUTF();
      }
    }
    catch (Exception e) {
      success = false;
    }

    return success;
  }


  public boolean writeArgs(OutputStream outs) {
    boolean success = true;
    DataOutputStream dout = new DataOutputStream(outs);
    ObjectOutputStream oout = new ObjectOutputStream(outs);
    ChessMove move = (ChessMove)argList.elementAt(0);

    try {
      oout.writeObject(move);
      dout.writeUTF(endToken);
    }
    catch (IOException e) {
      success = false;
    }
    return success;
  }
}


Library Navigation Links

Copyright © 2001 O'Reilly & Associates. All rights reserved.