Recipe 17.4 Handling Multiple Clients


Problem

Your server needs to handle multiple clients.

Solution

Use a thread for each.

Discussion

In the C world, several mechanisms allow a server to handle multiple clients. One is to use a special " system call" select( ) or poll( ), which notifies the server when any of a set of file/socket descriptors is ready to read, ready to write, or has an error. By including its rendezvous socket (equivalent to our ServerSocket) in this list, the C-based server can read from any of a number of clients in any order. Java does not provide this call, as it is not readily implementable on some Java platforms. Instead, Java uses the general-purpose Thread mechanism, as described in Recipe 24.10. Threads are, in fact, one of the other mechanisms available to the C programmer on most platforms. Each time the code accepts a new connection from the ServerSocket, it immediately constructs and starts a new thread object to process that client.[2]

[2] There are some limits to how many threads you can have, which affect only very large, enterprise-scale servers. You can't expect to have thousands of threads running in the standard Java runtime. For large, high-performance servers, you may wish to resort to native code (see Recipe 26.5) using select( ) or poll( ).

The code to implement accepting on a socket is pretty simple, apart from having to catch IOExceptions:

/** Run the main loop of the Server. */ void runServer(  ) {     while (true) {         try {             Socket clntSock = sock.accept(  );             new Handler(clntSock).start(  );         } catch(IOException e) {             System.err.println(e);         }     } }

To use a thread, you must either subclass Thread or implement Runnable. The Handler class must be a subclass of Thread for this code to work as written; if Handler instead implemented the Runnable interface, the code would pass an instance of the Runnable into the constructor for Thread, as in:

Thread t = new Thread(new Handler(clntSock)); t.start(  );

But as written, Handler is constructed using the normal socket returned by the accept( ) call, and normally calls the socket's getInputStream( ) and getOutputStream( ) methods and holds its conversation in the usual way. I'll present a full implementation, a threaded echo client. First, a session showing it in use:

$ java  EchoServerThreaded EchoServerThreaded ready for connections. Socket starting: Socket[addr=localhost/127.0.0.1,port=2117,localport=7] Socket starting: Socket[addr=darian/192.168.1.50,port=13386,localport=7] Socket starting: Socket[addr=darian/192.168.1.50,port=22162,localport=7] Socket ENDED: Socket[addr=darian/192.168.1.50,port=22162,localport=7] Socket ENDED: Socket[addr=darian/192.168.1.50,port=13386,localport=7] Socket ENDED: Socket[addr=localhost/127.0.0.1,port=2117,localport=7]

Here, I connected to the server once with my EchoClient program and, while still connected, called it up again (and again) with an operating system-provided Telnet client. The server communicated with all the clients concurrently, sending the answers from the first client back to the first client, and the data from the second client back to the second client. In short, it works. I ended the sessions with the end-of-file character in the program and used the normal disconnect mechanism from the Telnet client. Example 17-6 is the code for the server.

Example 17-6. EchoServerThreaded.java
/**  * Threaded Echo Server, sequential allocation scheme.  */ public class EchoServerThreaded {     public static final int ECHOPORT = 7;     public static void main(String[] av)     {         new EchoServerThreaded(  ).runServer(  );     }     public void runServer(  )     {         ServerSocket sock;         Socket clientSocket;         try {             sock = new ServerSocket(ECHOPORT);                      System.out.println("EchoServerThreaded ready for connections.");             /* Wait for a connection */             while(true){                 clientSocket = sock.accept(  );                 /* Create a thread to do the communication, and start it */                 new Handler(clientSocket).start(  );             }         } catch(IOException e) {             /* Crash the server if IO fails. Something bad has happened */             System.err.println("Could not accept " + e);             System.exit(1);         }     }     /** A Thread subclass to handle one client conversation. */     class Handler extends Thread {         Socket sock;         Handler(Socket s) {             sock = s;         }         public void run(  )          {             System.out.println("Socket starting: " + sock);             try {                 DataInputStream is = new DataInputStream(                     sock.getInputStream(  ));                 PrintStream os = new PrintStream(                     sock.getOutputStream(  ), true);                 String line;                 while ((line = is.readLine(  )) != null) {                     os.print(line + "\r\n");                     os.flush(  );                 }                 sock.close(  );             } catch (IOException e) {                 System.out.println("IO Error on socket " + e);                 return;             }             System.out.println("Socket ENDED: " + sock);         }     } }

A lot of short transactions can degrade performance since each client causes the creation of a new threaded object. If you know or can reliably predict the degree of concurrency that is needed, an alternative paradigm involves the precreation of a fixed number of threads. But then how do you control their access to the ServerSocket? A look at the ServerSocket class documentation reveals that the accept( ) method is not synchronized, meaning that any number of threads can call the method concurrently. This could cause bad things to happen. So I use the synchronized keyword around this call to ensure that only one client runs in it at a time, because it updates global data. When no clients are connected, you will have one (randomly selected) thread running in the ServerSocket object's accept( ) method, waiting for a connection, plus n-1 threads waiting for the first thread to return from the method. As soon as the first thread manages to accept a connection, it goes off and holds its conversation, releasing its lock in the process so that another randomly chosen thread is allowed into the accept( ) method. Each thread's run( ) method has an indefinite loop beginning with an accept( ) and then holding the conversation. The result is that client connections can get started more quickly, at a cost of slightly greater server startup time. Doing it this way also avoids the overhead of constructing a new Handler or Thread object each time a request comes along. This general approach is similar to what the popular Apache web server does, although it normally creates a number or pool of identical processes (instead of threads) to handle client connections. Accordingly, I have modified the EchoServerThreaded class shown in Example 17-6 to work this way, as you can see in Example 17-7.

Example 17-7. EchoServerThreaded2.java
/**  * Threaded Echo Server, pre-allocation scheme.  */ public class EchoServerThreaded2 {     public static final int ECHOPORT = 7;     public static final int NUM_THREADS = 4;     /** Main method, to start the servers. */     public static void main(String[] av)     {         new EchoServerThreaded2(ECHOPORT, NUM_THREADS);     }     /** Constructor */     public EchoServerThreaded2(int port, int numThreads)     {         ServerSocket servSock;         Socket clientSocket;         try {             servSock = new ServerSocket(ECHOPORT);                  } catch(IOException e) {             /* Crash the server if IO fails. Something bad has happened */             throw new RuntimeException("Could not create ServerSocket " + e);         }         // Create a series of threads and start them.         for (int i=0; i<numThreads; i++) {             new Handler(servSock, i).start(  );         }     }     /** A Thread subclass to handle one client conversation. */     class Handler extends Thread {         ServerSocket servSock;         int threadNumber;         /** Construct a Handler. */         Handler(ServerSocket s, int i) {             super(  );             servSock = s;             threadNumber = i;             setName("Thread " + threadNumber);         }         public void run(  )          {             /* Wait for a connection */             while (true){                 try {                     System.out.println( getName(  ) + " waiting");                     Socket clientSocket;                     // Wait here for the next connection.                     synchronized(servSock) {                          clientSocket = servSock.accept(  );                     }                     System.out.println(getName(  ) + " starting, IP=" +                          clientSocket.getInetAddress(  ));                     BufferedReader is = new BufferedReader(new InputStreamReader(                         clientSocket.getInputStream(  ));                     PrintStream os = new PrintStream(                         clientSocket.getOutputStream(  ), true);                     String line;                     while ((line = is.readLine(  )) != null) {                         os.print(line + "\r\n");                         os.flush(  );                     }                     System.out.println(getName(  ) + " ENDED ");                     clientSocket.close(  );                 } catch (IOException ex) {                     System.out.println(getName(  ) + ": IO Error on socket " + ex);                     return;                 }             }         }     } }



Java Cookbook
Java Cookbook, Second Edition
ISBN: 0596007019
EAN: 2147483647
Year: 2003
Pages: 409
Authors: Ian F Darwin

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net