Principle 15.8.1. EFFECTIVE DESIGN: Infinite Loop.
A server is an application that’s designed to run in an infinite loop. The loop should be exited only when some kind of exception occurs.
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.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
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
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.ClientServer
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.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.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.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.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.EchoServer
Class
ClientServer
(Figure 15.8.5)). 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
IOException
. Note also that we have included a stub version of run()
, which we want to define in this class.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()
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.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.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.”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.EchoClient
Class
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. 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
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"
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: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()
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
.protected String readFromKeyboard() throws IOException {
BufferedReader input = new BufferedReader(
new InputStreamReader(System.in));
System.out.print("INPUT: ");
String line = input.readLine();
return line;
}// readFromKeyboard()
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.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.)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()
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()
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