Threaded Network Server

Problem

You want a network server to be multithreaded.

Solution

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

Discussion

Networking (see Chapter 15 through Chapter 17) 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 Section 16.5; 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, or the World Wide Web Consortium’s jigsaw server. 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-11.

Example 24-11. Httpd.java

/**
 * A very very simple Web server.
 *
 * NO SECURITY. ALMOST NO CONFIGURATION. NO CGI. NO SERVLETS.
 *
 * 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 */
    protected Properties wsp;
    /** A Properties, for loading mime types into */
    protected Properties mimeTypes;
    /** The root directory */
    protected String rootDir;

    public static void main(String argv[]) {
        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(  ) {
        while (true) {
            try {
                Socket clntSock = sock.accept(  );
                new Handler(this, clntSock).start(  );
            } catch(IOException e) {
                System.err.println(e);
            }
        }
    }

    /** Construct a server object for a given port number */
    Httpd(  ) {
        super(  );
        // A ResourceBundle can't load from the same basename as your class,
        // but a simple Properties can.
        wsp=loadProps("httpd.properties");
        rootDir = wsp.getProperty("rootDir", ".");
        mimeTypes = loadProps(wsp.getProperty("mimeProperties", "mime.properties"));
    }

    public void startServer(int portNum) {
        String portNumString = null;
        if (portNum == HTTP) {
            portNumString = wsp.getProperty("portNum");
            if (portNumString != null) {
                portNum = Integer.parseInt(portNumString);
            }
        }
        try {
            sock = new ServerSocket(portNum);
        } catch(NumberFormatException e) {
            System.err.println("Httpd: "" + portNumString +
                "" not a valid number, unable to start server");
            System.exit(1);
        } catch(IOException e) {
            System.err.println("Network error " + e);
            System.err.println("Unable to start server");
            System.exit(1);
        }
    }

    /** Load the Properties. */
    protected Properties loadProps(String fname) {
        Properties sp = new Properties(  );

        try {
            // Create input file to load from.
            FileInputStream ifile = new FileInputStream(fname);

            sp.load(ifile);
        } catch (FileNotFoundException notFound) {
            System.err.println(notFound);
            System.exit(1);
        } catch (IOException badLoad) {
            System.err.println(badLoad);
            System.exit(1);
        }
        return sp;
    }

}

The Handler class -- shown in its current form in Example 24-12 -- 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:

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 (described in Section 18.2’s discussion of servlets), 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; once a file has been read from disk, the program will not reread it, to save disk I/O overhead. This means you have to restart the server if you change files; comparing the timestamps (see Section 10.2) and reloading files if they have changed is left as an exercise for the reader.

Example 24-12. Handler.java

import java.io.*;
import java.net.*;
import java.text.*;
import java.util.*;

/** Called from Httpd 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.
 */
public class Handler extends Thread {
    /** The Socket that we read from and write to. */
    Socket clntSock;
    /** inputStream, from Viewer */
    BufferedReader is;
    /** outputStream, to Viewer */
    PrintStream os;
    /** Main program */
    Httpd parent;
    /** The default filename in a directory. */
    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 per request).
     */
    static Hashtable h = new Hashtable(  );

    /** Construct a Handler */
    Handler(Httpd prnt, Socket sock) {
        super("client thread");
        parent = prnt;
        clntSock = sock;
        // First time, put in null handler.
        if (h.size(  ) == 0) {
            h.put("", "<HTML><BODY><B>Unknown server error");
        }
    }

    protected static final int RQ_INVALID = 0, RQ_GET = 1, RQ_HEAD = 2,
        RQ_POST = 3; 

    public void run(  ) {
        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(  );
                        System.out.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;
            }
                
            // Someday: new MyRequest(clntSock, rqName, methodType);
            // new MyResponse(clntSock, os);

            // 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.rootDir + 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="" + fl[i] + "">" +
            "<IMG ALIGN=absbottom BORDER=0 SRC="internal-gopher-unknown">" +
            ' ' + 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.
     * boolean 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";
    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.mimeTypes.getProperty(exten, UNKNOWN);

        // System.out.println("guessMime: input " + fn + 
        //     ", extention " + exten + ", result " + guess);

        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 pre-create 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 may never replace the Apache web server, but there it is: small, simple, and threaded.

See Also

There are several issues I have not discussed. Scheduling of threads is not necessarily preemptive; it may be cooperative. This means that, on some platforms, the threads mechanism does not interrupt the running thread periodically to give other threads a “fair” chance to get CPU time. Therefore, in order to be portable to all Java platforms, your code must use yield( ) or wait( ) periodically (or some other method that causes the thread to be suspended, such as reading or writing). I also didn’t get into priorities. The priority model is more limited than some other thread models, such as POSIX threads.

All in all, it’s important to understand that threaded classes require careful design. For this reason, you should refer to a good book on threaded Java before unleashing anything threaded upon the world. Recommendations include Concurrent Programming in Java by Doug Lea (Addison Wesley), Multithreaded Programming with Java Technology by Lewis et al (Prentice Hall), and Java Threads by Scott Oaks and Henry Wong (O’Reilly).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.179.100