Skip to main content

Section 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.

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 so they can be easily extended to support byte-oriented, two-way communications, should that type of service become needed.

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 that 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:

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 doesn't 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 that they are running 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

Subsection 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.

Subsection 15.8.2 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've 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.

Therefore, let's define the ClientServer class as a subclass of Thread(Figure 15.8.2). 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'll define run() in both the Client and Server subclasses.

Figure 15.8.2. ClientServer as a subclass of Thread

For now, the only methods contained in ClientServer(Listing 15.8.3) are the two I/O methods we designed. The only modification we have made 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.

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 that takes place between one of our clients and servers will be line oriented.

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
Listing 15.8.3. The ClientServer class serves as the superclass for client/server applications.

Subsection 15.8.3 The EchoServer Class

Let's now develop a design for the echo server. This class will be a subclass of ClientServer(Figure 15.8.5)).

Figure 15.8.5. Design of the EchoServer class.
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 then 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

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. That is, 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()

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 developed. The implementation of this method is shown, along with the complete implementation of EchoServer, in Listing 15.8.7.

The protocol used by EchoServer.provideService() 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 that was 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.”

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'll discuss the details of this change later.

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
Listing 15.8.7. EchoServer simply echoes the client's message.

Subsection 15.8.4 The EchoClient Class

The EchoClient class is just as easy to design (Fig 15.8.8). It, too, will be a subclass of ClientServer. It needs an instance variable for the Socket that 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.

Figure 15.8.8. EchoClient Class
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

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"

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. Like for the provideService() method, this design is another example of the encapsulation principle:

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, writing it to the socket, then reading the server's response, and displaying it on System.out.

Note that the task of reading user input from the keyboard has been made into a separate method, which is one we've 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 the run(), which is shown with the complete definition of EchoClient in Listing 15.8.10. The run() method can simply call the requestService() 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 that exception.

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
Listing 15.8.10. The EchoClient class prompts the user for a string and sends it to the EchoServer, which simply echoes it back.

Subsection 15.8.5 Testing the Echo Service

Both EchoServer and EchoClient contain main() methods ( Listing 15.8.7 and Listing 15.8.10). 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 numbers—those below 1024—and it cannot use a port that's 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 trying to introduce 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
You have attempted of activities on this page.