15.9. Playing One-Row Nim Over the NetworkIn the preceding section we developed and tested a generic echo service. It is based on a common root class, ClientServer, which is a subclass of THRead. Both EchoServer and EchoClient extend the root class, and each implements its own version of run(). In this section, we will generalize this design so that it can support a wide range of services. To illustrate the effectiveness of the design, we will use it as the basis for a program that plays One-Row Nim over the Internet.
Designing for extensibility In order to generalize our design, we begin by identifying the elements common to all servers and clients and what is particular to the echo service and client. Clearly, the general server and client protocols defined here in their respective run() methods are something that all servers and clients have in common. What differs from one application to another is the particular service provided and requested, as detailed in their respective provideService() and requestService() methods. In this example, the service provided will be One-Row Nim. The clients that use this service will be (human) players of the game. Therefore, the way to generalize the application is to define the run() method in the generic Server and Client classes. The overall design of the One-Row Nim service will now consist of five classes organized into the hierarchy shown in Figure 15.31. At the root of the hierarchy is the ClientServer class, which contains nothing but I/O methods used by both clients and servers. The abstract Server and Client classes contain implementations of Thread's run() method, which defines the basic protocols for servers and clients, respectively. The details of the particular service are encoded in the provideService() and requestService() methods. Because the run() methods defined in Client and Server call provideService() and requestService(), respectively, these methods must be declared as abstract methods in the Server and Client classes. Any class that contains an abstract method must itself be declared abstract. Figure 15.31. Class hierarchy for a generic client/server application. |
import java.net.*; import java.io.*; public abstract class Server extends ClientServer { protected ServerSocket port; protected Socket socket; public Server(int portNum, int nBacklog) { try { port = new ServerSocket (portNum, nBacklog); } catch (IOException e) { e.printStackTrace(); } // try/catch } // Server() public void run() { try { System.out.println("Server at " + InetAddress.getLocalHost() + " waiting for connections "); while (true) { socket = port.accept(); System.out.println("Accepted a connection from " + socket.getInetAddress()); provideService(socket); socket.close(); System.out.println("Closed the connection\n"); } // while } catch (IOException e) { e.printStackTrace(); } // try/catch } // run() // Implemented in subclass protected abstract void provideService(Socket socket); } // Server class |
import java.net.*; import java.io.*; public abstract class Client extends ClientServer { protected Socket socket; public Client(String url, int port) { try { socket = new Socket(url,port); System.out.println("CLIENT: connected to " + url + ":" + port); } catch (Exception e) { e.printStackTrace(); System.exit(1); } // try/catch block } // Client() public void run() { try { requestService(socket); socket.close(); System.out.println("CLIENT: connection closed"); } catch (IOException e) { System.out.println(e.getMessage()); e.printStackTrace(); } // try/catch block } // run() // Implemented in subclass protected abstract void requestService(Socket socket)throws IOException; protected String readFromKeyboard() throws IOException { BufferedReader input = new BufferedReader (new InputStreamReader(System.in)); System.out.print("INPUT: "); String line = input.readLine(); return line; } // readFromKeyboard() } // Client class |
Effective Design: Polymorphism
![]() | Defining a method as abstract within a superclass, and implementing it in various ways in subclasses, is an example of polymorphism. Polymorphism is a powerful object-oriented design technique. |
Given the abstract definition of the Server class, defining a new service is simply a matter of extending Server and implementing the provideService() method in the new subclass. We will name the subclass NimServer.
Extensibility
Figure 15.34 provides a definition of the NimServer subclass. Note how its implementation of provideService() uses an instance of the OneRowNim class from Chapter 8. The play() method, which encapsulates the game-playing algorithm, similarly uses an instance of NimPlayer from Chapter 8. As you will recall, OneRowNim is a TwoPlayerGame, and NimPlayer defines methods that allow a computer to play an optimal game of One-Row Nim. In this example, the server acts as one of the players and uses a NimPlayer to manage its playing strategy. Thus, clients that connect to NimServer will be faced by a computer that plays an optimal game.
import java.net.*; import java.io.*; public class NimServer extends Server { public NimServer(int port, int backlog) { super(port, backlog); } protected void provideService (Socket socket) { OneRowNim nim = new OneRowNim(); try { writeToSocket(socket, "Hi Nim player. You're Player 1 and I'm Player 2. " + nim.reportGameState() + " " + nim.getGamePrompt() + "\n"); play(nim, socket); } catch (IOException e) { e.printStackTrace(); } // try/catch } // provideService() private void play(OneRowNim nim, Socket socket) throws IOException { NimPlayer computerPlayer = new NimPlayer(nim); nim.addComputerPlayer(computerPlayer); String str="", response=""; int userTakes = 0, computerTakes = 0; do { str = readFromSocket(socket); boolean legalMove = false; do { userTakes = Integer.parseInt(str); if (nim.takeSticks(userTakes)) { legalMove = true; nim.changePlayer(); response = nim.reportGameState() + " "; if (!nim.gameOver()) { computerTakes = Integer.parseInt(computerPlayer.makeAMove("")); response = response + " My turn. I take " + computerTakes + " sticks. "; nim.takeSticks(computerTakes); nim.changePlayer(); response = response + nim.reportGameState() + " "; if (!nim.gameOver()) response = response + nim.getGamePrompt(); } // if not game over writeToSocket(socket, response); } else { writeToSocket(socket, "That's an illegal move. Try again.\n"); str = readFromSocket(socket); } // if user takes } while (!legalMove); } while (!nim.gameOver()); } // play // Overriding writeToSocket to remove \n from str protected void writeToSocket(Socket soc, String str) throws IOException { |
If you compare the details of the NimServer's play() method with the play() method from the implementation of OneRowNim, you will see that they are very similar. In this implementation, we use public methods of the OneRowNim object to manage the playing of the game. Thus, addComputerPlayer() adds an instance of NimPlayer to the game. The takeSticks(), changePlayer(), and gameOver() methods are used to manage the moves made by both itself and the client. And the getGamePrompt() and reportGameState() methods are used to manage the interaction and communication with the client. Note that whenever it is the server's turn to move, it uses the NimPlayer's makeAMove() method to determine the optimal move to make.
Although the programming logic employed in the play() method looks somewhat complex, it is very similar to the logic employed in the Chapter 8 version of the game. The main difference here is that the server uses the writeToSocket() and readFromSocket() methods to manage the communication with the client. In this regard, this instance of provideService() is no different than the provideService() method we used in the EchoServer class.
Finally, note that NimServer provides an implementation of the writeToSocket() method. This method is implemented in the ClientServer() class and is inherited by NimServer. However, the default implementation assumes that the client will use a carriage return (\n) to determine the end of a particular message in the socket. Because OneRowNim's methods, getGamePrompt() and reportGameState(), contain embedded carriage returns, it is necessary to filter these. The new version of writeToSocket() performs this filtering and calls the default method, super.writeToSocket(), after it has finished its filtering task.
Overriding a method
The NimClient class is even easier to implement. As its task is simply to manage the communication between the human user and the NimServer, it is very similar to the requestService() method we used in EchoClient. The relationship between the abstract Client class (Fig. 15.33) and its extension in NimClient (Fig. 15.35) is very similar to the relationship between Server and NimServer. The requestService() method is called by Client.run(). It is implemented in NimClient. In this way, the Client class can serve as a superclass for any number of clients. New clients for new services can be derived from Client by simply implementing their own requestService() method.
import java.net.*; import java.io.*; public class NimClient extends Client { private KeyboardReader kb = new KeyboardReader(); public NimClient( String url, int port) { super(url,port); } protected void requestService(Socket socket) throws IOException { String servStr = readFromSocket(socket); // Get server's response kb.prompt("NIM SERVER: " + servStr +"\n"); // Report server's response if ( servStr.substring(0,6).equals("Hi Nim") ) { String userStr = ""; do { userStr = kb.getKeyboardInput(); // Get user's move writeToSocket(socket, userStr + "\n"); // Send it to server servStr = readFromSocket(socket); // Read server's response kb.prompt("NIM SERVER: " + servStr + "\n"); // Report response } while(servStr.indexOf("Game over!") == -1); // Until game over } } // requestService() public static void main(String args[]) { NimClient client = new NimClient("localhost", 10001); client.start(); } // main() } // NimClient class |
Creating new clients
Effective Design: Inheritance
![]() | By placing as much functionality as possible into a generic client/server superclass, you can simplify the creation of new services. This is an effective use of Java's inheritance mechanism. |
Testing the One-Row Nim service will be no different than testing the Echo service. To test the service, you want to run both NimServer and NimClient at the same time and preferably on different computers. As they are currently coded, you will have to modify the main() methods of both NimServer and NimClient to provide the correct URL and port for your environment.
Exercise 15.7 | The design of the client/server hierarchy makes it easy to create a new service by extending the Server and Client classes. Describe how you would implement a scramble service with this model. A scramble service can be used to solve the daily scramble puzzles found in many newspapers. Given a string of letters, the scramble service will return a string containing all possible letter combinations. For example, given "cat", the scramble service will return "act atc cat cta tac tca". |
Exercise 15.8 | Describe what happens when each of the following errors is introduced into the EchoClient or EchoServer program:
|