Recipe 24.10 Program: Threaded Network Server


Problem

You want a network server to be multithreaded.

Solution

Either create a thread when you accept a connection or create a pool of threads in advance and have each wait on the accept( ) call.

Discussion

Networking (see Chapter 16 and Chapter 18) and threads are two very powerful APIs that are a standard part of the Java platform. Used alone, each can increase the reach of your Java programming skills. A common paradigm is a threaded network server, which can either preallocate a certain number of threads or can start a new thread each time a client connects. The big advantage is that each thread can block on read without causing other client threads to delay.

One example of a threaded socket server was discussed in Recipe Recipe 17.4; another is shown here. It seems to be some kind of rite (or wrong) of passage for Java folk to write a web server entirely in Java. This one is fairly small and simple; if you want a full-bodied flavor, check out the Apache Foundation's Apache (written in C) and Tomcat (pure Java) servers (I may be biased because I coauthored O'Reilly's Tomcat: The Definitive Guide, recommended for administering Tomcat). The main program of mine constructs one instance of class Httpd. This creates a socket and waits for incoming clients in the accept( ) method. Each time there is a return from accept( ), we have another client, so we create a new thread to process that client. This happens in the main( ) and runserver( ) methods, which are near the beginning of Example 24-14.

Example 24-14. Httpd.java
import java.net.ServerSocket; import java.net.Socket; import java.util.Properties; import com.darwinsys.util.FileProperties; /**  * A very very simple Web server.  * <p>  * NO SECURITY. ALMOST NO CONFIGURATION. NO CGI. NO SERVLETS.  *<p>  * This version is threaded. I/O is done in Handler.  */ public class Httpd {     /** The default port number */     public static final int HTTP = 80;     /** The server socket used to connect from clients */     protected ServerSocket sock;     /** A Properties, for loading configuration info */     private Properties wsp;     /** A Properties, for loading mime types into */     private Properties mimeTypes;     /** The root directory */     private String rootDir;     public static void main(String argv[]) throws Exception {         System.out.println("DarwinSys JavaWeb Server 0.1 starting...");         Httpd w = new Httpd( );         if (argv.length == 2 && argv[0].equals("-p")) {             w.startServer(Integer.parseInt(argv[1]));         } else {             w.startServer(HTTP);         }         w.runServer( );         // NOTREACHED     }     /** Run the main loop of the Server. Each time a client connects,      * the ServerSocket accept( ) returns a new Socket for I/O, and      * we pass that to the Handler constructor, which creates a Thread,      * which we start.      */     void runServer( ) throws Exception  {         while (true) {                 final Socket clntSock = sock.accept( );                 Thread t = new Thread( ){                     public void run( ) {                         new Handler(Httpd.this).process(clntSock);                     }                 };                 t.start( );         }     }     /** Construct a server object for a given port number */     Httpd( ) throws Exception {         super( );         wsp=new FileProperties("httpd.properties");         rootDir = wsp.getProperty("rootDir", ".");         mimeTypes = new FileProperties(wsp.getProperty("mimeProperties",                     "mime.properties"));     }     public void startServer(int portNum) throws Exception {         String portNumString = null;         if (portNum == HTTP) {             portNumString = wsp.getProperty("portNum");             if (portNumString != null) {                 portNum = Integer.parseInt(portNumString);             }         }         sock = new ServerSocket(portNum);         System.out.println("Listening on port " + portNum);          }     public String getMimeType(String type) {         return mimeTypes.getProperty(type);     }     public String getMimeType(String type, String dflt) {         return mimeTypes.getProperty(type, dflt);     }     public String getServerProperty(String name) {         return wsp.getProperty(name);     }     public String getRootDir( ) {         return rootDir;     } }

The Handler class shown in Example 24-15 is the part that knows the HTTP protocol, or at least a small subset of it. You may notice near the middle that it parses the incoming HTTP headers into a Hashmap but does nothing with them. Here is a log of one connection with debugging enabled (see Recipe 1.11 for information on the Debug class):

Connection accepted from localhost/127.0.0.1 Request: Command GET, file /, version HTTP/1.0 hdr(Connection,Keep-Alive) hdr(User-Agent,Mozilla/4.6 [en] (X11; U; OpenBSD 2.8 i386; Nav)) hdr(Pragma,no-cache) hdr(Host,127.0.0.1) hdr(Accept,image/gif, image/jpeg, image/pjpeg, image/png, */*) hdr(Accept-Encoding,gzip) hdr(Accept-Language,en) hdr(Accept-Charset,iso-8859-1,*,utf-8) Loading file //index.html END OF REQUEST

At this stage of evolution, the server is getting ready to create an HttpServletRequest object, but it is not sufficiently evolved to do so. This file is a snapshot of work in progress. More interesting is the Hashtable used as a cache; to save disk I/O overhead, once a file has been read from disk, the program does not reread it. This means you have to restart the server if you change files; comparing the timestamps (see Recipe 11.1) and reloading files if they have changed is left as an exercise for the reader.

Example 24-15. Handler.java
import java.io.*; import java.net.*; import java.util.*; import com.darwinsys.util.Debug; /** Called from Httpd in a Thread to handle one connection.  * We are created with just a Socket, and read the  * HTTP request, extract a name, read it (saving it  * in Hashtable h for next time), and write it back.  * <p>  * TODO split into general handler stuff and "FileServlet",  *    then handle w/ either user HttpServlet subclasses or FileServlet.  * @version $Id: ch24.xml,v 1.4 2004/05/04 20:14:01 ian Exp $  */ public class Handler {     /** inputStream, from Viewer */     protected BufferedReader is;     /** outputStream, to Viewer */     protected PrintStream os;     /** Main program */     protected Httpd parent;     /** The default filename in a directory. */     protected final static String DEF_NAME = "/index.html";     /** The Hashtable used to cache all URLs we've read.      * Static, shared by all instances of Handler (one Handler per request;      * this is probably quite inefficient, but simple. Need ThreadPool).      * Note that Hashtable methods *are* synchronized.      */     private static Hashtable h = new Hashtable( );     static {     /** Construct a Handler */     Handler(Httpd parent) {         this.parent = parent;     }     protected static final int RQ_INVALID = 0, RQ_GET = 1, RQ_HEAD = 2,         RQ_POST = 3;      public void process(Socket clntSock) {         String request;        // what Viewer sends us.         int methodType = RQ_INVALID;         try {             System.out.println("Connection accepted from " +                 clntSock.getInetAddress( ));             is = new BufferedReader(new InputStreamReader(                 clntSock.getInputStream( )));             // Must do before any chance of errorResponse being called!             os = new PrintStream(clntSock.getOutputStream( ));             request = is.readLine( );             if (request == null || request.length( ) == 0) {                 // No point nattering: the sock died, nobody will hear                 // us if we scream into cyberspace... Could log it though.                 return;             }             // Use a StringTokenizer to break the request into its three parts:             // HTTP method, resource name, and HTTP version             StringTokenizer st = new StringTokenizer(request);             if (st.countTokens( ) != 3) {                 errorResponse(444, "Unparseable input " + request);                 return;             }             String rqCode = st.nextToken( );             String rqName = st.nextToken( );             String rqHttpVer = st.nextToken( );             System.out.println("Request: Command " + rqCode +                     ", file " + rqName + ", version " + rqHttpVer);             // Read headers, up to the null line before the body,             // so the body can be read directly if it's a POST.             HashMap map = new HashMap( );             String hdrLine;             while ((hdrLine = is.readLine( )) != null &&                     hdrLine.length( ) != 0) {                     int ix;                     if ((ix=hdrLine.indexOf(':')) != -1) {                         String hdrName = hdrLine.substring(0, ix);                         String hdrValue = hdrLine.substring(ix+1).trim( );                         Debug.println("hdr", hdrName+","+hdrValue);                         map.put(hdrName, hdrValue);                     } else {                         System.err.println("INVALID HEADER: " + hdrLine);                     }             }             // check that rqCode is either GET or HEAD or ...             if ("get".equalsIgnoreCase(rqCode))                   methodType = RQ_GET;             else if ("head".equalsIgnoreCase(rqCode))                   methodType = RQ_HEAD;             else if ("post".equalsIgnoreCase(rqCode))                   methodType = RQ_POST;             else {                 errorResponse(400, "invalid method: " + rqCode);                 return;             }             // A bit of paranoia may be a good thing...             if (rqName.indexOf("..") != -1) {                 errorResponse(404, "can't seem to find: " + rqName);                 return;             }                              // XXX new MyRequest(clntSock, rqName, methodType);             // XXX new MyResponse(clntSock, os);             // XXX if (isServlet(rqName)) [             //         doServlet(rqName, methodType, map);             // else                 doFile(rqName, methodType == RQ_HEAD, os /*, map */);             os.flush( );             clntSock.close( );         } catch (IOException e) {             System.out.println("IOException " + e);         }         System.out.println("END OF REQUEST");     }     /** Processes one file request */     void doFile(String rqName, boolean headerOnly, PrintStream os) throws IOException {         File f;         byte[] content = null;         Object o = h.get(rqName);         if (o != null && o instanceof byte[]) {             content = (byte[])o;             System.out.println("Using cached file " + rqName);             sendFile(rqName, headerOnly, content, os);         } else if ((f = new File(parent.getRootDir( ) + rqName)).isDirectory( )) {             // Directory with index.html? Process it.             File index = new File(f, DEF_NAME);             if (index.isFile( )) {                 doFile(rqName + DEF_NAME, index, headerOnly, os);                 return;             }             else {                 // Directory? Do not cache; always make up dir list.                 System.out.println("DIRECTORY FOUND");                 doDirList(rqName, f, headerOnly, os);                 sendEnd( );             }         } else if (f.canRead( )) {             // REGULAR FILE             doFile(rqName, f, headerOnly, os);         }         else {             errorResponse(404, "File not found");         }     }     void doDirList(String rqName, File dir, boolean justAHead, PrintStream os) {         os.println("HTTP/1.0 200 directory found");         os.println("Content-type: text/html");         os.println("Date: " + new Date( ).toString( ));         os.println( );         if (justAHead)             return;         os.println("<html>");         os.println("<title>Contents of directory " + rqName + "</title>");         os.println("<h1>Contents of directory " + rqName + "</h1>");         String fl[] = dir.list( );         Arrays.sort(fl);         for (int i=0; i<fl.length; i++)             os.println("<br/><a href=\"" + rqName + File.separator + fl[i] + "\">" +             "<DEFANGED_IMG align='center' border='0' src='/books/2/213/1/html/2//images/file.jpg'>" +             ' ' + fl[i] + "</a>");     }     /** Send one file, given a File object. */     void doFile(String rqName, File f, boolean headerOnly, PrintStream os) throws         IOException {         System.out.println("Loading file " + rqName);         InputStream in = new FileInputStream(f);         byte c_content[] = new byte[(int)f.length( )];         // Single large read, should be fast.         int n = in.read(c_content);         h.put(rqName, c_content);         sendFile(rqName, headerOnly, c_content, os);         in.close( );     }     /** Send one file, given the filename and contents.      * @param justHead - if true, send heading and return.      */     void sendFile(String fname, boolean justHead,         byte[] content, PrintStream os) throws IOException {         os.println("HTTP/1.0 200 Here's your file");         os.println("Content-type: " + guessMime(fname));         os.println("Content-length: " + content.length);         os.println( );         if (justHead)             return;         os.write(content);     }     /** The type for unguessable files */     final static String UNKNOWN = "unknown/unknown";          protected String guessMime(String fn) {         String lcname = fn.toLowerCase( );         int extenStartsAt = lcname.lastIndexOf('.');         if (extenStartsAt<0) {             if (fn.equalsIgnoreCase("makefile"))                 return "text/plain";             return UNKNOWN;         }         String exten = lcname.substring(extenStartsAt);         String guess = parent.getMimeType(exten, UNKNOWN);         return guess;     }     /** Sends an error response, by number, hopefully localized. */     protected void errorResponse(int errNum, String errMsg) {         // Check for localized messages         ResourceBundle messages = ResourceBundle.getBundle("errors");         String response;         try { response = messages.getString(Integer.toString(errNum)); }         catch (MissingResourceException e) { response=errMsg; }         // Generate and send the response         os.println("HTTP/1.0 " + errNum + " " + response);         os.println("Content-type: text/html");         os.println( );         os.println("<html>");         os.println("<head><title>Error " + errNum + "--" + response +             "</title></head>");         os.println("<h1>" + errNum + " " + response + "</h1>");         sendEnd( );     }     /** Send the tail end of any page we make up. */     protected void sendEnd( ) {         os.println("<HR>");         os.println("<address>Java Web Server,");         String myAddr = "http://www.darwinsys.com/freeware/";         os.println("<a href=\"" + myAddr + "\">" +             myAddr + "</a>");         os.println("</address>");         os.println("</html>");         os.println( );     } }

From a performance point of view, it may be better to precreate a pool of threads and cause each one to run the Handler when a connection comes along. This is how servlet engines drive ordinary servlets to high levels of performance; it avoids the overhead of creating a Thread object for each request. This can be done easily in JDK 1.5, using the Concurrency Utilities.



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

Similar book on Amazon

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