18. Sockets and Networking

18.8. Playing One Row Nim Over the Network

In the previous 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,

 

,,

import j ava . net . ;

import j ava . io . ;

public c l a s s Echo Client extends Clie n t Se r ve r {

protected Socket socket ;

 

public Echo Client ( S t r i n g url , in t port ) {

t r y s {o cket = new Socket ( url , port ) ;

System . out . p r i n t l n ( ”CLIENT : connected to + ur l + ” : + port ) ;

catch ( Exception e ) e . pr int St ac k Tr ac e ( ) ; System . e x i t ( 1 ) ;

}

}// E c h o C l i e n t ( )

public void run ( ) {

t r y {r e q ue s t Se r vi c e ( socket ) ;

socket . c l o s e ( ) ;

System . out . p r i n t l n ( ”CLIENT : connection c losed ) ;

catch ( IOException e )

System . out . p r i n t l n ( e . getMessage ( ) ) ; e . pr int St ac k Tr ac e ( ) ;

}

}// r u n ( )

protected void r e q ue s t Se r vi c e ( Socket socket ) throws IOException

S t r i n g s e r v S t r = readFromSocket ( socket ) ;// C h e c k f o r H e l l o System . out . p r i n t l n ( ”SERVER : + s e r v S t r ) ;// R e p o r t t h e s e r v e r s r e s p o n s e System . out . p r i n t l n ( ”CLIENT : type a l i n e or goodbye to quit ) ; // P r o m p t u s e r i f ( s e r v S t r . s ubs t r ing ( 0 , 5 ) . equals ( Hello ) )

S t r i n g us e r S t r = ”” ;

do u{s e r St r = readFromKeyboard ( ) ;

// G e t i n p u t f r o m u s e r

write To Socket ( socket , us e r S t r + n” ) ;// S e n d i t t o s e r v e r

s e r v S t r = readFromSocket ( socket ) ;// R e a d s e r v e r s r e s p o n s e

System . out . p r i n t l n ( ”SERVER : + s e r v S t r ) ;// R e p o r t s e r v e r s r e s p o n s e

} while ( ! us e r S t r . toLowerCase ( ) . equals ( ”goodbye” ) ) ; // U n t i l g o o d b y e

}

}// r e q u e s t S e r v i c e ( )

protected S t r i n g readFromKeyboard ( ) throws IOException

BufferedReader input = new Buffered Reader ( new InputStreamReader ( System . in ) ) ; System . out . pr i n t ( ”INPUT : ) ;

S t r i n g l i n e = input . readLine ( ) ;

return l i n e ;

}// r e a d F r o m K e y b o a r d ( )

public s t a t i c void main ( S t r i n g args [ ] )

Echo Client c l i e n t = new Echo Client ( j ava . t r i n c o l l . edu” , 1 0 0 0 1 ) ; c l i e n t . s t a r t ( ) ;

}// m a i n ( )

// E c h o C l i e n t

\J

Figure 15.30: The EchoClient class prompts the user for a string and sends it to the EchoServer, which simply echoes it back.

 

Figure 15.31: Class hierarchy for a generic client/server application.

 

 

 

 

 

ClientServer

# iStream : InputStream

# oStream : OutputStream

# readFromSocket(in sock : Socket) : String

# writetoSocket(in sock : Socket, in str : String)

 

 

 

NimClient

# socket : Socket

+NimClient(in url : String, in requestServer : Socket)

# requestService(in s : Socket)

+main()

 

 

 

 

 

 

Designing for extensibility

 

 

 

 

 

 

 

Abstract service methods


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.

In order to generalize our design, we begin by identifying those ele- ments that are common to all servers and clients and what is particular to the echo service and client. Clearly, the general server and client protocols that are defined here in their respective run() methods, are something that all servers and clients have in common. What differs from one appli- cation to another is the particular service provided and requested, as de- tailed in their respective provideService() and requestService() methods. In this example, the service that is provided will be One Row Nim. The clients that use this service will be (human) players of the game. Therefore, the way to generalize this 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 noth- ing but I/O methods used by both clients and servers.The ab- stract Server and Client classes contain implementations of the

 

Thread.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() meth- ods. 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.

Note that we have left the readFromSocket() and writeToSocket() methods in the ClientServer class. These methods are written in

a general way and can be used, without change, by a wide range of clients and servers. If necessary, they can also be overridden by a client or server. In fact, as we will see, the NimServer class does override the writeToSocket() method. Similarly, note that the readFromKeyboard() method is defined in the Client superclass. This is a general method that can be used by a large variety of clients, so it is best if they don’t have to redefine it themselves.

These design decisions lead to the definitions of Server and Client shown in Figures 15.32 and 15.33, respectively. Note that provideService() and requestService() are left unimple- mented. Subclasses of Server, such as NimServer, and subclasses of Client, such as NimClient, can implement provideService() and requestService() in a way that is appropriate for the particular ser- vice they are providing.

 

The NimServer Class

Given the abstract definition of the Server class, defining a new ser-

vice is simply a matter of extending Server and implementing theExtensibility

provideService() method in the new subclass. We will name the subclass NimServer.

 

 

 

 

 

 

 

 

,,

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

J

Figure 15.32: The abstract Server class.

 

 

 

 

 

 

,,

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

J

Figure 15.33: The abstract Client class.

 

,,

import j ava . net . ;

import j ava . io . ;

public c l a s s NimServer extends Server

public NimServer ( in t port , in t backlog )

super ( port , backlog ) ;

}p rotected void provide Service ( Socket socket ) OneRowNim nim = new OneRowNim ( ) ;

t r y

write To Socket ( socket , ”Hi Nim player . You re Player 1 and I ’m Player 2 . + nim . report Game State ( ) + + nim . getGamePrompt ( ) + n” ) ;

play ( nim , socket ) ;

catch ( IOException e ) e . pr int St ac k Tr ac e ( ) ;

} // t r y/ catch

} // provide Service ( )

private void play (OneRowNim nim , Socket socket ) throws IOException NimPlayer computer Player = new NimPlayer ( nim ) ;

nim . addComputerPlayer ( computer Player ) ; S t r i n g s t r =”” , response=”” ;

in t user Takes = 0 , computerTakes = 0 ;

do s{t r = readFromSocket ( socket ) ;

boolean legalMove = fa l s e ;

do u{s er Takes = I n t e ge r . pa r s e Int ( s t r ) ;

i f ( nim . t a k e S t i c k s ( user Takes ) ) legalMove = t rue ;

nim . change Player ( ) ;

response = nim . report Game State ( ) + ;

i f ( ! nim . gameOver ( ) )

computerTakes = I n t e ge r . pa r s e Int ( computer Player . makeAMove( ”” ) ) ;

response = response + ” My turn . I take + computerTakes + s t i c k s . ” ; nim . t a k e S t i c k s ( computerTakes ) ;

nim . change Player ( ) ;

response = response + nim . report Game State ( ) + ;

i f ( ! nim . gameOver ( ) )

response = response + nim . getGamePrompt ( ) ;

// i f not game over

write To Socket ( socket , response ) ;

e ls e

write To Socket ( socket , That ’ s an i l l e g a l move . Try again . n” ) ; s t r = readFromSocket ( socket ) ;

} // i f user takes

} while ( ! legalMove ) ;

} while ( ! nim . gameOver ( ) ) ;

} // play

// Overriding write To Socket to remove n from s t r

protected void write To Socket ( Socket soc , S t r i n g s t r ) throws IOException S t r i n g B u f fe r sb = new S t r i n g B u f fe r ( ) ;

for ( in t k = 0 ; k < s t r . length ( ) ; k++)

i f ( s t r . charAt ( k ) ! = ’ n ’ ) sb . append ( s t r . charAt ( k ) ) ;

super . write To Socket ( soc , sb . t o S t r i n g ( ) + \n” ) ;

}p ublic s t a t i c void main ( S t r i n g args [ ] )

NimServer server = new NimServer ( 1 0 0 0 1 , 5 ) ; server . s t a r t ( ) ;

} // main ( )

\} // NimServer J

Figure 15.34: The NimServer class.

 

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. Also, the play() method, which encapsulates the game-playing algorithm, uses an instance of

 

NimPlayer, also from Chapter 8. You might recall that OneRowNim is a TwoPlayerGame and NimPlayer defines methods that allow a com- puter to play an optimal game of One Row Nim. In this example, the server acts as one of the players and use a NimPlayer to manage its play- ing strategy. Thus, clients that connect to NimServer will be faced by a computer that plays an optimal game.

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() meth- ods 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 also 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 Overriding a method

ClientServer() class and is inherited by NimServer. However, the default implementation assumes that the client will use the a car- riage 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 fil- tering and calls the default method (super.writeToSocket(), after it has finished its filtering task.

 

 

The NimClient Class

 

The NimClient class is even easier to implement. As its task is sim- ply 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 request- Service() 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 derivedCreating new clients

from Client by simply implementing their own requestService()

method.

 

,

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Figure 15.35: The derived NimClient class.

 

 

Testing the Nim Service

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 presently coded, you will have to modify the main() methods of both NimServer and NimClient to provide the correct URL and port for your environment.

SELF-STUDY EXERCISE

EXERCISE 15.7 The design of the client/server hierarchy makes it easy to create a new service by extending both the Server and Client classes. Describe how you would implement a scramble service with this model. A scramble service can be used by people trying to solve the daily scram- ble puzzles found in many newspapers. Given a string of letters, the

 

SECTION 15.10 Java Network Security Restrictions741

scramble service will return a string containing all possible letter com- binations. 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 programs:

Specify the wrong host name when running EchoClient.

Specify the wrong port number when running EchoClient. Remove the reference to \n in the writeToSocket() call in

requestService().