In the C world, there are several mechanisms that 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 Section 24.9. 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.[38]
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 16-6 is the
code for the server.
Example 16-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 + " "); os.flush( ); } sock.close( ); } catch (IOException e) { System.out.println("IO Error on socket " + e); return; } System.out.println("Socket ENDED: " + sock); } } }
There can be a performance issue if there are a lot of
short
transactions, since each client causes the creation of a new threaded
object. If you know or can reliably predict the degree of concurrency
that will be needed, an alternative paradigm involves the
pre-creation 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 there are no clients 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, though it normally creates a
number of identical processes (instead of threads) to handle client
connections. Accordingly, I have modified the
EchoServerThreaded
class shown in Example 16-7 to work this way.
Example 16-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 */ System.err.println("Could not create ServerSocket " + e); System.exit(1); return; /*NOTREACHED*/ } // Create a series of threads and start them. for (int i=0; i<numThreads; i++) { new Thread(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"); // Wait here for the next connection. synchronized(servSock) { Socket clientSocket = servSock.accept( ); } System.out.println(getName( ) + " starting, IP=" + clientSocket.getInetAddress( )); DataInputStream is = new DataInputStream( clientSocket.getInputStream( )); PrintStream os = new PrintStream( clientSocket.getOutputStream( ), true); String line; while ((line = is.readLine( )) != null) { os.print(line + " "); os.flush( ); } System.out.println(getName( ) + " ENDED "); clientSocket.close( ); } catch (IOException ex) { System.out.println(getName( ) + ": IO Error on socket " + ex); return; } } } } }
[38] 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
Section 26.5) using select( )
or
poll( )
.
3.135.247.11