Section 15.8. CASE STUDY: Generic ClientServer Classes


[Page 736 (continued)]

15.8. CASE STUDY: Generic Client/Server Classes

Suppose your boss asks you to set up generic client/server classes that can be used to implement a number of related client/server applications. One application that the company has in mind is a query service in which the client would send a query string to the server, and the server would interpret the string and return a string that provides the answer. For example, the client might send the query "Hours of service", and the client would respond with the company's business hours.

Problem statement


Another application the company wants will enable the client to fill out an order form and transmit it as a string to the server. The server will interpret the order, fill it, and return a receipt, including instructions as to when the customer will receive the order.

All of the applications to be supported by this generic client/server will communicate via strings, so something very much like the readFromSocket() and writeToSocket() methods can be used for their communication. Of course, you want to design classes that can be easily extended to support byte-oriented, two-way communications should that type of service be needed.

The echo service


In order to test the generic models, we will subclass them to create a simple echo service. This service will echo back to the client any message the server receives. For example, we'll have the client accept keyboard input from the user and then send the user's input to the server and simply report what the server returns. The following shows the output generated by a typical client session:


[Page 737]

CLIENT: connected to 'java.cs.trincoll.edu' SERVER: Hello, how may I help you? CLIENT: type a line or 'goodbye' to quit INPUT: hello SERVER: You said 'hello' INPUT: this is fun SERVER: You said 'this is fun' INPUT: java java java SERVER: You said 'java java java' INPUT: goodbye SERVER: Goodbye CLIENT: connection closed 


On the server side, the client's message will be read from the input stream and then simply echoed back (with some additional characters attached) through the output stream. The server does not display a trace of its activity other than to report when connections are established and closed. We will code the server in an infinite loop so that it will accept connections from a (potentially) endless stream of clients. In fact, most servers are coded in this way. They are designed to run forever and must be restarted whenever the host they are running on needs to be rebooted. The output from a typical server session is as follows:

Echo server at java.cs.trincoll.edu/157.252.16.21 waiting for connections Accepted a connection from java.cs.trincoll.edu/157.252.16.21 Closed the connection Accepted a connection from java.cs.trincoll.edu/157.252.16.21 Closed the connection 


Effective Design: Infinite Loop

A server is an application designed to run in an infinite loop. The loop should be exited only when some kind of exception occurs.


15.8.1. Object-Oriented Design

A suitable solution for this project will make extensive use of object-oriented design principles. We want Server and Client classes that can easily be subclassed to support a wide variety of services. The solution should make appropriate use of inheritance and polymorphism in its design. Perhaps the best way to develop our generic class is first to design the echo service as a typical example and then generalize it.

The Threaded Root Subclass: ClientServer

One lesson we can draw at the outset is that both clients and servers use basically the same socket I/O methods. Thus, as we have seen, the readFromSocket() and writeToSocket() methods could be used by both clients and servers. Because we want all clients and servers to inherit these methods, they must be placed in a common superclass. Let's name this the ClientServer class.

Where should we place this class in the Java hierarchy? Should it be a direct subclass of Object, or should it extend some other class that would give it appropriate functionality? One feature that would make our clients and servers more useful is if they were independent threads. That way they could be instantiated as part of another object and given the subtask of communicating on behalf of that object.


[Page 738]

Therefore, let's define the ClientServer class as a subclass of Thread (Fig. 15.25). Recall from Chapter 14 that the typical way to derive functionality from a Thread subclass is to override the run() method. The run() method will be a good place to implement the client and server protocols. Because they are different, we will define run() in both the Client and Server subclasses.

Figure 15.25. Overall design of a client/server application.


For now, the only methods contained in ClientServer (Fig. 15.26) are the two I/O methods we designed. The only modification to the methods occurs in the writeToSocket() method, where we have added code to make sure that any strings written to the socket are terminated with an end-of-line character.


[Page 739]
Figure 15.26. The ClientServer class serves as the superclass for client/server applications.
(This item is displayed on page 738 in the print version)

import java.io.*; import java.net.*; public class ClientServer extends Thread {   protected InputStream iStream;  // Instance variables   protected OutputStream oStream;   protected String readFromSocket(Socket sock)throws IOException {     iStream = sock.getInputStream();     String str="";     char c;     while ( ( c = (char) iStream.read() ) != '\n')       str = str + c + "";     return str;   }   protected void writeToSocket(Socket sock, String str)throws IOException {     oStream = sock.getOutputStream();     if (str.charAt( str.length() - 1 ) != '\n')       str = str + '\n';     for (int k = 0; k < str.length() ; k++)       oStream.write(str.charAt(k));   } // writeToSocket() } // ClientServer class 

This is an important enhancement, because the read loop in the readFromSocket() method expects to receive an end-of-line character. Rather than rely on specific clients to guarantee that their strings end with \n, our design takes care of this problem for them. This ensures that every communication between one of our clients and servers will be line oriented.

Effective Design: Defensive Design

Code that performs I/O, whether across a network or otherwise, should be designed to anticipate and remedy common errors. This will lead to more robust programs.


15.8.2. The EchoServer Class

Let's now develop a design for the echo server. This class will be a subclass of ClientServer (Fig. 15.27). As we saw in discussing the server protocol, one task that echo server will do is create a ServerSocket and establish a port number for its service. Then it will wait for a Socket connection, and once a connection is accepted, the echo server will communicate with the client. This suggests that our server needs at least two instance variables. It also suggests that the task of creating a ServerSocket would be an appropriate action for its constructor method. This leads to the following initial definition:

import java.net.*; import java.io.*; public class EchoServer extends ClientServer {     private ServerSocket port;     private Socket socket;     public EchoServer(int portNum, int nBacklog) {       try {         port = new ServerSocket (portNum, nBacklog);       } catch (IOException e) {         e.printStackTrace();       }     }     public void run() { } // Stub method } // EchoServer class 


Figure 15.27. Design of the EchoServer class.


What data do we need?



[Page 740]

Note that the constructor method catches the IOException. Note also that we have included a stub version of run(), which we want to define in this class.

Once EchoServer has set up a port, it should issue the port.accept() method and wait for a client to connect. This part of the server protocol belongs in the run() method. As we have said, most servers are designed to run in an infinite loop. They don't just handle one request and then quit. Instead, once started (usually by the system), they repeatedly handle requests until deliberately stopped by the system. This leads to the following run algorithm:

public void run() {    try {      System.out.println("Echo 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");      }    } catch (IOException e) {       e.printStackTrace();    } } // run() 


The server algorithm


For simplicity, we are printing the server's status messages on System.out. Ordinarily these should go to a log file. Note also that the details of the actual service algorithm are hidden in the provideService() method.

As described earlier, the provideService() method consists of writing a greeting to the client and then repeatedly reading a string from the input stream and echoing it back to the client via the output stream. This is easily done using the writeToSocket() and readFromSocket() methods we have developed. The implementation of this method is shown, along with the complete implementation of EchoServer, in Figure 15.28.

Figure 15.28. EchoServer simply echoes the client's message.
(This item is displayed on page 741 in the print version)

import java.net.*; import java.io.*; public class EchoServer extends ClientServer {    private ServerSocket port;    private Socket socket;    public EchoServer( int portNum, int nBacklog) {      try {        port = new ServerSocket (portNum, nBacklog);      } catch (IOException e) {        e.printStackTrace();      }    } // EchoServer()    public void run() {      try {        System.out.println("Echo 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()    protected void provideService (Socket socket) {      String str="";      try {        writeToSocket(socket, "Hello, how may I help you?\n");        do {          str = readFromSocket(socket);          if (str.toLowerCase().equals("goodbye"))            writeToSocket(socket, "Goodbye\n");          else            writeToSocket( socket, "You said '" + str + "'\n");        }  while (!str.toLowerCase().equals("goodbye"));      } catch (IOException e) {        e.printStackTrace();      } // try/catch    } // provideServer()    public static void main(String args[]) {        EchoServer server = new EchoServer(10001,3);        server.start();    } // main() } // EchoServer class 

The protocol used by EchoServer's provideService() method starts by saying "hello" and loops until the client says "goodbye". When the client says "goodbye", the server responds with "goodbye". In all other cases it responds with "You said X", where X is the string received from the client. Note the use of the toLowerCase() method to convert client messages to lowercase. This simplifies the task of checking for "goodbye" by removing the necessity of checking for different spellings of "Goodbye".

Effective Design: Defensive Design

Converting I/O to lowercase helps to minimize miscommunication between client and server and leads to a more robust protocol.


This completes the design of the EchoServer. We have deliberately designed it in a way that will make it easy to convert into a generic server. Hence, we have the motivation for using provideService() as the name of the method that provides the echo service. In order to turn EchoServer into a generic Server class, we can simply make provideService() an abstract method, leaving its implementation to the Server subclasses. We will discuss the details of this change later.


[Page 741]

Designing for extensibility



[Page 742]

15.8.3. The EchoClient Class

The EchoClient class is just as easy to design (Fig. 15.29). It, too, will be a subclass of ClientServer. It needs an instance variable for the Socket it will use, and its constructor should be responsible for opening a socket connection to a particular server and port. The main part of its protocol should be placed in the run() method. The initial definition is as follows:

import java.net.*; import java.io.*; public class EchoClient extends ClientServer {      protected Socket socket;      public EchoClient(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);        }      } // EchoClient()      public void run() { } // Stub method } // EchoClient class 


Figure 15.29. Design of the EchoClient class.


The constructor method takes two parameters that specify the URL and port number of the echo server. By making these parameters, rather than hard coding them within the method, we give the client the flexibility to connect to servers on a variety of hosts.

As with other clients, EchoClient's run() method will consist of requesting some kind of service from the server. Our initial design called for EchoClient to repeatedly input a line from the user, send the line to the server, and then display the server's response. Thus, for this particular client, the service requested consists of the following algorithm:

Wait for the server to say "hello". Repeat      Prompt and get and line of input from the user.      Send the user's line to the server.      Read the server's response.      Display the response to the user. until the user types "goodbye" 


The client algorithm



[Page 743]

With an eye toward eventually turning EchoClient into a generic client, let's encapsulate this procedure into a requestService() method that we can simply call from the run() method. As in the case of the provideService() method, this design is another example of the encapsulation principle:

Effective Design: Encapsulation

Encapsulating a portion of the algorithm into a separate method makes it easy to change the algorithm by overriding the method.


The requestService() method will take a Socket parameter and perform all the I/O for this particular client:

protected void requestService(Socket socket) throws IOException {    String servStr = readFromSocket(socket);       // Check for "Hello"    System.out.println("SERVER: " + servStr);      // Report the server's response    System.out.println("CLIENT: type a line or 'goodbye' to quit"); // Prompt    if (servStr.substring(0,5).equals("Hello")) {      String userStr = "";      do {        userStr = readFromKeyboard();              // Get input        writeToSocket(socket, userStr + "\n");     // Send it to server        servStr = readFromSocket(socket);          // Read the server's response        System.out.println("SERVER: " + servStr);  // Report server's response      } while (!userStr.toLowerCase().equals("goodbye")); // Until 'goodbye'    } } // requestService() 


Although this method involves several lines, they should all be familiar to you. Each time the client reads a message from the socket, it prints it on System.out. The first message it reads should start with the substring "Hello". This is part of its protocol with the client. Note how the substring() method is used to test for this. After the initial greeting from the server, the client begins reading user input from the keyboard, writes it to the socket, then reads the server's response and displays it on System.out.

Note that the task of reading user input from the keyboard has been made into a separate method that we have used before:

protected String readFromKeyboard() throws IOException {    BufferedReader input = new BufferedReader(new InputStreamReader(System.in));    System.out.print("INPUT: ");    String line = input.readLine();    return line; } // readFromKeyboard() 


The only method remaining to be defined is run(), which is shown with the complete definition of EchoClient in Figure 15.30. The run() method can simply call the request-Service() method. When control returns from the requestService() method, run() closes the socket connection. Because requestService() might throw an IOException, the entire method must be embedded within a try/catch block that catches the exception.


[Page 744]

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

import java.net.*; import java.io.*; public class EchoClient extends ClientServer {   protected Socket socket;   public EchoClient(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);       }    } // EchoClient()   public void run() {     try {         requestService(socket);         socket.close();         System.out.println("CLIENT: connection closed");     } catch (IOException e) {         System.out.println(e.getMessage());         e.printStackTrace();     }   } // run()   protected void requestService(Socket socket) throws IOException {     String servStr = readFromSocket(socket);      // Check for "Hello"     System.out.println("SERVER: " + servStr);     // Report the server's response     System.out.println("CLIENT: type a line or 'goodbye' to quit");// Prompt user     if (servStr.substring(0,5).equals("Hello")) {        String userStr = "";        do {          userStr = readFromKeyboard();            // Get input from user          writeToSocket(socket, userStr + "\n");   // Send it to server          servStr = readFromSocket(socket);        // Read server's response          System.out.println("SERVER: " + servStr);// Report server's response        } while (!userStr.toLowerCase().equals("goodbye"));// Until 'goodbye'     }   } // requestService()   protected String readFromKeyboard( ) throws IOException {     BufferedReader input = new BufferedReader(new InputStreamReader(System.in));     System.out.print("INPUT: ");     String line = input.readLine();     return line;   } // readFromKeyboard()   public static void main(String args[]) {     EchoClient client = new EchoClient("java.trincoll.edu",10001);     client.start();   } // main() } // EchoClient class 


[Page 745]
Testing the Echo Service

Both EchoServer and EchoClient contain main() methods (Figs. 15.28 and 15.30). In order to test the programs, you would run the server on one computer and the client on another computer. (Actually they can both be run on the same computer, although they wouldn't know this and would still access each other through a socket connection.)

The EchoServer must be started first, so that its service will be available when the client starts running. It also must pick a port number. In this case it picks 10001. The only constraint on its choice is that it cannot use one of the privileged port numbersthose below 1024and it cannot use a port that is already in use.

public static void main(String args[]) {     EchoServer server = new EchoServer(10001,3);     server.start(); } // main() 


When an EchoClient is created, it must be given the server's URL (java.trincoll.edu) and the port that the service is using:

public static void main(String args[]) {     EchoClient client = new EchoClient("java.trincoll.edu",10001);     client.start(); } // main() 


As they are presently coded, you will have to modify both EchoServer and EchoClient to provide the correct URL and port for your environment. In testing this program, you might wish to experiment by introducing various errors into the code and observing the results. When you run the service, you should observe something like the following output on the client side:

CLIENT: connected to java.trincoll.edu:10001 SERVER: Hello, how may I help you? CLIENT: type a line or 'goodbye' to quit INPUT:  this is a test SERVER: You said 'this is a test' INPUT:  goodbye SERVER: Goodbye CLIENT: connection closed 





Java, Java, Java(c) Object-Orienting Problem Solving
Java, Java, Java, Object-Oriented Problem Solving (3rd Edition)
ISBN: 0131474340
EAN: 2147483647
Year: 2005
Pages: 275

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net