Client/Server Tic-Tac-Toe Using a Multithreaded Server
Client Server Tic Tac Toe Using a Multithreaded Server
In this section, we present the popular game Tic-Tac-Toe implemented by using client/server techniques with stream sockets. The program consists of a TicTacToeServer application (Fig. 24.13Fig. 24.14) that allows two TicTacToeClient applications (Fig. 24.15Fig. 24.16) to connect to the server and play Tic-Tac-Toe. Sample outputs are shown in Fig. 24.17.
Figure 24.13. Server side of client/server Tic-Tac-Toe program.
(This item is displayed on pages 1140 - 1145 in the print version)
1 // Fig. 24.13: TicTacToeServer.java 2 // This class maintains a game of Tic-Tac-Toe for two clients. 3 import java.awt.BorderLayout; 4 import java.net.ServerSocket; 5 import java.net.Socket; 6 import java.io.IOException; 7 import java.util.Formatter; 8 import java.util.Scanner; 9 import java.util.concurrent.ExecutorService; 10 import java.util.concurrent.Executors; 11 import java.util.concurrent.locks.Lock; 12 import java.util.concurrent.locks.ReentrantLock; 13 import java.util.concurrent.locks.Condition; 14 import javax.swing.JFrame; 15 import javax.swing.JTextArea; 16 import javax.swing.SwingUtilities; 17 18 public class TicTacToeServer extends JFrame 19 { 20 private String[] board = new String[ 9 ]; // tic-tac-toe board 21 private JTextArea outputArea; // for outputting moves 22 private Player[] players; // array of Players 23 private ServerSocket server; // server socket to connect with clients 24 private int currentPlayer; // keeps track of player with current move 25 private final static int PLAYER_X = 0; // constant for first player 26 private final static int PLAYER_O = 1; // constant for second player 27 private final static String[] MARKS = { "X", "O" }; // array of marks 28 private ExecutorService runGame; // will run players 29 private Lock gameLock; // to lock game for synchronization 30 private Condition otherPlayerConnected; // to wait for other player 31 private Condition otherPlayerTurn; // to wait for other player's turn 32 33 // set up tic-tac-toe server and GUI that displays messages 34 public TicTacToeServer() 35 { 36 super( "Tic-Tac-Toe Server" ); // set title of window 37 38 // create ExecutorService with a thread for each player 39 runGame = Executors.newFixedThreadPool( 2 ); 40 gameLock = new ReentrantLock(); // create lock for game 41 42 // condition variable for both players being connected 43 otherPlayerConnected = gameLock.newCondition(); 44 45 // condition variable for the other player's turn 46 otherPlayerTurn = gameLock.newCondition(); 47 48 for ( int i = 0; i < 9; i++ ) 49 board[ i ] = new String( "" ); // create tic-tac-toe board 50 players = new Player[ 2 ]; // create array of players 51 currentPlayer = PLAYER_X; // set current player to first player 52 53 try 54 { 55 server = new Server-Socket( 12345, 2 ); // set up ServerSocket 56 } // end try 57 catch ( IOException ioException ) 58 { 59 ioException.printStackTrace(); 60 System.exit( 1 ); 61 } // end catch 62 63 outputArea = new JTextArea(); // create JTextArea for output 64 add( outputArea, BorderLayout.CENTER ); 65 outputArea.setText( "Server awaiting connections " ); 66 67 setSize( 300, 300 ); // set size of window 68 setVisible( true ); // show window 69 } // end TicTacToeServer constructor 70 71 // wait for two connections so game can be played 72 public void execute() 73 { 74 // wait for each client to connect 75 for ( int i = 0; i < players.length; i++ ) 76 { 77 try // wait for connection, create Player, start runnable 78 { 79 players[ i ] = new Player( server.accept(), i ); 80 runGame.execute( players[ i ] ); // execute player runnable 81 } // end try 82 catch ( IOException ioException ) 83 { 84 ioException.printStackTrace(); 85 System.exit( 1 ); 86 } // end catch 87 } // end for 88 89 gameLock.lock(); // lock game to signal player X's thread 90 91 try 92 { 93 players[ PLAYER_X ].setSuspended( false ); // resume player X 94 otherPlayerConnected.signal(); // wake up player X's thread 95 } // end try 96 finally 97 { 98 gameLock.unlock(); // unlock game after signalling player X 99 } // end finally 100 } // end method execute 101 102 // display message in outputArea 103 private void displayMessage( final String messageToDisplay ) 104 { 105 // display message from event-dispatch thread of execution 106 SwingUtilities.invokeLater( 107 new Runnable() 108 { 109 public void run() // updates outputArea 110 { 111 outputArea.append( messageToDisplay ); // add message 112 } // end method run 113 } // end inner class 114 ); // end call to SwingUtilities.invokeLater 115 } // end method displayMessage 116 117 // determine if move is valid 118 public boolean validateAndMove( int location, int player ) 119 { 120 // while not current player, must wait for turn 121 while ( player != currentPlayer ) 122 { 123 gameLock.lock(); // lock game to wait for other player to go 124 125 try 126 { 127 otherPlayerTurn.await(); // wait for player's turn 128 } // end try 129 catch ( InterruptedException exception ) 130 { 131 exception.printStackTrace(); 132 } // end catch 133 finally 134 { 135 gameLock.unlock(); // unlock game after waiting 136 } // end finally 137 } // end while 138 139 // if location not occupied, make move 140 if ( !isOccupied( location ) ) 141 { 142 board[ location ] = MARKS[ currentPlayer ]; // set move on board 143 currentPlayer = ( currentPlayer + 1 ) % 2; // change player 144 145 // let new current player know that move occurred 146 players[ currentPlayer ].otherPlayerMoved( location ); 147 148 gameLock.lock(); // lock game to signal other player to go 149 150 try 151 { 152 otherPlayerTurn.signal(); // signal other player to continue 153 } // end try 154 finally 155 { 156 gameLock.unlock(); // unlock game after signaling 157 } // end finally 158 159 return true; // notify player that move was valid 160 } // end if 161 else // move was not valid 162 return false; // notify player that move was invalid 163 } // end method validateAndMove 164 165 // determine whether location is occupied 166 public boolean isOccupied( int location ) 167 { 168 if ( board[ location ].equals( MARKS[ PLAYER_X ] ) || 169 board [ location ].equals( MARKS[ PLAYER_O ] ) ) 170 return true; // location is occupied 171 else 172 return false; // location is not occupied 173 } // end method isOccupied 174 175 // place code in this method to determine whether game over 176 public boolean isGameOver() 177 { 178 return false; // this is left as an exercise 179 } // end method isGameOver 180 181 // private inner class Player manages each Player as a runnable 182 private class Player implements Runnable 183 { 184 private Socket connection; // connection to client 185 private Scanner input; // input from client 186 private Formatter output; // output to client 187 private int playerNumber; // tracks which player this is 188 private String mark; // mark for this player 189 private boolean suspended = true; // whether thread is suspended 190 191 // set up Player thread 192 public Player( Socket socket, int number ) 193 { 194 playerNumber = number; // store this player's number 195 mark = MARKS[ playerNumber ]; // specify player's mark 196 connection = socket; // store socket for client 197 198 try // obtain streams from Socket 199 { 200 input = new Scanner( connection.getInputStream() ); 201 output = new Formatter( connection.getOutputStream() ); 202 } // end try 203 catch ( IOException ioException ) 204 { 205 ioException.printStackTrace(); 206 System.exit( 1 ); 207 } // end catch 208 } // end Player constructor 209 210 // send message that other player moved 211 public void otherPlayerMoved( int location ) 212 { 213 output.format( "Opponent moved " ); 214 output.format( "%d ", location ); // send location of move 215 output.flush(); // flush output 216 } // end method otherPlayerMoved 217 218 // control thread's execution 219 public void run() 220 { 221 // send client its mark (X or O), process messages from client 222 try 223 { 224 displayMessage( "Player " + mark + " connected " ); 225 output.format( "%s ", mark ); // send player's mark 226 output.flush(); // flush output 227 228 // if player X, wait for another player to arrive 229 if ( playerNumber == PLAYER_X ) 230 { 231 output.format( "%s %s", "Player X connected", 232 "Waiting for another player " ); 233 output.flush(); // flush output 234 235 gameLock.lock(); // lock game to wait for second player 236 237 try 238 { 239 while( suspended ) 240 { 241 otherPlayerConnected.await(); // wait for player O 242 } // end while 243 } // end try 244 catch ( InterruptedException exception ) 245 { 246 exception.printStackTrace(); 247 } // end catch 248 finally 249 { 250 gameLock.unlock(); // unlock game after second player 251 } // end finally 252 253 // send message that other player connected 254 output.format( "Other player connected. Your move. " ); 255 output.flush(); // flush output 256 } // end if 257 else 258 { 259 output.format( "Player O connected, please wait " ); 260 output.flush(); // flush output 261 } // end else 262 263 // while game not over 264 while ( !isGameOver() ) 265 { 266 int location = 0; // initialize move location 267 268 if ( input.hasNext() ) 269 location = input.nextInt(); // get move location 270 271 // check for valid move 272 if ( validateAndMove( location, playerNumber ) ) 273 { 274 displayMessage( " location: " + location ); 275 output.format( "Valid move. " ); // notify client 276 output.flush(); // flush output 277 } // end if 278 else // move was invalid 279 { 280 output.format( "Invalid move, try again " ); 281 output.flush(); // flush output 282 } // end else 283 } // end while 284 } // end try 285 finally 286 { 287 try 288 { 289 connection.close(); // close connection to client 290 } // end try 291 catch ( IOException ioException ) 292 { 293 ioException.printStackTrace(); 294 System.exit( 1 ); 295 } // end catch 296 } // end finally 297 } // end method run 298 299 // set whether or not thread is suspended 300 public void setSuspended( boolean status ) 301 { 302 suspended = status; // set value of suspended 303 } // end method setSuspended 304 } // end class Player 305 } // end class TicTacToeServer |
Figure 24.14. Class that tests Tic-Tac-Toe server.
(This item is displayed on page 1146 in the print version)
1 // Fig. 24.14: TicTacToeServerTest.java 2 // Tests the TicTacToeServer. 3 import javax.swing.JFrame; 4 5 public class TicTacToeServerTest 6 { 7 public static void main( String args[] ) 8 { 9 TicTacToeServer application = new TicTacToeServer(); 10 application.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); 11 application.execute(); 12 } // end main 13 } // end class TicTacToeServerTest
|
Figure 24.15. Client side of client/server Tic-Tac-Toe program.
(This item is displayed on pages 1147 - 1152 in the print version)
1 // Fig. 24.15: TicTacToeClient.java 2 // Client that let a user play Tic-Tac-Toe with another across a network. 3 import java.awt.BorderLayout; 4 import java.awt.Dimension; 5 import java.awt.Graphics; 6 import java.awt.GridLayout; 7 import java.awt.event.MouseAdapter; 8 import java.awt.event.MouseEvent; 9 import java.net.Socket; 10 import java.net.InetAddress; 11 import java.io.IOException; 12 import javax.swing.JFrame; 13 import javax.swing.JPanel; 14 import javax.swing.JScrollPane; 15 import javax.swing.JTextArea; 16 import javax.swing.JTextField; 17 import javax.swing.SwingUtilities; 18 import java.util.Formatter; 19 import java.util.Scanner; 20 import java.util.concurrent.Executors; 21 import java.util.concurrent.ExecutorService; 22 23 public class TicTacToeClient extends JFrame implements Runnable 24 { 25 private JTextField idField; // textfield to display player's mark 26 private JTextArea displayArea; // JTextArea to display output 27 private JPanel boardPanel; // panel for tic-tac-toe board 28 private JPanel panel2; // panel to hold board 29 private Square board[][]; // tic-tac-toe board 30 private Square currentSquare; // current square 31 private Socket connection; // connection to server 32 private Scanner input; // input from server 33 private Formatter output; // output to server 34 private String ticTacToeHost; // host name for server 35 private String myMark; // this client's mark 36 private boolean myTurn; // determines which client's turn it is 37 private final String X_MARK = "X"; // mark for first client 38 private final String O_MARK = "O"; // mark for second client 39 40 // set up user-interface and board 41 public TicTacToeClient( String host ) 42 { 43 ticTacToeHost = host; // set name of server 44 displayArea = new JTextArea( 4, 30 ); // set up JTextArea 45 displayArea.setEditable( false ); 46 add( new JScrollPane( displayArea ), BorderLayout.SOUTH ); 47 48 boardPanel = new JPanel(); // set up panel for squares in board 49 boardPanel.setLayout( new GridLayout( 3, 3, 0, 0 ) ); 50 51 board = new Square[ 3 ][ 3 ]; // create board 52 53 // loop over the rows in the board 54 for ( int row = 0; row < board.length; row++ ) 55 { 56 // loop over the columns in the board 57 for ( int column = 0; column < board[ row ].length; column++ ) 58 { 59 // create square 60 board[ row ][ column ] = new Square( ' ', row * 3 + column ); 61 boardPanel.add( board[ row ][ column ] ); // add square 62 } // end inner for 63 } // end outer for 64 65 idField = new JTextField(); // set up textfield 66 idField.setEditable( false ); 67 add( idField, BorderLayout.NORTH ); 68 69 panel2 = new JPanel(); // set up panel to contain boardPanel 70 panel2.add( boardPanel, BorderLayout.CENTER ); // add board panel 71 add( panel2, BorderLayout.CENTER ); // add container panel 72 73 setSize( 300, 225 ); // set size of window 74 setVisible( true ); // show window 75 76 startClient(); 77 } // end TicTacToeClient constructor 78 79 // start the client thread 80 public void startClient() 81 { 82 try // connect to server, get streams and start outputThread 83 { 84 // make connection to server 85 connection = new Socket( 86 InetAddress.getByName( ticTacToeHost ), 12345 ); 87 88 // get streams for input and output 89 input = new Scanner( connection.getInputStream() ); 90 output = new Formatter( connection.getOutputStream() ); 91 } // end try 92 catch ( IOException ioException ) 93 { 94 ioException.printStackTrace(); 95 } // end catch 96 97 // create and start worker thread for this client 98 ExecutorService worker = Executors.newFixedThreadPool( 1 ); 99 worker.execute( this ); // execute client 100 } // end method startClient 101 102 // control thread that allows continuous update of displayArea 103 public void run() 104 { 105 myMark = input.nextLine(); // get player's mark (X or O) 106 107 SwingUtilities.invokeLater( 108 new Runnable() 109 { 110 public void run() 111 { 112 // display player's mark 113 idField.setText( "You are player "" + myMark + """ ); 114 } // end method run 115 } // end anonymous inner class 116 ); // end call to SwingUtilities.invokeLater 117 118 myTurn = ( myMark.equals( X_MARK ) ); // determine if client's turn 119 120 // receive messages sent to client and output them 121 while ( true ) 122 { 123 if ( input.hasNextLine() ) 124 processMessage( input.nextLine() ); 125 } // end while 126 } // end method run 127 128 // process messages received by client 129 private void processMessage( String message ) 130 { 131 // valid move occurred 132 if ( message.equals( "Valid move." ) ) 133 { 134 displayMessage( "Valid move, please wait. " ); 135 setMark( currentSquare, myMark ); // set mark in square 136 } // end if 137 else if ( message.equals( "Invalid move, try again" ) ) 138 { 139 displayMessage( message + " " ); // display invalid move 140 myTurn = true; // still this client's turn 141 } // end else if 142 else if ( message.equals( "Opponent moved" ) ) 143 { 144 int location = input.nextInt(); // get move location 145 input.nextLine(); // skip newline after int location 146 int row = location / 3; // calculate row 147 int column = location % 3; // calculate column 148 149 setMark( board[ row ][ column ], 150 ( myMark.equals( X_MARK ) ? O_MARK : X_MARK ) ); // mark move 151 displayMessage( "Opponent moved. Your turn. " ); 152 myTurn = true; // now this client's turn 153 } // end else if 154 else 155 displayMessage( message + " " ); // display the message 156 } // end method processMessage 157 158 // manipulate outputArea in event-dispatch thread 159 private void displayMessage( final String messageToDisplay ) 160 { 161 SwingUtilities.invokeLater( 162 new Runnable() 163 { 164 public void run() 165 { 166 displayArea.append( messageToDisplay ); // updates output 167 } // end method run 168 } // end inner class 169 ); // end call to SwingUtilities.invokeLater 170 } // end method displayMessage 171 172 // utility method to set mark on board in event-dispatch thread 173 private void setMark( final Square squareToMark, final String mark ) 174 { 175 SwingUtilities.invokeLater( 176 new Runnable() 177 { 178 public void run() 179 { 180 squareToMark.setMark( mark ); // set mark in square 181 } // end method run 182 } // end anonymous inner class 183 ); // end call to SwingUtilities.invokeLater 184 } // end method setMark 185 186 // send message to server indicating clicked square 187 public void sendClickedSquare( int location ) 188 { 189 // if it is my turn 190 if ( myTurn ) 191 { 192 output.format( "%d ", location ); // send location to server 193 output.flush(); 194 myTurn = false; // not my turn anymore 195 } // end if 196 } // end method sendClickedSquare 197 198 // set current Square 199 public void setCurrentSquare( Square square ) 200 { 201 currentSquare = square; // set current square to argument 202 } // end method setCurrentSquare 203 204 // private inner class for the squares on the board 205 private class Square extends JPanel 206 { 207 private String mark; // mark to be drawn in this square 208 private int location; // location of square 209 210 public Square( String squareMark, int squareLocation ) 211 { 212 mark = squareMark; // set mark for this square 213 location = squareLocation; // set location of this square 214 215 addMouseListener( 216 new MouseAdapter() 217 { 218 public void mouseReleased( MouseEvent e ) 219 { 220 setCurrentSquare( Square.this ); // set current square 221 222 // send location of this square 223 sendClickedSquare( getSquareLocation() ); 224 } // end method mouseReleased 225 } // end anonymous inner class 226 ); // end call to addMouseListener 227 } // end Square constructor 228 229 // return preferred size of Square 230 public Dimension getPreferredSize() 231 { 232 return new Dimension( 30, 30 ); // return preferred size 233 } // end method getPreferredSize 234 235 // return minimum size of Square 236 public Dimension getMinimumSize() 237 { 238 return getPreferredSize(); // return preferred size 239 } // end method getMinimumSize 240 241 // set mark for Square 242 public void setMark( String newMark ) 243 { 244 mark = newMark; // set mark of square 245 repaint(); // repaint square 246 } // end method setMark 247 248 // return Square location 249 public int getSquareLocation() 250 { 251 return location; // return location of square 252 } // end method getSquareLocation 253 254 // draw Square 255 public void paintComponent( Graphics g ) 256 { 257 super .paintComponent( g ); 258 259 g.drawRect( 0, 0, 29, 29 ); // draw square 260 g.drawString( mark, 11, 20 ); // draw mark 261 } // end method paintComponent 262 } // end inner-class Square 263 } // end class TicTacToeClient |
Figure 24.16. Test class for Tic-Tac-Toe client.
(This item is displayed on page 1153 in the print version)
1 // Fig. 24.16: TicTacToeClientTest.java 2 // Tests the TicTacToeClient class. 3 import javax.swing.JFrame; 4 5 public class TicTacToeClientTest 6 { 7 public static void main( String args[] ) 8 { 9 TicTacToeClient application; // declare client application 10 11 // if no command line args 12 if ( args.length == 0 ) 13 application = new TicTacToeClient( "127.0.0.1" ); // localhost 14 else 15 application = new TicTacToeClient( args[ 0 ] ); // use args 16 17 application.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); 18 } // end main 19 } // end class TicTacToeClientTest |
Figure 24.17. Sample outputs from the client/server Tic-Tac-Toe program.
(This item is displayed on pages 1153 - 1154 in the print version)
TicTacToeServer Class
As the TicTacToeServer receives each client connection, it creates an instance of inner-class Player (lines 182301 of Fig. 24.13) to process the client in a separate thread. These threads enable the clients to play the game independently. The first client to connect to the server is player X and the second is player O. Player X makes the first move. The server maintains the information about the board so it can determine whether a player's move is valid or invalid.
We begin with a discussion of the server side of the Tic-Tac-Toe game. When the TicTacToeServer application executes, the main method (lines 712 of Fig. 24.14) creates a TicTacToeServer object called application. The constructor (lines 3469 of Fig. 24.13) attempts to set up a ServerSocket. If successful, the program displays the server window, then main invokes the TicTacToeServer method execute (lines 72100). Method execute loops twice, blocking at line 79 each time while waiting for a client connection. When a client connects, line 79 creates a new Player object to manage the connection as a separate thread, and line 80 executes the Player in the runGame thread pool.
When the TicTacToeServer creates a Player, the Player constructor (lines 192208) receives the Socket object representing the connection to the client and gets the associated input and output streams. Line 201 creates a Formatter (see Chapter 28) by wrapping it around the output stream of the socket. The Player's run method (lines 219297) controls the information that is sent to and received from the client. First, it passes to the client the character that the client will place on the board when a move is made (line 225). Line 226 calls Formatter method flush to force this output to the client. Line 241 suspends player X's thread as it starts executing, because player X can move only after player O connects.
After player O connects, the game can be played, and the run method begins executing its while statement (lines 264283). Each iteration of this loop reads an integer (line 269) representing the location where the client wants to place a mark, and line 272 invokes the TicTacToeServer method validateAndMove (declared at lines 118163) to check the move. If the move is valid, line 275 sends a message to the client to this effect. If not, line 280 sends a message indicating that the move was invalid. The program maintains board locations as numbers from 0 to 8 (0 through 2 for the first row, 3 through 5 for the second row and 6 through 8 for the third row).
Method validateAndMove (lines 118163 in class TicTacToeServer) allows only one player at a time to move, thereby preventing them from modifying the state information of the game simultaneously. If the Player attempting to validate a move is not the current player (i.e., the one allowed to make a move), it is placed in a wait state until its turn to move. If the position for the move being validated is already occupied on the board, validMove returns false. Otherwise, the server places a mark for the player in its local representation of the board (line 142), notifies the other Player object (line 146) that a move has been made (so that the client can be sent a message), invokes method signal (line 152) so that the waiting Player (if there is one) can validate a move and returns true (line 159) to indicate that the move is valid.
TicTacToeClient Class
Each TicTacToeClient application (Fig. 24.15) maintains its own GUI version of the Tic-Tac-Toe board on which it displays the state of the game. The clients can place a mark only in an empty square on the board. Inner class Square (lines 205262 of Fig. 24.15) implements each of the nine squares on the board. When a TicTacToeClient begins execution, it creates a JTextArea in which messages from the server and a representation of the board using nine Square objects are displayed. The startClient method (lines 80100) opens a connection to the server and gets the associated input and output streams from the Socket object. Lines 8586 make a connection to the server. Class TicTacToeClient implements interface Runnable so that a separate thread can read messages from the server. This approach enables the user to interact with the board (in the event-dispatch thread) while waiting for messages from the server. After establishing the connection to the server, line 99 executes the client with the worker ExecutorService. The run method (lines 103124) controls the separate thread of execution. The method first reads the mark character (X or O) from the server (line 105), then loops continuously (lines 121125) and reads messages from the server (line 124). Each message is passed to the processMessage method (lines 129156) for processing.
If the message received is "Valid move.", lines 134135 display the message "Valid move, please wait." and call method setMark (lines 173184) to set the client's mark in the current square (the one in which the user clicked) using SwingUtilities method invokeLater to ensure that the GUI updates occur in the event-dispatch thread. If the message received is "Invalid move, try again.", line 139 displays the message so that the user can click a different square. If the message received is "Opponent moved.", line 145 reads an integer from the server indicating where the opponent moved, and lines 149150 place a mark in that square of the board (again using SwingUtilities method invokeLater to ensure that the GUI updates occur in the event-dispatch thread). If any other message is received, line 155 simply displays the message. Figure 24.17 shows sample screen captures of two applications interacting via the TicTacToeServer.