"

68 Javanotes 9.0, Section 12.5 — Network Programming Example: A Networked Game Framework

Section 12.5

Network Programming Example: A Networked Game Framework



This section presents
several programs that use
networking and threads. The common problem in each application is to support
network communication between several programs running on different computers.
A typical example of such an application is a networked game with two or
more players, but the same problem can come up in less frivolous applications
as well. The first part of this section describes a framework that
can be used for a variety of such applications, and the rest of the section
discusses three specific applications that use that framework. This is
a fairly complex example, probably the most complex
in this book. Understanding it is not essential for a basic understanding
of networking.

This section was inspired by a pair of students, Alexander Kittelberger and
Kieran Koehnlein, who wanted to write a networked poker game as a final project
in a class that I was teaching. I helped them with the network part of the
project by writing a basic framework to support communication between the players.
Since the application illustrates a variety of important ideas, I decided to
include a somewhat more advanced and general version of that framework in
this book. The final example in this section is a networked poker game.


12.5.1  The Netgame Framework

One can imagine playing many different games over the network. As far as the
network goes, all of those games have at least one thing in common: There has
to be some way for actions taken by one player to be communicated over the
network to other players. It makes good programming sense to make that
capability available in a reusable common core that can be used in many
different games. I have written such a core; it is defined by several
classes in the package netgame.common.

We have not done much with packages in this book, aside from using
built-in classes. Packages were introduced in Subsection 2.6.6,
but we have stuck to the “default package” in our programming examples.
In practice, however, packages are used in all but the simplest programming
projects to divide the code into groups of related classes. It makes particularly
good sense to define a reusable framework in a package that can be included as
a unit in a variety of projects.

Integrated development environments such as Eclipse make it very
easy to use packages: To use the netgame package in a project in an IDE, simply
copy-and-paste the entire netgame directory into the
project. Of course, since netgames use JavaFX, you need to use
an Eclipse project configured to support JavaFX, as discussed in
Section 2.6.

If you work on the command line, you should be in a working directory
that includes the netgame directory as a subdirectory.
You need to add JavaFX options to the javac
and java commands. Let’s say that you’ve defined
jfxc and jfx commands that are equivalent
to the javac and java with JavaFX options included, as discussed in
Subsection 2.6.7. Then, to compile
all the java files in the package netgame.common,
for example, you can use the following command in MacOS or Linux:

jfxc netgame/common/*.java

For Windows, you should use backslashes instead of forward slashes:

jfxc netgame\common\*.java

You will need similar commands to compile the source code for the examples in
this section, which are defined in other subpackages of netgame.

To run a main program that is defined in a package, you should again be in
a directory that contains the package as a subdirectory, and you should use the
full name of the class that you want to run. For example, the ChatRoomWindow
class, discussed later in this section, is defined in the package netgame.chat,
so you would run it with the command

jfx netgame.chat.ChatRoomWindow

The applications discussed in this section are examples of distributed
computing, since they involve several computers communicating over a network.
Like the example in Subsection 12.4.5, they use a central “server,”
or “master,” to which a number of “clients” will connect. All communication
goes through the server; a client cannot send messages directly to another
client. In this section, I will refer to the server as a hub,
in the sense of “communications hub”:

a central hub communicating with several clients

The main things that you need to understand are that: The hub must be running
before any clients are started. Clients connect to the hub and can send messages to
the hub. The hub processes all messages from clients sequentially, in the order
in which they are received. The processing can result in the hub sending messages
out to one or more clients. Each client is identified by a unique ID number.
This is a framework that can be used in a variety of applications, and the
messages and processing will be defined by the particular application.
Here are some of the details…

In Subsection 12.4.5,
messages were sent back and forth between the server and the client in a definite,
predetermined sequence. Communication between the server and a client
was actually communication between one thread running on the server and another
thread running on the client. For the netgame framework, however, I want to
allow for asynchronous communication, in which it is not possible to wait for
messages to arrive in a predictable sequence. To make this possible a netgame
client will use two threads for communication, one for sending messages to the hub and
one for receiving messages from the hub. Similarly, the netgame hub will use two threads
for communicating with each client.

The hub is generally connected to many clients and can receive messages
from any of those clients at any time. The hub will have to process each
message in some way. To organize this processing, the hub uses a single
thread to process all incoming messages. When a communication thread
receives a message from a client, it simply drops that message into a
queue of incoming messages. There is only one such queue, which is
used for messages from all clients. The message processing thread runs
in a loop in which it removes a message from the queue, processes it,
removes another message from the queue, processes it, and so on.
The queue itself is implemented as an object of type
LinkedBlockingQueue (see Subsection 12.3.3).

hub and clients, showing threads and message queue

There is one more thread in the hub, not shown in the illustration. This final
thread creates a ServerSocket and uses it to listen
for connection requests from clients. Each time it accepts a connection request,
it hands off the client socket to another object, defined by the nested class
ConnectionToClient, which will handle communication with that client.
Each connected client is identified by an ID number. ID numbers 1, 2, 3, … are
assigned to clients as they connect. Since clients can also disconnect, the clients
connected at any give time might not have consecutive IDs. A variable
of type TreeMap<Integer,ConnectionToClient>
associates the ID numbers of connected clients with the objects that
handle their connections.

The messages that are sent and received are objects. The I/O streams
that are used for reading and writing objects are of type
ObjectInputStream and ObjectOutputStream.
(See Subsection 11.1.6.) The output stream of a socket is wrapped
in an ObjectOutputStream to make it possible to transmit
objects through that socket. The socket’s input stream is wrapped in
an ObjectInputStream to make it possible to receive
objects. Remember that the objects that are used with such streams
must implement the interface java.io.Serializable.

The netgame Hub class is defined in the file
Hub.java, in the
package netgame.common.
The port on which the server socket will listen must be specified as a
parameter to the Hub constructor.
The Hub class defines a method

protected void messageReceived(int playerID, Object message)

When a message from some client arrives at the front of the
queue of messages, the message-processing thread removes it
from the queue and calls this method. This is the point at which
the message from the client is actually processed.

The first parameter, playerID, is the ID number of the client
from whom the message was received, and the second parameter is the message
itself. In the Hub class, this method will simply
forward a copy of the message to every connected client. This defines the default processing
for incoming messages to the hub. To forward the message, it
wraps both the playerID and the message in
an object of type ForwardedMessage (defined in the
file ForwardedMessage.java,
in the package netgame.common). In a simple application such as
the chat room discussed in the next subsection,
this default processing might be exactly what is needed by the application.
For most applications, however, it will be necessary
to define a subclass of Hub and redefine
the messageReceived() method to do more complicated message processing.
There are several other methods in the Hub
class that you might want to redefine in a subclass, including

  • protected void playerConnected(int playerID) — This method is
    called each time a player connects to the hub. The parameter playerID
    is the ID number of the newly connected player. In the Hub
    class, this method does nothing. (The hub has already sent a
    StatusMessage to
    every client to inform them about the new player; playerConnected()
    is for any additional actions that a subclass of Hub
    might want to take.) Note that the complete list of ID numbers
    for currently connected players can be obtained by calling
    getPlayerList().
  • protected void playerDisconnected(int playerID) — This
    is called each time a player disconnects from the hub (after the hub sends a
    StatusMessage to the clients). The parameter tells
    which player has just disconnected. In the Hub class,
    this method does nothing.

The Hub class also defines a number of useful public
methods, notably

  • sendToAll(message) — sends the specified message
    to every client that is currently connected to the hub. The message must be a non-null
    object that implements the Serializable interface.
  • sendToOne(recipientID,message) — sends a
    specified message to just one user. The first parameter,
    recipientID is the ID number of the client who will receive the
    message. This method returns a boolean value, which is false if
    there is no connected client with the specified recipientID.
  • shutDownServerSocket() — shuts down the hub’s
    server socket, so that no additional clients will be able to connect. This could
    be used, for example, in a two-person game, after the second client has connected.
  • setAutoreset(autoreset) — sets the boolean
    value of the autoreset property. If this property is true,
    then the ObjectOutputStreams that are used to transmit
    messages to clients will automatically be reset before each message is
    transmitted. The default value is false.
    (Resetting an ObjectOutputStream is something
    that has to be done if an object is written to the stream, modified, and then
    written to the stream again. If the stream is not reset before writing the
    modified object, then the old, unmodified value is sent to the stream instead of the new value.
    See Subsection 11.1.6 for a discussion of this technicality. The preferred solution
    is to use only immutable objects for communication; in that case, no resetting is necessary.)

For more information—and to see how all this is implemented—you
should read the source code file Hub.java.
With some effort and study, you should be able to understand everything in that file.
(However, you only need to understand the public and protected interface of
Hub and other classes in the netgame framework
to write applications based on it.)


Turning to the client side, the basic netgame client class is defined in the file
Client.java, in
the package netgame.common.
The Client class has a constructor that specifies
the host name (or IP address) and port number of the hub to which the client will connect.
This constructor blocks until the connection has been established.

Client is an abstract class.
Every netgame application must define a subclass of Client
and provide a definition for the abstract method:

abstract protected void messageReceived(Object message);

This method is called each time a message is received from
the netgame hub. A subclass of client
might also override the protected methods
playerConnected, playerDisconnected,
serverShutdown, and connectionClosedByError.
See the source code
for more information. I should also note that Client
contains the protected instance variable connectedPlayerIDs,
of type int[], an array containing the ID numbers of all the clients
that are currently connected to the hub. The most important public
methods that are provided by the Client class are

  • send(message) — transmits a message to the hub. The
    message can be any non-null object that implements the
    Serializable interface.
  • getID() — gets the ID number that was assigned to this client by the hub.
  • disconnect() — closes the client’s connection to the hub.
    It is not possible to send messages after disconnecting. The send()
    method will throw an IllegalStateException if an attempt is
    made to do so.

The Hub and Client classes
are meant to define a general framework that can be used as the basis for
a variety of networked games—and, indeed, of other distributed programs.
The low level details of network communication and multithreading are hidden
in the private sections of these classes. Applications that
build on these classes can work in terms of higher-level concepts such
as players and messages. The design of these classes was developed though several
iterations, based on experience with several actual applications. I urge
you to look at the source code to see how Hub and
Client use threads, sockets, and I/O streams. In the
remainder of this section, I will discuss three applications built on
the netgame framework. I will not discuss these applications in great detail.
You can find the complete source code for all three in the
netgame package.


12.5.2  A Simple Chat Room

Our first example is a “chat room,” a network application
where users can connect to a server and can then post messages
that will be seen by all current users of the room. It is similar
to the GUIChat program
from Subsection 12.4.2, except that any number of
users can participate in a chat. While this application is not
a game, it does show the basic functionality of the
netgame framework.

The chat room application consists of two programs. The first,
ChatRoomServer.java,
is a completely trivial program that simply creates a netgame
Hub to listen for connection requests
from netgame clients:

public static void main(String[] args) {
    try {
        new Hub(PORT);
    }
    catch (IOException e) {
        System.out.println("Can't create listening socket.  Shutting down.");
    }
}

The port number, PORT, is defined as a constant in the
program and is arbitrary, as long as both the server and the
clients use the same port. Note that
ChatRoom uses the Hub class itself, not a subclass.

The second part of the chat room application is the program
ChatRoomWindow.java,
which is meant to be run by users who want to participate in the chat room.
A potential user must know the name (or IP address) of the computer
where the hub is running. (For testing, it is possible to run
the client program on the same computer as the hub, using localhost
as the name of the computer where the hub is running.)
When ChatRoomWindow is
run, it uses a dialog box to ask the user for this information. It
then opens a window that will serve as the user’s interface to the chat
room. The window has a large transcript area that displays messages that
users post to the chat room. It also has a text input box where the
user can enter messages. When the user enters a message, that message
will be posted to the transcript of every user who is connected to the
hub, so all users see every message sent by every user. Let’s look
at some of the programming.

Any netgame application must define a subclass of the abstract
Client class.
For the chat room application, clients are defined by a nested
class ChatClient inside ChatRoomWindow.
The program has an instance variable, connection, of type
ChatClient, which represents the program’s
connection to the hub. When the user enters a message, that message
is sent to the hub by calling

connection.send(message);

When the hub receives the message, it packages it into an object
of type ForwardedMessage,
along with the ID number of the client who sent the message. The hub
sends a copy of that ForwardedMessage to every
connected client, including the client who sent the message. On the client
side in each client, when the message is received from the hub,
the messageReceived() method of the ChatClient
object in that client is called.
ChatClient overrides this method to program it to
add the message to the transcript of the ChatClientWindow.
To summarize: Every message entered by any user is sent to the hub, which
just sends out copies of each message that it receives to every client. Each
client will see exactly the same stream of messages from the hub.

A client is also notified when a player connects to or disconnects from
the hub and when the connection with the hub is lost. ChatClient
overrides the methods that are called when these events happen so that
they post appropriate messages to the transcript. Here’s the complete definition
of the client class for the chat room application:

/**
 * A ChatClient connects to the Hub and is used to send messages to
 * the Hub and receive messages from the Hub.  Messages received from
 * the Hub will be of type ForwardedMessage and will contain the
 * ID number of the sender and the string that was sent by
 * that user.
 */
private class ChatClient extends Client {

    /**
     * Opens a connection to the chat room server on a specified computer.
     */
    ChatClient(String host) throws IOException {
        super(host, PORT);
    }

    /**
     * Responds when a message is received from the server.  It should be
     * a ForwardedMessage representing something that one of the participants
     * in the chat room is saying.  The message is simply added to the
     * transcript, along with the ID number of the sender.
     */
    protected void messageReceived(Object message) {
        if (message instanceof ForwardedMessage) {  
                  // (no other message types are expected)
            ForwardedMessage bm = (ForwardedMessage)message;
            addToTranscript("#" + bm.senderID + " SAYS:  " + bm.message);
        }
    }

    /**
     * Called when the connection to the client is shut down because of some
     * error message.  (This will happen if the server program is terminated.)
     */
    protected void connectionClosedByError(String message) {
        addToTranscript(
           "Sorry, communication has shut down due to an error:\n     " 
                                     + message );
        Platform.runLater( () -> {
            sendButton.setDisable(true);
            messageInput.setEditable(false);
            messageInput.setDisable(true);
            messageInput.setText("");
        });
        connected = false;
        connection = null;
    }

    /**
     * Posts a message to the transcript when someone joins the chat room.
     */
    protected void playerConnected(int newPlayerID) {
        addToTranscript(
                "Someone new has joined the chat room, with ID number " 
                + newPlayerID );
    }

    /**
     * Posts a message to the transcript when someone leaves the chat room.
     */
    protected void playerDisconnected(int departingPlayerID) {
        addToTranscript( "The person with ID number " 
                            + departingPlayerID + " has left the chat room");
    }

} // end nested class ChatClient

Except for the constructor, none of the methods in the ChatClient
class are called by the ChatRoomWindow program; they are called from
the connection-handling thread in the client object, which was programmed in
Client.java.
For the full source code of the chat room application, see the
source code files, which can be found in the package
netgame.chat.

Note: A user of my chat room application is identified only by an ID number that
is assigned by the hub when the client connects. Essentially, users are
anonymous, which is not very satisfying. See Exercise 12.7
at the end of this chapter for a way of addressing this issue.


12.5.3  A Networked TicTacToe Game

My second example is a very simple game: the familiar children’s game
TicTacToe. In TicTacToe, two players alternate placing marks on a
three-by-three board. One player plays X’s; the other plays O’s.
The object is to get three X’s or three O’s in a row.

At a given time, the state of a TicTacToe game consists of
various pieces of information such as the current contents of
the board, whose turn it is, and—when the game is over—who
won or lost. In a typical non-networked version of the game,
this state would be represented by instance variables. The
program would consult those instance variables to determine
how to draw the board and how to respond to user actions such
as mouse clicks. In the networked netgame version, however,
there are three objects involved: Two objects belonging to a
client class, which provide the interface to the two players
of the game, and the hub object that manages the connections to the
clients. These objects are not even on the same
computer, so they certainly can’t use the same state variables!
Nevertheless, the game has to have a single, well-defined
state at any time, and both players have to be aware of
that state.

My solution for TicTacToe is to store the “official” game state in
the hub, and to send a copy of that state to each player
every time the state changes. The players can’t change
the state directly. When a player takes some action, such
as placing a piece on the board, that action is sent
as a message to the hub. The hub changes the state to
reflect the result of the action, and it sends the new
state to both players. The window used by each player will
then be updated to reflect the new state. In this way, we
can be sure that the game always looks the same to both players.
(Instead of sending a complete copy of the state each time the state
changes, I might have sent just the change. But that would require
some way to encode the changes into messages that can be sent
over the network. Since the state is so simple, it seemed easier
just to send the entire state as the message in this case.)

Networked TicTacToe is defined in several classes in the
package netgame.tictactoe. The class
TicTacToeGameState
represents the state of a game. It includes a method

public void applyMessage(int senderID, Object message)

that modifies the state of the game to reflect the effect of a message
received from one of the players of the game. The message will
represent some action taken by the player, such as clicking
on the board.

The basic Hub class knows nothing about TicTacToe.
Since the hub for the TicTacToe game has to keep track of the state
of the game, it has to be defined by a subclass of Hub.
The TicTacToeGameHub
class is quite simple. It overrides the messageReceived() method
so that it responds to a message from a player by applying that message
to the game state and sending a copy of the new state to both players. It
also overrides the playerConnected() and playerDisconnected()
methods to take appropriate actions, since the game can only be played when
there are exactly two connected players. Here is the complete source code:

package netgame.tictactoe;

import java.io.IOException;

import netgame.common.Hub;

/**
 * A "Hub" for the network TicTacToe game.  There is only one Hub
 * for a game, and both network players connect to the same Hub.
 * Official information about the state of the game is maintained
 * on the Hub.  When the state changes, the Hub sends the new 
 * state to both players, ensuring that both players see the
 * same state.
 */
public class TicTacToeGameHub extends Hub {
    
    private TicTacToeGameState state;  // Records the state of the game.

    /**
     * Create a hub, listening on the specified port.  Note that this
     * method calls setAutoreset(true), which will cause the output stream
     * to each client to be reset before sending each message.  This is
     * essential since the same state object will be transmitted over and
     * over, with changes between each transmission.
     * @param port the port number on which the hub will listen.
     * @throws IOException if a listener cannot be opened on the specified port.
     */
    public TicTacToeGameHub(int port) throws IOException {
        super(port);
        state = new TicTacToeGameState();
        setAutoreset(true);
    }

    /**
     * Responds when a message is received from a client.  In this case,
     * the message is applied to the game state, by calling state.applyMessage().
     * Then the possibly changed state is transmitted to all connected players.
     */
    protected void messageReceived(int playerID, Object message) {
        state.applyMessage(playerID, message);
        sendToAll(state);
    }

    /**
     * This method is called when a player connects.  If that player
     * is the second player, then the server's listening socket is
     * shut down (because only two players are allowed), the 
     * first game is started, and the new state -- with the game
     * now in progress -- is transmitted to both players.
     */
    protected void playerConnected(int playerID) {
        if (getPlayerList().length == 2) {
            shutdownServerSocket();
            state.startFirstGame();
            sendToAll(state);
        }
    }

    /**
     * This method is called when a player disconnects.  This will
     * end the game and cause the other player to shut down as
     * well.  This is accomplished by setting state.playerDisconnected
     * to true and sending the new state to the remaining player, if 
     * there is one, to notify that player that the game is over.
     */
    protected void playerDisconnected(int playerID) {
        state.playerDisconnected = true;
        sendToAll(state);
    }
}

A player’s interface to the game is represented by the
class TicTacToeWindow.
As in the chat room application, this class defines a nested subclass
of Client to represent the client’s connection
to the hub. When the state of the game changes, a message is sent
to each client, and the client’s messageReceived() method
is called to process that message. That method, in turn, calls a
newState() method in the TicTacToeWindow
class to update the window. That method is called on the JavaFX
application thread using Platform.runLater():

protected void messageReceived(Object message) {
    if (message instanceof TicTacToeGameState) {
        Platform.runLater( () -> newState( (TicTacToeGameState)message ) );
    }
}

To run the TicTacToe netgame, the two players should each run the program
Main.java
in the package netgame.tictactoe.
This program presents the user with a window where the user can
choose to start a new game or to join an existing game. If the user
starts a new game, then a TicTacToeHub is created
to manage the game, and a second window of type TicTacToeWindow is opened
that immediately connects to the hub. The game will start as soon as a second
player connects to the hub. On the other hand, if the user running
Main chooses to connect to an existing
game, then no hub is created. A TicTacToeWindow is created,
and that window connects to the
hub that was created by the first player. The second player has to know
the name of the computer where the first player’s program is running.
As usual, for testing, you can run everything on one computer and use
“localhost” as the computer name.

(This is the first program that we have seen that uses two different windows.
Note that TicTacToeWindow is defined as a subclass of
Stage, the JavaFX class that represents windows.
A JavaFX program starts with a “primary stage” that is created by the system
and passed as a parameter to the start() method. But an application
can certainly create additional windows.)


12.5.4  A Networked Poker Game

And finally, we turn very briefly to the application that inspired the
netgame framework: Poker. In particular, I have implemented a
two-player version of the traditional “five card draw” version of
that game. This is a rather complex application, and I do not
intend to say much about it here other than to describe the general
design. The full source code can be found in the package
netgame.fivecarddraw.
To fully understand it, you will need to be familiar with the
game of five card draw poker.

In general outline, the Poker game is similar to the TicTacToe game.
There is a Main
class that is run by both players. The first player starts a new game; the second
must join that existing game. There is a class
PokerGameState
to represent the state of a game. And there is a subclass,
PokerHub,
of Hub to manage the game.

But Poker is a much more complicated game than TicTacToe, and the
game state is correspondingly more complicated. It’s not clear that we
want to broadcast a new copy of the complete game state to the players
every time some minor change is made in the state. Furthermore, it
doesn’t really make sense for both players to know the full game state—that
would include the opponent’s hand and full knowledge of the deck from which
the cards are dealt. (Of course, our client programs wouldn’t have to show
the full state to the players, but it would be easy enough for a player to
substitute their own client program to enable cheating.) So in the Poker
application, the full game state is known only to the PokerHub.
A PokerGameState object represents a view of the
game from the point of view of one player only. When the state of the game
changes, the PokerHub creates two different
PokerGameState objects, representing the state of the
game from each player’s point of view, and it sends the appropriate game state
object to each player. You can see the source code
for details.

(One of the hard parts in poker is to implement some way to compare
two hands, to see which is higher. In my game, this is handled by the
class PokerRank.
You might find this class useful in other poker games.)

License

ITP 220 Advanced Java Copyright © by Amanda Shelton. All Rights Reserved.