18. Sockets and Networking

18.7. 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 ex- ample, 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 clien- t/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 key- board 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:

,,

 

 

 

 

 

 

 

 

 

J

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:

,,

 

 

 

 

J

 

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’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 (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’ll define run() in both the Client and Server subclasses.

For now, the only methods contained in ClientServer (Fig. 15.26)

are the two I/O methods we designed. The only modification we have

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

 

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 ev-

,,

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

J

Figure 15.26: The ClientServer class serves as the superclass for clien- t/server applications.

 

ery communication that takes place between one of our clients and servers will be line oriented.

 

The EchoServer Class

Let’s now develop a design for the echo server. This class will be a sub- class of ClientServer (Fig. 15.27). As we saw in discussing the server protocol, one task that echo server will do is create a ServerSocket

 

Design of the ass.


 

 

 

 

EchoServer

-port : ServerSocket

-socket : Socket

+EchoServer(in por : int, in backlogs : int)

# provideService(in s : Socket)

+run()

 

 

 

 

 

 

 

 

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 needsWhat data do we need?

at least two instance variables. It also suggests that the task of creat- ing a ServerSocket would be an appropriate action for its constructor method. This leads to the following initial definition:

,,

 

 

 

 

 

 

 

 

 

 

 

 

 

J

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 designedThe server algorithm

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:

,,

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Designing for extensibility


J

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 writ- ing 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 Figure 15.28. 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 con- vert 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.

 

 

,,

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

J

Figure 15.28: EchoServer simply echoes the client’s message.

 

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 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. The initial definition is as follows:

,,

 

Figure 15.29:Design of the

EchoClient class.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

The client algorithm


J

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 re- questing 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:

 

,,

 

 

 

 

 

J

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 en- capsulation principle:

 

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

,

 

 

 

 

 

 

 

 

 

 

 

J

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 sub- string “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:

,,

 

 

 

 

 

J

The only method remaining to be defined is the run(), which is shown with the complete definition of EchoClient in Figure 15.30. 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.

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 avail- able 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.

,,

 

 

 

J

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

,,

 

 

 

J

As they are presently coded, you will have to modify both EchoServer and EchoClient to provide the correct URL and port for your environ- ment. 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:

,,

 

 

 

 

 

J