Client/Server Tic-Tac-Toe Using a Multithreaded Server

Client Server Tic Tac Toe Using a Multithreaded Server

In this section, we present a networked version of the popular game Tic-Tac-Toe, implemented with stream sockets and client/server techniques. The program consists of a TicTacToeServer application (Fig. 23.5) and a TicTacToeClient application (Fig. 23.6). The TicTacToeServer allows two TicTacToeClient instances to connect to the server and play Tic-Tac-Toe against each other. We depict the output in Fig. 23.6. When the server receives a client connection, lines 7887 of Fig. 23.5 create instances of class Player to process each client in a separate thread of execution. This enables the server to handle requests from both clients. The server assigns value "X" to the first client that connects (player X makes the first move), then assigns value "O" to the second client. Throughout the game, the server maintains information regarding the status of the board so that the server can validate players' requested moves. However, neither the server nor the client can establish whether a player has won the gamein this application, method GameOver (lines 139143) always returns false. Each Client maintains its own GUI version of the Tic-Tac-Toe board to display the game. The clients can place marks only in empty squares on the board. Class Square (Fig. 23.7) is used to define squares on the Tic-Tac-Toe board.

Figure 23.5. Server side of client/server Tic-Tac-Toe program.

1 // Fig. 23.5: TicTacToeServer.cs 2 // This class maintains a game of Tic-Tac-Toe for two 3 // client applications. 4 using System; 5 using System.Windows.Forms; 6 using System.Net; 7 using System.Net.Sockets; 8 using System.Threading; 9 using System.IO; 10 11 public partial class TicTacToeServerForm : Form 12 { 13 public TicTacToeServerForm() 14 { 15 InitializeComponent(); 16 } // end constructor 17 18 private byte[] board; // the local representation of the game board 19 private Player[] players; // two Player objects 20 private Thread[] playerThreads; // Threads for client interaction 21 private TcpListener listener; // listen for client connection 22 private int currentPlayer; // keep track of whose turn it is 23 private Thread getPlayers; // Thread for acquiring client connections 24 internal bool disconnected = false; // true if the server closes 25 26 // initialize variables and thread for receiving clients 27 private void TicTacToeServerForm_Load( object sender, EventArgs e ) 28 { 29 board = new byte[ 9 ]; 30 players = new Player[ 2 ]; 31 playerThreads = new Thread[ 2 ]; 32 currentPlayer = 0; 33 34 // accept connections on a different thread 35 getPlayers = new Thread( new ThreadStart( SetUp ) ); 36 getPlayers.Start(); 37 } // end method TicTacToeServerForm_Load 38 39 // notify Players to stop Running 40 private void TicTacToeServerForm_FormClosing( object sender, 41 FormClosingEventArgs e ) 42 { 43 disconnected = true; 44 System.Environment.Exit( System.Environment.ExitCode ); 45 } // end method TicTacToeServerForm_FormClosing 46 47 // delegate that allows method DisplayMessage to be called 48 // in the thread that creates and maintains the GUI 49 private delegate void DisplayDelegate( string message ); 50 51 // method DisplayMessage sets displayTextBox's Text property 52 // in a thread-safe manner 53 internal void DisplayMessage( string message ) 54 { 55 // if modifying displayTextBox is not thread safe 56 if ( displayTextBox.InvokeRequired ) 57 { 58 // use inherited method Invoke to execute DisplayMessage 59 // via a delegate 60 Invoke( new DisplayDelegate( DisplayMessage ), 61 new object[] { message } ); 62 } // end if 63 else // OK to modify displayTextBox in current thread 64 displayTextBox.Text += message; 65 } // end method DisplayMessage 66 67 // accepts connections from 2 players 68 public void SetUp() 69 { 70 DisplayMessage( "Waiting for players... " ); 71 72 // set up Socket 73 listener = 74 new TcpListener( IPAddress.Parse( "127.0.0.1" ), 50000 ); 75 listener.Start(); 76 77 // accept first player and start a player thread 78 players[ 0 ] = new Player( listener.AcceptSocket(), this, 0 ); 79 playerThreads[ 0 ] = 80 new Thread( new ThreadStart( players[ 0 ].Run ) ); 81 playerThreads[ 0 ].Start(); 82 83 // accept second player and start another player thread 84 players[ 1 ] = new Player( listener.AcceptSocket(), this, 1 ); 85 playerThreads[ 1 ] = 86 new Thread( new ThreadStart( players[ 1 ].Run ) ); 87 playerThreads[ 1 ].Start(); 88 89 // let the first player know that the other player has connected 90 lock ( players[ 0 ] ) 91 { 92 players[ 0 ].threadSuspended = false; 93 Monitor.Pulse( players[ 0 ] ); 94 } // end lock 95 } // end method SetUp 96 97 // determine if a move is valid 98 public bool ValidMove( int location, int player ) 99 { 100 // prevent another thread from making a move 101 lock ( this ) 102 { 103 // while it is not the current player's turn, wait 104 while ( player != currentPlayer ) 105 Monitor.Wait( this ); 106 107 // if the desired square is not occupied 108 if ( !IsOccupied( location ) ) 109 { 110 // set the board to contain the current player's mark 111 board[ location ] = ( byte ) ( currentPlayer == 0 ? 112 'X' : 'O' ); 113 114 // set the currentPlayer to be the other player 115 currentPlayer = ( currentPlayer + 1 ) % 2; 116 117 // notify the other player of the move 118 players[ currentPlayer ].OtherPlayerMoved( location ); 119 120 // alert the other player that it's time to move 121 Monitor.Pulse( this ); 122 return true; 123 } // end if 124 else 125 return false; 126 } // end lock 127 } // end method ValidMove 128 129 // determines whether the specified square is occupied 130 public bool IsOccupied( int location ) 131 { 132 if ( board[ location ] == 'X' || board[ location ] == 'O' ) 133 return true; 134 else 135 return false; 136 } // end method IsOccupied 137 138 // determines if the game is over 139 public bool GameOver() 140 { 141 // place code here to test for a winner of the game 142 return false; 143 } // end method GameOver 144 } // end class TicTacToeServerForm 145 146 // class Player represents a tic-tac-toe player 147 public class Player 148 { 149 internal Socket connection; // Socket for accepting a connection 150 private NetworkStream socketStream; // network data stream 151 private TicTacToeServerForm server; // reference to server 152 private BinaryWriter writer; // facilitates writing to the stream 153 private BinaryReader reader; // facilitates reading from the stream 154 private int number; // player number 155 private char mark; // player's mark on the board 156 internal bool threadSuspended = true; // if waiting for other player 157 158 // constructor requiring Socket, TicTacToeServerForm and int 159 // objects as arguments 160 public Player( Socket socket, TicTacToeServerForm serverValue, 161 int newNumber ) 162 { 163 mark = (newNumber == 0 ? 'X' : 'O'); 164 connection = socket; 165 server = serverValue; 166 number = newNumber; 167 168 // create NetworkStream object for Socket 169 socketStream = new NetworkStream( connection ); 170 171 // create Streams for reading/writing bytes 172 writer = new BinaryWriter( socketStream ); 173 reader = new BinaryReader( socketStream ); 174 } // end constructor 175 176 // signal other player of move 177 public void OtherPlayerMoved( int location ) 178 { 179 // signal that opponent moved 180 writer.Write( "Opponent moved." ); 181 writer.Write( location ); // send location of move 182 } // end method OtherPlayerMoved 183 184 // allows the players to make moves and receive moves 185 // from the other player 186 public void Run() 187 { 188 bool done = false; 189 190 // display on the server that a connection was made 191 server.DisplayMessage( "Player " + ( number == 0 ? 'X' : 'O' ) 192 + " connected " ); 193 194 // send the current player's mark to the client 195 writer.Write( mark ); 196 197 // if number equals 0 then this player is X, 198 // otherwise O must wait for X's first move 199 writer.Write( "Player " + ( number == 0 ? 200 "X connected. " : "O connected, please wait. " ) ); 201 202 // X must wait for another player to arrive 203 if ( mark == 'X' ) 204 { 205 writer.Write( "Waiting for another player." ); 206 207 // wait for notification from server that another 208 // player has connected 209 lock ( this ) 210 { 211 while ( threadSuspended ) 212 Monitor.Wait( this ); 213 } // end lock 214 215 writer.Write( "Other player connected. Your move." ); 216 } // end if 217 218 // play game 219 while ( !done ) 220 { 221 // wait for data to become available 222 while ( connection.Available == 0 ) 223 { 224 Thread.Sleep( 1000 ); 225 226 if ( server.disconnected ) 227 return; 228 } // end while 229 230 // receive data 231 int location = reader.ReadInt32(); 232 233 // if the move is valid, display the move on the 234 // server and signal that the move is valid 235 if ( server.ValidMove( location, number ) ) 236 { 237 server.DisplayMessage( "loc: " + location + " " ); 238 writer.Write( "Valid move." ); 239 } // end if 240 else // signal that the move is invalid 241 writer.Write( "Invalid move, try again." ); 242 243 // if game is over, set done to true to exit while loop 244 if ( server.GameOver() ) 245 done = true; 246 } // end while loop 247 248 // close the socket connection 249 writer.Close(); 250 reader.Close(); 251 socketStream.Close(); 252 connection.Close(); 253 } // end method Run 254 } // end class Player

Figure 23.6. Client side of client/server Tic-Tac-Toe program.

(This item is displayed on pages 1260 - 1266 in the print version)

1 // Fig. 23.6: TicTacToeClient.cs 2 // Client for the TicTacToe program. 3 using System; 4 using System.Drawing; 5 using System.Windows.Forms; 6 using System.Net.Sockets; 7 using System.Threading; 8 using System.IO; 9 10 public partial class TicTacToeClientForm : Form 11 { 12 public TicTacToeClientForm() 13 { 14 InitializeComponent(); 15 } // end constructor 16 17 private Square[ , ] board; // local representation of the game board 18 private Square currentSquare; // the Square that this player chose 19 private Thread outputThread; // Thread for receiving data from server 20 private TcpClient connection; // client to establish connection 21 private NetworkStream stream; // network data stream 22 private BinaryWriter writer; // facilitates writing to the stream 23 private BinaryReader reader; // facilitates reading from the stream 24 private char myMark; // player's mark on the board 25 private bool myTurn; // is it this player's turn? 26 private SolidBrush brush; // brush for drawing X's and O's 27 private bool done = false; // true when game is over 28 29 // initialize variables and thread for connecting to server 30 private void TicTacToeClientForm_Load( object sender, EventArgs e ) 31 { 32 board = new Square[ 3, 3 ]; 33 34 // create 9 Square objects and place them on the board 35 board[ 0, 0 ] = new Square( board0Panel, ' ', 0 ); 36 board[ 0, 1 ] = new Square( board1Panel, ' ', 1 ); 37 board[ 0, 2 ] = new Square( board2Panel, ' ', 2 ); 38 board[ 1, 0 ] = new Square( board3Panel, ' ', 3 ); 39 board[ 1, 1 ] = new Square( board4Panel, ' ', 4 ); 40 board[ 1, 2 ] = new Square( board5Panel, ' ', 5 ); 41 board[ 2, 0 ] = new Square( board6Panel, ' ', 6 ); 42 board[ 2, 1 ] = new Square( board7Panel, ' ', 7 ); 43 board[ 2, 2 ] = new Square( board8Panel, ' ', 8 ); 44 45 // create a SolidBrush for writing on the Squares 46 brush = new SolidBrush( Color.Black ); 47 48 // make connection to server and get the associated 49 // network stream 50 connection = new TcpClient( "127.0.0.1", 50000 ); 51 stream = connection.GetStream(); 52 writer = new BinaryWriter( stream ); 53 reader = new BinaryReader( stream ); 54 55 // start a new thread for sending and receiving messages 56 outputThread = new Thread( new ThreadStart( Run ) ); 57 outputThread.Start(); 58 } // end method TicTacToeClientForm_Load 59 60 // repaint the Squares 61 private void TicTacToeClientForm_Paint( object sender, 62 PaintEventArgs e ) 63 { 64 PaintSquares(); 65 } // end method TicTacToeClientForm_Load 66 67 // game is over 68 private void TicTacToeClientForm_FormClosing( object sender, 69 FormClosingEventArgs e ) 70 { 71 done = true; 72 System.Environment.Exit( System.Environment.ExitCode ); 73 } // end TicTacToeClientForm_FormClosing 74 75 // delegate that allows method DisplayMessage to be called 76 // in the thread that creates and maintains the GUI 77 private delegate void DisplayDelegate( string message ); 78 79 // method DisplayMessage sets displayTextBox's Text property 80 // in a thread-safe manner 81 private void DisplayMessage( string message ) 82 { 83 // if modifying displayTextBox is not thread safe 84 if ( displayTextBox.InvokeRequired ) 85 { 86 // use inherited method Invoke to execute DisplayMessage 87 // via a delegate 88 Invoke( new DisplayDelegate( DisplayMessage ), 89 new object[] { message } ); 90 } // end if 91 else // OK to modify displayTextBox in current thread 92 displayTextBox.Text += message; 93 } // end method DisplayMessage 94 95 // delegate that allows method ChangeIdLabel to be called 96 // in the thread that creates and maintains the GUI 97 private delegate void ChangeIdLabelDelegate( string message ); 98 99 // method ChangeIdLabel sets displayTextBox's Text property 100 // in a thread-safe manner 101 private void ChangeIdLabel( string label ) 102 { 103 // if modifying idLabel is not thread safe 104 if ( idLabel.InvokeRequired ) 105 { 106 // use inherited method Invoke to execute ChangeIdLabel 107 // via a delegate 108 Invoke( new ChangeIdLabelDelegate( ChangeIdLabel ), 109 new object[] { label } ); 110 } // end if 111 else // OK to modify idLabel in current thread 112 idLabel.Text = label; 113 } // end method ChangeIdLabel 114 115 // draws the mark of each square 116 public void PaintSquares() 117 { 118 Graphics g; 119 120 // draw the appropriate mark on each panel 121 for ( int row = 0; row < 3; row++ ) 122 { 123 for ( int column = 0; column < 3; column++ ) 124 { 125 // get the Graphics for each Panel 126 g = board[ row, column ].SquarePanel.CreateGraphics(); 127 128 // draw the appropriate letter on the panel 129 g.DrawString( board[ row, column ].Mark.ToString(), 130 board0Panel.Font, brush, 10, 8 ); 131 } // end for 132 } // end for 133 } // end method PaintSquares 134 135 // send location of the clicked square to server 136 private void square_MouseUp( object sender, 137 System.Windows.Forms.MouseEventArgs e ) 138 { 139 // for each square check if that square was clicked 140 for ( int row = 0; row < 3; row++ ) 141 { 142 for ( int column = 0; column < 3; column++ ) 143 { 144 if ( board[ row, column ].SquarePanel == sender ) 145 { 146 CurrentSquare = board[ row, column ]; 147 148 // send the move to the server 149 SendClickedSquare( board[ row, column ].Location ); 150 } // end if 151 } // end for 152 } // end for 153 } // end method square_MouseUp 154 155 // control thread that allows continuous update of the 156 // TextBox display 157 public void Run() 158 { 159 // first get players's mark (X or O) 160 myMark = reader.ReadChar(); 161 ChangeIdLabel( "You are player "" + myMark + """ ); 162 myTurn = ( myMark == 'X' ? true : false ); 163 164 // process incoming messages 165 try 166 { 167 // receive messages sent to client 168 while ( !done ) 169 ProcessMessage( reader.ReadString() ); 170 } // end try 171 catch ( IOException ) 172 { 173 MessageBox.Show( "Server is down, game over", "Error", 174 MessageBoxButtons.OK, MessageBoxIcon.Error ); 175 } // end catch 176 } // end method Run 177 178 // process messages sent to client 179 public void ProcessMessage( string message ) 180 { 181 // if the move the player sent to the server is valid 182 // update the display, set that square's mark to be 183 // the mark of the current player and repaint the board 184 if ( message == "Valid move." ) 185 { 186 DisplayMessage( "Valid move, please wait. " ); 187 currentSquare.Mark = myMark; 188 PaintSquares(); 189 } // end if 190 else if ( message == "Invalid move, try again." ) 191 { 192 // if the move is invalid, display that and it is now 193 // this player's turn again 194 DisplayMessage( message + " " ); 195 myTurn = true; 196 } // end else if 197 else if ( message == "Opponent moved." ) 198 { 199 // if opponent moved, find location of their move 200 int location = reader.ReadInt32(); 201 202 // set that square to have the opponents mark and 203 // repaint the board 204 board[ location / 3, location % 3 ].Mark = 205 ( myMark == 'X' ? 'O' : 'X' ); 206 PaintSquares(); 207 208 DisplayMessage( "Opponent moved. Your turn. " ); 209 210 // it is now this player's turn 211 myTurn = true; 212 } // end else if 213 else 214 DisplayMessage( message + " " ); // display message 215 } // end method ProcessMessage 216 217 // sends the server the number of the clicked square 218 public void SendClickedSquare( int location ) 219 { 220 // if it is the current player's move right now 221 if ( myTurn ) 222 { 223 // send the location of the move to the server 224 writer.Write( location ); 225 226 // it is now the other player's turn 227 myTurn = false; 228 } // end if 229 } // end method SendClickedSquare 230 231 // write-only property for the current square 232 public Square CurrentSquare 233 { 234 set 235 { 236 currentSquare = value; 237 } // end set 238 } // end property CurrentSquare 239 } // end class TicTacToeClientForm

At the start of the game.

(a)

(b)

After Player X makes the first move.

(c)

(d)

After Player O makes the second move.

(e)

(f)

After Player X makes the final move.

(g)

(h)

The Tie Tac Toe Server's output from the client interactions

(i)

Figure 23.7. Class Square.

(This item is displayed on pages 1266 - 1267 in the print version)

1 // Fig. 23.7: Square.cs 2 // A Square on the TicTacToe board. 3 using System.Windows.Forms; 4 5 // the representation of a square in a tic-tac-toe grid 6 public class Square 7 { 8 private Panel panel; // GUI Panel that represents this Square 9 private char mark; // player's mark on this Square (if any) 10 private int location; // location on the board of this Square 11 12 // constructor 13 public Square( Panel newPanel, char newMark, int newLocation ) 14 { 15 panel = newPanel; 16 mark = newMark; 17 location = newLocation; 18 } // end constructor 19 20 // property SquarePanel; the panel which the square represents 21 public Panel SquarePanel 22 { 23 get 24 { 25 return panel; 26 } // end get 27 } // end property SquarePanel 28 29 // property Mark; the mark on the square 30 public char Mark 31 { 32 get 33 { 34 return mark; 35 } // end get 36 set 37 { 38 mark = value; 39 } // end set 40 } // end property Mark 41 42 // property Location; the square's location on the board 43 public int Location 44 { 45 get 46 { 47 return location; 48 } // end get 49 } // end property Location 50 } // end class Square

TicTacToeServerForm Class

TicTacToeServerForm (Fig. 23.5) uses its Load event handler (lines 2737) to create a byte array to store the moves the players have made (line 29). The program creates an array of two references to Player objects (line 30) and an array of two references to Thread objects (line 31). Each element in both arrays corresponds to a Tic-Tac-Toe player. Variable currentPlayer is set to 0 (line 32), which corresponds to player "X". In our program, player "X" makes the first move. Lines 3536 create and start THRead getPlayers, which the TicTacToeServerForm uses to accept connections so that the current Thread does not block while awaiting players.

Lines 4965 define DisplayDelegate and DisplayMessage, allowing any thread to modify displayTextBox's Text property. This time, the DisplayMessage method is declared as internal, so it can be called inside a method of class Player tHRough a TicTacToeServerForm reference.

Thread getPlayers executes method SetUp (lines 6895), which creates a TcpListener object to listen for requests on port 50000 (lines 7375). This object then listens for connection requests from the first and second players. Lines 78 and 84 instantiate Player objects representing the players, and lines 7981 and 8587 create two THReads that execute the Run methods of each Player object.

The Player constructor (Fig. 23.5, lines 160174) receives as arguments a reference to the Socket object (i.e., the connection to the client), a reference to the TicTacToeServerForm object and an int indicating the player number (from which the constructor infers the mark, "X" or "O" used by that player). In this case study, TicTacToeServerForm calls method Run (lines 186253) after instantiating a Player object. Lines 191200 notify the server of a successful connection and send to the client the char that the client will place on the board when making a move. If Run is executing for Player "X", lines 205215 execute, causing Player "X" to wait for a second player to connect. Lines 211212 define a while statement that suspends the Player "X" Thread until the server signals that Player "O" has connected. The server notifies the Player of the connection by setting the Player's threadSuspended variable to false (line 92). When threadSuspended becomes false, Player exits the while statement at lines 211212.

Method Run executes the while statement at lines 21924), enabling the user to play the game. Each iteration of this statement waits for the client to send an int specifying where on the board to place the "X" or "O"the Player then places the mark on the board, if the specified mark location is valid (e.g., that location does not already contain a mark). Note that the while statement continues execution only if bool variable done is false. This variable is set to true by event handler TicTacToeServerForm_FormClosing of class TicTacToeServerForm, which is invoked when the server closes the connection.

Line 222 of Fig. 23.5 begins a while statement that loops until Socket property Available indicates that there is information to receive from the Socket (or until the server disconnects from the client). If there is no information, the Thread goes to sleep for one second. On awakening, the Thread uses property Disconnected to check whether server variable disconnected is true (line 226). If the value is true, the Thread exits the method (thus terminating the Thread); otherwise, the Thread loops again. However, if property Available indicates that there is data to receive, the while statement of lines 222228 terminates, enabling the information to be processed.

This information contains an int representing the location in which the client wants to place a mark. Line 231 calls method ReadInt32 of the BinaryReader object (which reads from the NetworkStream created with the Socket) to read this int. Line 235 then passes the int to TicTacToeServerForm method ValidMove. If this method validates the move, the Player places the mark in the desired location.

Method ValidMove (lines 98127) sends to the client a message indicating whether the move was valid. Locations on the board correspond to numbers from 08 (02 for the top row, 35 for the middle and 68 for the bottom). All statements in method ValidMove are enclosed in a lock statement that allows only one move to be attempted at a time. This prevents two players from modifying the game's state information simultaneously. If the Player attempting to validate a move is not the current player (i.e., the one allowed to make a move), that Player is placed in a Wait state until it is that Player's turn to move. If the user attempts to place a mark on a location that already contains a mark, method ValidMove returns false. However, if the user has selected an unoccupied location (line 108), lines 111112 place the mark on the local representation of the board. Line 118 notifies the other Player that a move has been made, and line 121 invokes the Pulse method so that the waiting Player can validate a move. The method then returns true to indicate that the move is valid.

When a TicTacToeClientForm application (Fig. 23.6) executes, it creates a TextBox to display messages from the server and the Tic-Tac-Toe board representation. The board is created out of nine Square objects (Fig. 23.7) that contain Panels on which the user can click, indicating the position on the board in which to place a mark. The TicTacToeClientForm's Load event handler (lines 3058) opens a connection to the server (line 50) and obtains a reference to the connection's associated NetworkStream object from TcpClient (line 51). Lines 5657 start a thread to read messages sent from the server to the client. The server passes messages (for example, whether each move is valid) to method ProcessMessage (lines 179215). If the message indicates that a move is valid (line 184), the client sets its Mark to the current square (the square that the user clicked) and repaints the board. If the message indicates that a move is invalid (line 190), the client notifies the user to click a different square. If the message indicates that the opponent made a move (line 197), line 200 reads an int from the server specifying where on the board the client should place the opponent's Mark. TicTacToeClientForm includes a delegate/method pair for allowing threads to modify idLabel's Text property (lines 97113), as well as DisplayDelegate and DisplayMesage for modifying displayTextBox's Text property (lines 7793).

Категории