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.
The code to implement accepting on a socket is pretty simple, apart from having to catch
IOException
s:
/** 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;
}
}
}
}
}
|