Multi-Threaded Server Applications: Java Client/Server Programming: Part II

By Qusay H. Mahmoud

Last month, we covered the basic concepts of client/server systems (i.e. sockets), and how to write client programs to interact with existing services. In this second part of the series, we'll discuss threads, and show how they can be used to develop multi-threaded servers. Lastly, we'll look at writing servers that are capable of serving multiple clients simultaneously.

Java and Threads

In its simplest form, a thread of control is a section of the program that is executed independently of other threads of control. A program with multiple threads is able to do multiple tasks simultaneously. In Java, a new thread is generated when we create an instance of the java.lang.Thread class. To use threads in an application, for example, we should inherit from the Thread class as follows:

class MyClass extends Thread {
  ...
}

The thread begins its life when we call its run method (which we must override) from within an object. This thread remains idle until we call its start method. Note that "idle" does not mean the thread has been suspended or stopped. Here is a short example:

class MyApp extends Thread {
  ...
  public void run() {
  ...
  }
}

To use it, we create a thread object and invoke the start method from an instance of MyApp as follows:

MyApp sample = new MyApp();
Thread t1 = new Thread(sample);
t1.start();

Here, the start method is implemented in the superclass, Thread, and is inherited as-is.

In a multi-threaded server, each time a new client requests a service, the server will be invoked from a separate thread. This is not the whole purpose of threads, however. Machines with multiple processors have multiple points of execution, and threads represent an ideal mechanism for allowing programs to take advantage of the available hardware.

The ECHO Server

The TCP/IP suite of protocols specify an ECHO service. An ECHO server merely returns all the data it receives from a client. This may sound useless at first, but the ECHO service is an important mechanism that network managers use to test reachability of remote machines. The ECHO server accepts an incoming connection request, reads the data from the connection, and writes the data back to the client until the client closes the connection.

The ECHO server, which runs on port number 7, is specified for both TCP and UDP. In this article, we will implement our ECHO server on TCP. Note that we'll use a port number greater than 1023 for our ECHO server, because we don't have root privileges or there is already an ECHO server listening on port 7.

First, we'll develop an ECHO server capable of serving one client's request at a time (i.e. the server will die after serving one client), then we'll discuss two ways it can be extended to handle multiple requests.

One Client at a Time

Listing Five (beginning on page X) shows the source code for a simple ECHO server. This server program is simple, and consists of five parts that perform the following:

Open a socket on port 5555, as well as create a socket object from the ServerSocket class to listen and accept connections from clients.

Open an input stream to receive input (data) from the client.

Open an output stream to send replies to the client.

Loop forever, reading data from the client and sending the same data back to the client.

Close the input streams, output streams, and the connection.

To test the echo server, compile it, run it, then telnet to the port on which the echo server is running — 5555 in this case — and type some words to the echo server. You should receive the same stuff back.

Multiple Clients

Now let's see how the previous server program can be extended to handle multiple clients' requests at the same time. You can accomplish this by:

extending the Thread class, or

implementing the Runnable interface.

Using threads. Listing Six (beginning on page XX) shows the source program for a multi-threaded ECHO server. The program shown consists of two classes: echo2 and Connects. Note that both classes inherit from the Thread class. As we mentioned earlier in the brief introduction to threads, when inheriting from the Thread class, we must provide an implementation to the run method — where the life of the thread starts. However, the thread will remain idle until we call its start method.

In the run method of the echo2 class of Listing Six, all we do is create a new connection for each new client. This is done by creating an instance of the Connects class which is responsible for — among other things — opening the input/output streams, and the run method inside it loops forever, reading data from a client and writing the same data back.

Implementing Runnable. In the beginning of the article, we mentioned that one way to create threads is by extending the Thread class. You can also create threads using the Runnable interface. This interface has the following definition:

package java.lang;

public interface Runnable {
  public abstract void run();
}

This approach to creating a thread was introduced in the Java language for a reason. Because Java doesn't support multiple inheritance, we cannot write a multi-threaded applet, for example, because we need to extend both the Applet and Thread classes. For this reason, the Runnable interface was introduced. Following is an explanation of how to use a class that implements the Runnable interface.

Listing Seven (beginning on page XXX) shows an updated version of our ECHO server. The server in Listing Seven is also multi-threaded, however, we did not inherit from the Thread class. Instead, we implemented the Runnable interface. When we created a thread using the Runnable interface, we had to reference the class that implements the Runnable interface. Also, note that we're implementing the Cloneable interface, and making a call to clone. This will create a copy of the class for each connection.

A good question at this point is: What approach should I use for creating threads and multi-threaded servers? Unfortunately, there is no simple answer. However, the following guidelines may help you in deciding which method to use:

If your class only needs to override the run method, then you should implement the Runnable interface.

If your class needs to override and use more than just the run method, then you should extend (inherit from) the Thread class.

If your class needs to extend another class besides the Thread class (which is not allowed in Java), then your class should extend just one class and implement the Runnable interface.

Conclusion

The java.net package provides an easy-to-use API for developing client/server applications. In this article, we discussed how to develop multi-threaded server programs using two approaches: by extending the Thread class and by implementing the Runnable interface.

Begin Listing Five — echo.java
import java.io.*;
import java.net.*;

public class echo {
  public static void main(String args[]) {

    // Declaration section:
    // Declare a server socket and a client socket for
    // the server. Declare an input and an output stream.
    ServerSocket echoServer = null;
    String line;
    BufferedReader is = null;
    DataOutputStream os = null;
    Socket clientSocket = null;
           
    // Try to open a server socket on port 5555.
    // Note that we can't choose a port less than 1023 if
    // we are not privileged users (root).
    try {
      echoServer = new ServerSocket(5555);
    }
    catch (IOException e) {
      System.out.println(e);
    }   

    // Create a socket object from the ServerSocket 
    // to listen and accept connections.
    // Open input and output streams.
    try {
      clientSocket = echoServer.accept();
      is = new BufferedReader(new InputStreamReader(
        clientSocket.getInputStream()));
      os = new DataOutputStream(
        clientSocket.getOutputStream());

      // As long as we receive data, echo that data back to
      // the client.
      for (;;) {  // Loop forever.
        line = is.readLine();
        os.writeBytes(line+"\r\n"); 
      }
    }
    catch (IOException e) {
      System.out.println(e);
    }

    // Close the input/output streams.
    // Close the socket.
    try {
       os.close();
       is.close();
       clientSocket.close();
    }
    catch(IOException ex) {
      ex.printStackTrace();
    }
  }
}

End Listing Five

Begin Listing Six — echo2.java
import java.io.*;
import java.net.*;

public class echo2 extends Thread {

  // Declaration section:
  // Declare a server socket and a client socket for 
  // the server. Declare an input and an output stream.
  ServerSocket echoServer = null;
  BufferedReader is;
  DataOutputStream os;
  Socket clientSocket = null;
           
  // Try to open a server socket on port 9999.
  // Note that we can't choose a port less than 1023 if we
  // are not privileged users (root).
  public echo2() {
    try {
      echoServer = new ServerSocket(5555);
    }
    catch(IOException e) {
      e.printStackTrace();
    }
    this.start();
  }

  // Implements the run method.
  public void run() {
    while(true) {
      try {
        Socket client = echoServer.accept();
        Connects cc = new Connects(client);
      }
      catch (IOException e) {
        System.out.println(e);
      }
    }
  }

  public static void main(String args[]) {
    new echo2();
  }
}

class Connects extends Thread {
  Socket client;
  BufferedReader in;
  DataOutputStream out;
  String line;

  public Connects(Socket s) {  // constructor
    client = s;
    try {
      in = new BufferedReader(
        new InputStreamReader(client.getInputStream()));
      out = new DataOutputStream(client.getOutputStream());
    }
    catch (IOException e) {
      System.out.println(
        "Error while getting socket streams.."+e);
    }
    this.start(); 
    // Thread starts here; this start will call run.
  }
 
  public void run() {
    // As long as we receive data, 
    // echo that data back to the client.
    try {
      while((line = in.readLine()) != null) {
        out.writeBytes(line+"\r\n"); 
      }
    }
    catch(IOException e) {
      e.printStackTrace();
    }

    // Close input/output streams and connection
    // to the client.
    try {
      in.close();
      out.close();
      client.close();
    }
    catch(IOException ex) {
      ex.printStackTrace();
    }
  } 
}

End Listing Six

Begin Listing Seven — echo3.java
import java.io.*;
import java.net.*;

public class echo3 implements Cloneable, Runnable {

  // Declaration section:
  // Declare a server socket and a client socket for 
  // the server. Declare an input and an output stream.
  // Create a null thread.
  ServerSocket echoServer = null;
  BufferedReader is= null;
  DataOutputStream os = null;
  Socket clientSocket = null;
  Thread worker = null;
           
  public synchronized void startEcho() throws IOException {
    if (worker == null) {
      echoServer = new ServerSocket(5555);
      worker = new Thread(this);
      worker.start();
    }
  }

  public void run() {
     Socket client = null;
     if (echoServer != null) { // Original or clone?
       while(true) {
         try {
           client = echoServer.accept();
           echo3 newEcho = (echo3) clone();
           newEcho.echoServer = null;
           newEcho.clientSocket = client;
           newEcho.worker = new Thread(newEcho);
           newEcho.worker.start();
         }
         catch(IOException e) {
           e.printStackTrace();
         }
         catch(CloneNotSupportedException e) {
           e.printStackTrace();
         }
       }
     } 
     else {
       perform(clientSocket);
     }       
  }

  private void perform(Socket client) {
    BufferedReader in = null;
    DataOutputStream out = null;
    String line;
    try {
      in = new BufferedReader(
        new InputStreamReader(client.getInputStream()));
      out = new DataOutputStream(client.getOutputStream());
    } 
    catch (IOException e) {
      System.out.println(
        "Error while getting socket streams.."+e);
    }
  
    // As long as we receive data, echo that 
    // data back to the client.
    try {
      while ((line = in.readLine()) != null) {
        out.writeBytes(line+"\r\n"); 
      }
    } 
    catch(IOException e) {
      e.printStackTrace();
    }

    // Close streams and connection.
    try {
      in.close();
      out.close();
      client.close();
    } 
    catch(IOException ex) {
      ex.printStackTrace();
    }
  }

  public static void main(
    String args[]) throws IOException {

      echo3 echo = new echo3();
      echo.startEcho();
  }
}

End Listing Seven

The files referenced in this article are available for download from the Informant Web site at http://www.informant.com/ji/jinewupl.htm. File name: JI9803QM.ZIP.

Qusay H. Mahmoud is a Sr. Software Engineer in the School of Computer Science at Carleton University. Before joining Carleton he worked as a Software Designer at Newbridge Networks. Qusay holds a B.Sc. in Data Analysis and a Masters degree in Computer Science, both from the University of New Brunswick, Canada. You can reach him at dejavu@acm.org.