package netgame.common; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.net.Socket; import java.util.concurrent.LinkedBlockingQueue; /** * This abstract class represents a Client, or Player, that can connect * to a netgame Hub. The client is used for sending messages to the * Hub (and through the Hub possibly to other clients). It also * receives messages from the Hub. An application must define a * subclass of this abstract class in order to create players. * Each player in the game will have an associated Client object, * which will handle details of network communication with the Hub. *
At a minimum, the abstract method messageReceived(Object) must * be defined to say how the Client should respond when a message is * received. Subclasses might also define the closedByError(), * serverShutdown(), playerConnected(), and playerDisconnected() methods. *
A client provides methods send(Object) and disconnect() for * sending a message to the Hub and for closing down the connection. * Any non-null object that implements the Serializable interface can be sent * as a message. Note that an ObjectOutputStream is used for sending * messages. If the same object is to be sent more than once, with * changes between transmissions, then the resetOutput() method should be * called between transmissions (or the autoreset property should be * set to true). *
A client has a unique ID number that is assigned to the client * when it connects to the hub. The ID can retrieved by calling * the getID() method. The protected variable connectedPlayerIDs * contains the ID numbers of all clients currently connected to the * hub, including this one. */ abstract public class Client { /** * A list of the ID numbers of all clients who are currently connected * to the hub. This list is set each time this client is notified that * a client has connected to or disconnected from the hub. */ protected int[] connectedPlayerIDs = new int[0]; /** * If the autoreset property is set to true, then the ObjectOutputStream * that is used for transmitting messages is reset before each object is * sent. */ private volatile boolean autoreset; /** * Constructor opens a connection to a Hub. This constructor will * block while waiting for the connection to be established. * @param hubHostName The host name (or IP address) of the computer where the Hub is running. * @param hubPort The port number on which the Hub is listening for connection requests. * @throws IOException if any I/O exception occurs while trying to connect. */ public Client(String hubHostName, int hubPort) throws IOException { connection = new ConnectionToHub(hubHostName, hubPort); } // ---------------- Methods that subclasses can override -------------------------- /** * This method is called when a message is received from the Hub. * Concrete subclasses of this class must override this method to * say how to respond to messages. Messages can be arbitrary * Serializable objects. */ abstract protected void messageReceived(Object message); /** * This method is called whenever this client is notified that * a client has connected to the hub. (Note that it is called * when this client connects, so this method will be called just * after the connection has been established.) The list of all * connected players, including the new one, is in the protected * variable connectedPlayerIDs. The method in this class does nothing. * @param newPlayerID the ID number of the player who has connected. */ protected void playerConnected(int newPlayerID) { } /** * This method is called when this client is notified that a client * has disconnected from the hub. (Note that it IS NOT called * when this client disconnects.) The list of all connected * players is in the protected variable connectedPlayerIDs. * The method in this class does nothing. * @param departingPlayerID the ID number of the player who has * just disconnected. */ protected void playerDisconnected(int departingPlayerID) { } /** * This method is called when the connection to the Hub is closed down * because of some error. The method in this class does nothing. Subclasses * can override this method to take some action when the error occurs. */ protected void connectionClosedByError(String message) { } /** * This method is called when the connection to the Hub is closed down * because the server is shutting down normally. The method in this class does * nothing. Subclasses can override this method to take some action when shutdown * occurs. The message will be "*shutdown*" if the message was in fact * sent by a Hub that is shutting down in the normal way. */ protected void serverShutdown(String message) { } /** * This method is called after a connection to the server has been opened * and after the client has been assigned an ID number. Its purpose is to * do extra checking or set up before the connection is fully established. * If this method throws an IOException, then the connection is closed * and the player is never added to the list of players. The method in * this class does nothing. The client and the hub must both be programmed * with the same handshake protocol. At the time this method is called, * the client's ID number has already been set and can be retrieved by * calling the getID() method, but the client has not yet been added to * the list of connected players. * @param in a stream from which messages from the hub can be read. * @param out a stream to which messages to the hub can be written. After writing * a message to this stream, it is important to call out.flush() to make sure * that the message is actually transmitted. * @throws IOException should be thrown if some error occurs that should * prevent the connection from being fully established. */ protected void extraHandshake(ObjectInputStream in, ObjectOutputStream out) throws IOException { } // ----------------------- Methods meant to be called by users of this class ----------- /** * This method can be called to disconnect cleanly from the server. * If the connection is already closed, this method has no effect. */ public void disconnect() { if (!connection.closed) connection.send(new DisconnectMessage("Goodbye Hub")); } /** * This method is called to send a message to the hub. This method simply * drops the message into a queue of outgoing messages, and it * never blocks. This method throws an IllegalStateException if the * connection to the Hub has already been closed. * @param message A non-null object representing the message. This object * must implement the Serializable interface. * @throws IllegalArgumentException if message is null or is not Serializable. * @throws IllegalStateException if the connection has already been closed, * either by the disconnect() method, because the Hub has shut down, or * because of a network error. */ public void send(Object message) { if (message == null) throw new IllegalArgumentException("Null cannot be sent as a message."); if (! (message instanceof Serializable)) throw new IllegalArgumentException("Messages must implement the Serializable interface."); if (connection.closed) throw new IllegalStateException("Message cannot be sent because the connection is closed."); connection.send(message); } /** * Returns the ID number of this client, which is assigned by the hub when * the connection to the hub is created. The id uniquely identifies this * client among all clients which have connected to the hub. ID numbers * are always assigned in the order 1, 2, 3, 4... There can be gaps in the * sequence if some client disconnects or because some client does not * completely connect because of an exception. (This can include an * exception in the "extra handshake" part, if there is one, of the * connection setup.) */ public int getID() { return connection.id_number; } /** * Resets the output stream, after any messages currently in the output queue * have been sent. The stream only needs to be reset in one case: If the same * object is transmitted more than once, and changes have been made to it * between transmissions. The reason for this is that ObjectOutputStreams are * optimized for sending objects that don't change -- if the same object is sent * twice it will not actually be transmitted the second time, unless the stream * has been reset in the meantime. */ public void resetOutput() { connection.send(new ResetSignal()); // A ResetSignal in the output stream is seen as a signal to reset } /** * If the autoreset property is set to true, then the output stream will be reset * before every object transmission. Use this if the same object is going to be * continually changed and retransmitted. See the resetOutput() method for more * information on resetting the output stream. */ public void setAutoreset(boolean auto) { autoreset = auto; } /** * Returns the value of the autoreset property. */ public boolean getAutoreset() { return autoreset; } //------------- Private implementation part of the class ----------------------------- private final ConnectionToHub connection; // Represents the network connection to the hub. /** * This private class handles the actual communication with the server. */ private class ConnectionToHub { private final int id_number; // The ID of this client, assigned by the hub. private final Socket socket; // The socket that is connected to the Hub. private final ObjectInputStream in; // A stream for sending messages to the Hub. private final ObjectOutputStream out; // A stream for receiving messages from the Hub. private final SendThread sendThread; // The thread that sends messages to the Hub. private final ReceiveThread receiveThread; // The thread that receives messages from the Hub. private final LinkedBlockingQueue