6.4. Fixed ProtocolsIf 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 6-1. Chess system architectureBefore 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
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:
Next, they will need messages to confirm or reject each other's moves:
Finally, they need a message to use when a game is being conceded:
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
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
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 ListsThis 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:
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
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
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:
6.4.2. Objects as Message ArgumentsWith 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
Example 6-9. A MoveMessage Class with Object Argument
![]() Copyright © 2001 O'Reilly & Associates. All rights reserved. |
|
|