Serving multiple clients

Bruce Eckel’s Thinking in Java Contents | Prev | Next

The
basic scheme is to make a single
ServerSocket
in the server and call
accept( )
to wait for a new connection. When
accept( )
returns, you take the resulting
Socket
and use it to create a new thread whose job is to serve that particular client.
Then you call
accept( )
again to wait for a new client.

In
the following server code, you can see that it looks similar to the
JabberServer.java
example except that all of the operations to serve a particular client have
been moved inside a separate thread class:

//: MultiJabberServer.java
// A server that uses multithreading to handle 
// any number of clients.
import java.io.*;
import java.net.*;
 
class ServeOneJabber extends Thread {
  private Socket socket;
  private BufferedReader in;
  private PrintWriter out;
  public ServeOneJabber(Socket s)
      throws IOException {
    socket = s;
    in =
      new BufferedReader(
        new InputStreamReader(
          socket.getInputStream()));
    // Enable auto-flush:
    out =
      new PrintWriter(
        new BufferedWriter(
          new OutputStreamWriter(
            socket.getOutputStream())), true);
    // If any of the above calls throw an 
    // exception, the caller is responsible for
    // closing the socket. Otherwise the thread
    // will close it.
    start(); // Calls run()
  }
  public void run() {
    try {
      while (true) {
        String str = in.readLine();
        if (str.equals("END")) break;
        System.out.println("Echoing: " + str);
        out.println(str);
      }
      System.out.println("closing...");
    } catch (IOException e) {
    } finally {
      try {
        socket.close();
      } catch(IOException e) {}
    }
  }
}
 
public class MultiJabberServer {
  static final int PORT = 8080;
  public static void main(String[] args)
      throws IOException {
    ServerSocket s = new ServerSocket(PORT);
    System.out.println("Server Started");
    try {
      while(true) {
        // Blocks until a connection occurs:
        Socket socket = s.accept();
        try {
          new ServeOneJabber(socket);
        } catch(IOException e) {
          // If it fails, close the socket,
          // otherwise the thread will close it:
          socket.close();
        }
      }
    } finally {
      s.close();
    }
  }
} ///:~ 

The
ServeOneJabber
thread
takes the
Socket
object that’s produced by
accept( )
in
main( )
every time a new client makes a connection. Then, as before, it creates a
BufferedReader
and auto-flushed
PrintWriter
object using the
Socket.
Finally, it calls the special
Thread
method
start( ),
which performs thread initialization and then calls
run( ).
This performs the same kind of action as in the previous example: reading
something from the socket and then echoing it back until it reads the special
“END” signal.

The
responsibility for cleaning up the socket must again be carefully designed. In
this case, the socket is created outside of the
ServeOneJabber
so the responsibility can be shared. If the
ServeOneJabber
constructor fails, it will just throw the exception to the caller, who will
then clean up the thread. But if the constructor succeeds, then the
ServeOneJabber
object takes over responsibility for cleaning up the thread, in its
run( ).

Notice
the simplicity of the
MultiJabberServer.
As before, a
ServerSocket
is created and
accept( )
is called to allow a new connection. But this time, the return value of
accept( )
(a
Socket)
is passed to the constructor for
ServeOneJabber,
which creates a new thread to handle that connection. When the connection is
terminated, the thread simply goes away.

If
the creation of the
ServerSocket
fails, the exception is again thrown through
main( ).
But if it succeeds, the outer
try-finally
guarantees its cleanup. The inner
try-catch
guards only against the failure of the
ServeOneJabber
constructor; if the constructor succeeds, then the
ServeOneJabber
thread will close the associated socket.

To
test that the server really does handle multiple clients, the following program
creates many clients (using threads) that connect to the same server. Each
thread has a limited lifetime, and when it goes away, that leaves space for the
creation of a new thread. The maximum number of threads allowed is determined
by the
final
int maxthreads
.
You’ll notice that this value is rather critical, since if you make it
too high the threads seem to run out of resources and the program mysteriously
fails.

//: MultiJabberClient.java
// Client that tests the MultiJabberServer
// by starting up multiple clients.
import java.net.*;
import java.io.*;
 
class JabberClientThread extends Thread {
  private Socket socket;
  private BufferedReader in;
  private PrintWriter out;
  private static int counter = 0;
  private int id = counter++;
  private static int threadcount = 0;
  public static int threadCount() {
    return threadcount;
  }
  public JabberClientThread(InetAddress addr) {
    System.out.println("Making client " + id);
    threadcount++;
    try {
      socket =
        new Socket(addr, MultiJabberServer.PORT);
    } catch(IOException e) {
      // If the creation of the socket fails, 
      // nothing needs to be cleaned up.
    }
    try {
      in =
        new BufferedReader(
          new InputStreamReader(
            socket.getInputStream()));
      // Enable auto-flush:
      out =
        new PrintWriter(
          new BufferedWriter(
            new OutputStreamWriter(
              socket.getOutputStream())), true);
      start();
    } catch(IOException e) {
      // The socket should be closed on any 
      // failures other than the socket 
      // constructor:
      try {
        socket.close();
      } catch(IOException e2) {}
    }
    // Otherwise the socket will be closed by
    // the run() method of the thread.
  }
  public void run() {
    try {
      for(int i = 0; i < 25; i++) {
        out.println("Client " + id + ": " + i);
        String str = in.readLine();
        System.out.println(str);
      }
      out.println("END");
    } catch(IOException e) {
    } finally {
      // Always close it:
      try {
        socket.close();
      } catch(IOException e) {}
      threadcount--; // Ending this thread
    }
  }
}
 
public class MultiJabberClient {
  static final int MAX_THREADS = 40;
  public static void main(String[] args)
      throws IOException, InterruptedException {
    InetAddress addr =
      InetAddress.getByName(null);
    while(true) {
      if(JabberClientThread.threadCount()
         < MAX_THREADS)
        new JabberClientThread(addr);
      Thread.currentThread().sleep(100);
    }
  }
} ///:~ 

The
JabberClientThread
constructor takes an
InetAddress
and uses it to open a
Socket.
You’re probably starting to see the pattern: the
Socket
is always used to create some kind of
Reader
and/or
Writer
(or
InputStream
and/or
OutputStream)
object, which is the only way that the
Socket
can be used. (You can, of course, write a class or two to automate this process
instead of doing all the typing if it becomes painful.) Again,
start( )
performs thread initialization and calls
run( ).
Here, messages are sent to the server and information from the server is echoed
to the screen. However, the thread has a limited lifetime and eventually
completes. Note that the socket is cleaned up if the constructor fails after
the socket is created but before the constructor completes. Otherwise the
responsibility for calling
close( )
for the socket is relegated to the
run( )
method.

The
threadcount
keeps track of how many
JabberClientThread
objects currently exist. It is incremented as part of the constructor and
decremented as
run( )
exits (which means the thread is terminating). In
MultiJabberClient.main( ),
you can see that the number of threads is tested, and if there are too many, no
more are created. Then the method sleeps. This way, some threads will
eventually terminate and more can be created. You can experiment with
MAX_THREADS
to see where your particular system begins to have trouble with too many
connections.

More by Author

Must Read