Either create a thread when you accept a connection, or pre-create a
pool of threads and have each wait on the accept( )
call.
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.
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).
3.138.179.100