10.2 Some Useful Servers

     

This section shows several servers you can build with server sockets. It starts with a server you can use to test client responses and requests , much as you use Telnet to test server behavior. Then three different HTTP servers are presented, each with a different special purpose and each slightly more complex than the previous one.

10.2.1 Client Tester

In the previous chapter, you learned how to use Telnet to experiment with servers. There's no equivalent program to test clients , so let's create one. Example 10-5 is a program called ClientTester that runs on a port specified on the command line, shows all data sent by the client, and allows you to send a response to the client by typing it on the command line. For example, you can use this program to see the commands that Internet Explorer sends to a server.

Clients are rarely as forgiving about unexpected server responses as servers are about unexpected client responses. If at all possible, try to run the clients that connect to this program on a Unix system or some other platform that is moderately crash-proof. Don't run them on Mac OS 9 or Windows ME, which are less stable.


This program uses two threads: one to handle input from the client and the other to send output from the server. Using two threads allows the program to handle input and output simultaneously : it can send a response to the client while receiving a requestor , more to the point, it can send data to the client while waiting for the client to respond. This is convenient because different clients and servers talk in unpredictable ways. With some protocols, the server talks first; with others, the client talks first. Sometimes the server sends a one-line response; often, the response is much larger. Sometimes the client and the server talk at each other simultaneously. Other times, one side of the connection waits for the other to finish before it responds. The program must be flexible enough to handle all these cases. Example 10-5 shows the code.

Example 10-5. A client tester
 import java.net.*; import java.io.*; import com.macfaq.io.SafeBufferedReader; // from Chapter 4 public class ClientTester {   public static void main(String[] args) {     int port;          try {       port = Integer.parseInt(args[0]);     }       catch (Exception ex) {       port = 0;     }          try {       ServerSocket server = new ServerSocket(port, 1);       System.out.println("Listening for connections on port "         + server.getLocalPort( ));       while (true) {         Socket connection = server.accept( );         try {           System.out.println("Connection established with "             + connection);           Thread input = new InputThread(connection.getInputStream( ));           input.start( );           Thread output             = new OutputThread(connection.getOutputStream( ));           output.start( );           // wait for output and input to finish            try {             input.join( );             output.join( );           }           catch (InterruptedException ex) {           }         }         catch (IOException ex) {           System.err.println(ex);          }         finally {           try {             if (connection != null) connection.close( );           }           catch (IOException ex) {}         }       }     }     catch (IOException ex) {       e.printStackTrace( );     }      } } class InputThread extends Thread {      InputStream in;       public InputThread(InputStream in) {      this.in = in;    }    public void run( )  {          try {             while (true) {          int i = in.read( );          if (i == -1) break;          System.out.write(i);        }      }      catch (SocketException ex) {        // output thread closed the socket      }      catch (IOException ex) {        System.err.println(ex);      }      try {        in.close( );      }      catch (IOException ex) {       }    } } class OutputThread extends Thread {      private Writer out;        public OutputThread(OutputStream out) {     this.out = new OutputStreamWriter(out);   }   public void run( ) {     String line;     BufferedReader in       = new SafeBufferedReader(new InputStreamReader(System.in));     try {       while (true) {         line = in.readLine( );         if (line.equals(".")) break;         out.write(line +"\r\n");         out.flush( );       }        }     catch (IOException ex) {      }      try {       out.close( );     }     catch (IOException ex) {      }          } } 

The client tester application is split into three classes: ClientTester , InputThread , and OutputThread . The ClientTester class reads the port from the command line, opens a ServerSocket on that port, and listens for incoming connections. Only one connection is allowed at a time, because this program is designed for experimentation, and a slow human being has to provide all responses. Consequently, it sets an unusually short queue length of 1. Further connections will be refused until the first one has been closed.

An infinite while loop waits for connections with the accept( ) method. When a connection is detected , its InputStream is used to construct a new InputThread and its OutputStream is used to construct a new OutputThread . After starting these threads, the program waits for them to finish by calling their join() methods .

The InputThread is contained almost entirely in the run( ) method. It has a single field, in , which is the InputStream from which data will be read. Data is read from in one byte at a time. Each byte read is written on System.out . The run( ) method ends when the end of stream is encountered or an IOException is thrown. The most likely exception here is a SocketException thrown because the corresponding OutputThread closed the connection.

The OutputThread reads input from the local user sitting at the terminal and sends that data to the client. Its constructor has a single argument, an output stream for sending data to the client. OutputThread reads input from the user on System.in , which is chained to an instance of the SafeBufferedReader class developed in Chapter 4. The OutputStream that was passed to the constructor is chained to an OutputStreamWriter for convenience. The run( ) method for OutputThread reads lines from the SafeBufferedReader and copies them onto the OutputStreamWriter , which sends them to the client. A period typed on a line by itself signals the end of user input. When this occurs, run( ) exits the loop and out is closed. This has the effect of also closing the socket so that a SocketException is thrown in the input thread, which also exits.

For example, here's the output when Netscape Communicator 4.6 for Windows connected to this server:

 D:\JAVA\JNP3\examples>  java ClientTester 80  Listening for connections on port 80 Connection established with  Socket[addr=localhost/127.0.0.1,port=1033,localport=80] GET / HTTP/1.0 Connection: Keep-Alive User-Agent: Mozilla/4.6 [en] (WinNT; I) Host: localhost Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */* Accept-Encoding: gzip Accept-Language: en Accept-Charset: iso-8859-1,*,utf-8 <html><body><h1>Hello Client!</h1></body></html> . 

Even minimal exploration of clients can reveal some surprising things. For instance, I didn't know until I wrote this example that Netscape Navigator 4.6 can read .gz files just as easily as it can read HTML files. That might be useful for serving large text files full of redundant data.

10.2.2 HTTP Servers

HTTP is a large protocol. As you saw in Chapter 3, a full-featured HTTP server must respond to requests for files, convert URLs into filenames on the local system, respond to POST and GET requests, handle requests for files that don't exist, interpret MIME types, and much, much more. However, many HTTP servers don't need all of these features. For example, many sites simply display an "under construction" message. Clearly, Apache is overkill for a site like this. Such a site is a candidate for a custom server that does only one thing. Java's network class library makes writing simple servers like this almost trivial.

Custom servers aren't useful only for small sites. High-traffic sites like Yahoo! are also candidates for custom servers because a server that does only one thing can often be much faster than a general purpose server such as Apache or Microsoft IIS. It is easy to optimize a special purpose server for a particular task; the result is often much more efficient than a general purpose server that needs to respond to many different kinds of requests. For instance, icons and images that are used repeatedly across many pages or on high-traffic pages might be better handled by a server that read all the image files into memory on startup and then served them straight out of RAM, rather than having to read them off disk for each request. Furthermore, this server could avoid wasting time on logging if you didn't want to track the image requests separately from the requests for the pages they were included in.

Finally, Java isn't a bad language for full-featured web servers meant to compete with the likes of Apache or IIS. Even if you believe CPU-intensive Java programs are slower than CPU-intensive C and C++ programs (something I very much doubt is true in modern VMs), most HTTP servers are limited by bandwidth, not by CPU speed. Consequently, Java's other advantages, such as its half-compiled/half-interpreted nature, dynamic class loading, garbage collection, and memory protection really get a chance to shine . In particular, sites that make heavy use of dynamic content through servlets, PHP pages, or other mechanisms can often run much faster when reimplemented on top of a pure or mostly pure Java web server. Indeed, there are several production web servers written in Java, such as the W3C's testbed server Jigsaw (http://www.w3.org/Jigsaw/). Many other web servers written in C now include substantial Java components to support the Java Servlet API and Java Server Pages. On many sites, these are replacing the traditional CGIs, ASPs, and server-side includes, mostly because the Java equivalents are faster and less resource- intensive . I'm not going to explore these technologies here since they easily deserve a book of their own. I refer interested readers to Jason Hunter's Java Servlet Programming (O'Reilly). However, it is important to note that servers in general and web servers in particular are one area where Java really is competitive with C.

10.2.2.1 A single-file server

Our investigation of HTTP servers begins with a server that always sends out the same file, no matter what the request. It's called SingleFileHTTPServer and is shown in Example 10-6. The filename, local port, and content encoding are read from the command line. If the port is omitted, port 80 is assumed. If the encoding is omitted, ASCII is assumed.

Example 10-6. An HTTP server that chunks out the same file
 import java.net.*; import java.io.*; import java.util.*; public class SingleFileHTTPServer extends Thread {   private byte[] content;   private byte[] header;   private int port = 80;   public SingleFileHTTPServer(String data, String encoding,     String MIMEType, int port) throws UnsupportedEncodingException {         this(data.getBytes(encoding), encoding, MIMEType, port);   }   public SingleFileHTTPServer(byte[] data, String encoding,     String MIMEType, int port) throws UnsupportedEncodingException {          this.content = data;     this.port = port;     String header = "HTTP/1.0 200 OK\r\n"      + "Server: OneFile 1.0\r\n"      + "Content-length: " + this.content.length + "\r\n"      + "Content-type: " + MIMEType + "\r\n\r\n";     this.header = header.getBytes("ASCII");   }      public void run( ) {        try {       ServerSocket server = new ServerSocket(this.port);        System.out.println("Accepting connections on port "          + server.getLocalPort( ));       System.out.println("Data to be sent:");       System.out.write(this.content);       while (true) {                  Socket connection = null;         try {           connection = server.accept( );           OutputStream out = new BufferedOutputStream(                                   connection.getOutputStream( )                                  );           InputStream in   = new BufferedInputStream(                                   connection.getInputStream( )                                  );           // read the first line only; that's all we need           StringBuffer request = new StringBuffer(80);           while (true) {             int c = in.read( );             if (c == '\r'  c == '\n'  c == -1) break;             request.append((char) c);           }           // If this is HTTP/1.0 or later send a MIME header           if (request.toString( ).indexOf("HTTP/") != -1) {             out.write(this.header);           }                    out.write(this.content);           out.flush( );         }  // end try         catch (IOException ex) {            }         finally {           if (connection != null) connection.close( );          }                } // end while     } // end try     catch (IOException ex) {       System.err.println("Could not start server. Port Occupied");     }   } // end run   public static void main(String[] args) {     try {                String contentType = "text/plain";       if (args[0].endsWith(".html")  args[0].endsWith(".htm")) {         contentType = "text/html";       }              InputStream in = new FileInputStream(args[0]);       ByteArrayOutputStream out = new ByteArrayOutputStream( );       int b;       while ((b = in.read( )) != -1) out.write(b);       byte[] data = out.toByteArray( );                // set the port to listen on       int port;       try {         port = Integer.parseInt(args[1]);         if (port < 1  port > 65535) port = 80;       }         catch (Exception ex) {         port = 80;       }                String encoding = "ASCII";       if (args.length >= 2) encoding = args[2];                Thread t = new SingleFileHTTPServer(data, encoding,        contentType, port);       t.start( );              }     catch (ArrayIndexOutOfBoundsException ex) {       System.out.println(        "Usage: java SingleFileHTTPServer filename port encoding");     }     catch (Exception ex) {       System.err.println(ex);     }      } } 

The constructors set up the data to be sent along with an HTTP header that includes information about content length and content encoding. The header and the body of the response are stored in byte arrays in the desired encoding so that they can be blasted to clients very quickly.

The SingleFileHTTPServer class itself is a subclass of Thread . Its run( ) method processes incoming connections. Chances are this server will serve only small files and will support only low-volume web sites. Since all the server needs to do for each connection is check whether the client supports HTTP/1.0 and spew one or two relatively small byte arrays over the connection, chances are this will be sufficient. On the other hand, if you find clients are getting refused, you could use multiple threads instead. A lot depends on the size of the file served, the peak number of connections expected per minute, and the thread model of Java on the host machine. Using multiple threads would be a clear win for a server that was even slightly more sophisticated than this one.

The run( ) method creates a ServerSocket on the specified port. Then it enters an infinite loop that continually accepts connections and processes them. When a socket is accepted, an InputStream reads the request from the client. It looks at the first line to see whether it contains the string HTTP . If it sees this string, the server assumes that the client understands HTTP/1.0 or later and therefore sends a MIME header for the file; then it sends the data. If the client request doesn't contain the string HTTP , the server omits the header, sending the data by itself. Finally, the server closes the connection and tries to accept the next connection.

The main( ) method just reads parameters from the command line. The name of the file to be served is read from the first command-line argument. If no file is specified or the file cannot be opened, an error message is printed and the program exits. Assuming the file can be read, its contents are read into the byte array data . A reasonable guess is made about the content type of the file, and that guess is stored in the contentType variable. Next, the port number is read from the second command-line argument. If no port is specified or if the second argument is not an integer from 0 to 65,535, port 80 is used. The encoding is read from the third command-line argument, if present. Otherwise , ASCII is assumed. (Surprisingly, some VMs don't support ASCII, so you might want to pick 8859-1 instead.) Then these values are used to construct a SingleFileHTTPServer object and start it running. This is only one possible interface. You could easily use this class as part of some other program. If you added a setter method to change the content, you could easily use it to provide simple status information about a running server or system. However, that would raise some additional issues of thread safety that Example 10-6 doesn't have to address because it's immutable.

Here's what you see when you connect to this server via Telnet; the specifics depend on the exact server and file:

 %  telnet macfaq.dialup.cloud9.net 80  Trying 168.100.203.234... Connected to macfaq.dialup.cloud9.net. Escape character is '^]'. GET / HTTP/1.0 HTTP/1.0 200 OK Server: OneFile 1.0 Content-length: 959 Content-type: text/html <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN"> <HTML> <HEAD> <TITLE>Under Construction</TITLE> </HEAD> <BODY> ... 

10.2.2.2 A redirector

Redirection is another simple but useful application for a special-purpose HTTP server. In this section, we develop a server that redirects users from one web site to anotherfor example, from cnet.com to www.cnet.com . Example 10-7 reads a URL and a port number from the command line, opens a server socket on the port, and redirects all requests that it receives to the site indicated by the new URL using a 302 FOUND code. Chances are this server is fast enough not to require multiple threads. Nonetheless, threads might be mildly advantageous, especially for a high volume site on a slow network connection. But really for purposes of example more than anything, I've made the server multithreaded. In this example, I chose to use a new thread rather than a thread pool for each connection. This is perhaps a little simpler to code and understand but somewhat less efficient. In Example 10-8, we'll look at an HTTP server that uses a thread pool.

Example 10-7. An HTTP redirector
 import java.net.*; import java.io.*; import java.util.*; public class Redirector implements Runnable {   private int port;   private String newSite;      public Redirector(String site, int port) {     this.port = port;     this.newSite = site;   }   public void run( ) {          try {              ServerSocket server = new ServerSocket(this.port);        System.out.println("Redirecting connections on port "          + server.getLocalPort( ) + " to " + newSite);                while (true) {                  try {           Socket s = server.accept( );           Thread t = new RedirectThread(s);           t.start( );         }  // end try         catch (IOException ex) {            }                } // end while            } // end try     catch (BindException ex) {       System.err.println("Could not start server. Port Occupied");     }              catch (IOException ex) {       System.err.println(ex);     }            }  // end run   class RedirectThread extends Thread {              private Socket connection;              RedirectThread(Socket s) {       this.connection = s;         }              public void run( ) {              try {                  Writer out = new BufferedWriter(                       new OutputStreamWriter(                        connection.getOutputStream( ), "ASCII"                       )                      );         Reader in = new InputStreamReader(                      new BufferedInputStream(                        connection.getInputStream( )                      )                     );                              // read the first line only; that's all we need         StringBuffer request = new StringBuffer(80);         while (true) {           int c = in.read( );           if (c == '\r'  c == '\n'  c == -1) break;           request.append((char) c);         }         // If this is HTTP/1.0 or later send a MIME header         String get = request.toString( );         int firstSpace = get.indexOf(' ');         int secondSpace = get.indexOf(' ', firstSpace+1);         String theFile = get.substring(firstSpace+1, secondSpace);         if (get.indexOf("HTTP") != -1) {           out.write("HTTP/1.0 302 FOUND\r\n");           Date now = new Date( );           out.write("Date: " + now + "\r\n");           out.write("Server: Redirector 1.0\r\n");           out.write("Location: " + newSite + theFile + "\r\n");                   out.write("Content-type: text/html\r\n\r\n");                            out.flush( );                         }         // Not all browsers support redirection so we need to          // produce HTML that says where the document has moved to.         out.write("<HTML><HEAD><TITLE>Document moved</TITLE></HEAD>\r\n");         out.write("<BODY><H1>Document moved</H1>\r\n");         out.write("The document " + theFile            + " has moved to\r\n<A HREF=\"" + newSite + theFile + "\">"           + newSite  + theFile           + "</A>.\r\n Please update your bookmarks<P>");         out.write("</BODY></HTML>\r\n");         out.flush( );       } // end try       catch (IOException ex) {       }       finally {         try {           if (connection != null) connection.close( );         }         catch (IOException ex) {}         }             }  // end run        }   public static void main(String[] args) {     int thePort;     String theSite;          try {       theSite = args[0];       // trim trailing slash       if (theSite.endsWith("/")) {         theSite = theSite.substring(0, theSite.length( )-1);       }     }     catch (Exception ex) {       System.out.println(        "Usage: java Redirector http://www.newsite.com/ port");       return;     }          try {       thePort = Integer.parseInt(args[1]);     }       catch (Exception ex) {       thePort = 80;     }              Thread t = new Thread(new Redirector(theSite, thePort));     t.start( );      }  // end main } 

In order to start the redirector on port 80 and redirect incoming requests to http://www. ibiblio .org/xml/, type:

 D:\JAVA\JNP3\examples>  java Redirector http://www.ibiblio.org/xml/  Redirecting connections on port 80 to http://www.ibiblio.org/xml/ 

If you connect to this server via Telnet, this is what you'll see:

 %  telnet macfaq.dialup.cloud9.net 80  Trying 168.100.203.234... Connected to macfaq.dialup.cloud9.net. Escape character is '^]'. GET / HTTP/1.0 HTTP/1.0 302 FOUND Date: Wed Sep 08 11:59:42 PDT 1999 Server: Redirector 1.0 Location: http://www.ibiblio.org/xml/ Content-type: text/html <HTML><HEAD><TITLE>Document moved</TITLE></HEAD> <BODY><H1>Document moved</H1> The document / has moved to <A HREF="http://www.ibiblio.org/xml/">http://www.ibiblio.org/xml/</A>.  Please update your bookmarks<P></BODY></HTML> Connection closed by foreign host. 

If, however, you connect with a reasonably modern web browser, you should be sent to http://www.ibiblio.org/xml/ with only a slight delay. You should never see the HTML added after the response code; this is only provided to support older browsers that don't do redirection automatically.

The main( ) method provides a very simple interface that reads the URL of the new site to redirect connections to and the local port to listen on. It uses this information to construct a Redirector object. Then it uses the resulting Runnable object ( Redirector implements Runnable ) to spawn a new thread and start it. If the port is not specified, Redirector listens on port 80. If the site is omitted, Redirector prints an error message and exits.

The run() method of Redirector binds the server socket to the port, prints a brief status message, and then enters an infinite loop in which it listens for connections. Every time a connection is accepted, the resulting Socket object is used to construct a RedirectThread . This RedirectThread is then started. All further interaction with the client takes place in this new thread. The run( ) method of Redirector then simply waits for the next incoming connection.

The run( ) method of RedirectThread does most of the work. It begins by chaining a Writer to the Socket 's output stream and a Reader to the Socket 's input stream. Both input and output are buffered. Then the run( ) method reads the first line the client sends. Although the client will probably send a whole MIME header, we can ignore that. The first line contains all the information we need. The line looks something like this:

 GET /directory/filename.html HTTP/1.0 

It is possible that the first word will be POST or PUT instead or that there will be no HTTP version. The second "word" is the file the client wants to retrieve. This must begin with a slash (/). Browsers are responsible for converting relative URLs to absolute URLs that begin with a slash; the server does not do this. The third word is the version of the HTTP protocol the browser understands. Possible values are nothing at all (pre-HTTP/1.0 browsers), HTTP/1.0, or HTTP/1.1.

To handle a request like this, Redirector ignores the first word. The second word is attached to the URL of the target server (stored in the field newSite ) to give a full redirected URL. The third word is used to determine whether to send a MIME header; MIME headers are not used for old browsers that do not understand HTTP/1.0. If there is a version, a MIME header is sent; otherwise, it is omitted.

Sending the data is almost trivial. The Writer out is used. Since all the data we send is pure ASCII, the exact encoding isn't too important. The only trick here is that the end-of-line character for HTTP requests is \r\n-- a carriage return followed by a linefeed .

The next lines each send one line of text to the client. The first line printed is:

 HTTP/1.0 302 FOUND 

This is an HTTP/1.0 response code that tells the client to expect to be redirected. The second line is a Date : header that gives the current time at the server. This line is optional. The third line is the name and version of the server; this line is also optional but is used by spiders that try to keep statistics about the most popular web servers. (It would be very surprising to ever see Redirector break into single digits in lists of the most popular servers.) The next line is the Location : header, which is required for this server. It tells the client where it is being redirected to. Last is the standard Content-type : header. We send the content type text/html to indicate that the client should expect to see HTML. Finally, a blank line is sent to signify the end of the header data.

Everything after this will be HTML, which is processed by the browser and displayed to the user. The next several lines print a message for browsers that do not support redirection, so those users can manually jump to the new site. That message looks like:

 <HTML><HEAD><TITLE>Document moved</TITLE></HEAD> <BODY><H1>Document moved</H1> The document / has moved to <A HREF="http://www.ibiblio.org/xml/">http://www.ibiblio.org/xml/</A>.  Please update your bookmarks<P></BODY></HTML> 

Finally, the connection is closed and the thread dies.

10.2.2.3 A full-fledged HTTP server

Enough special-purpose HTTP servers. This next section develops a full-blown HTTP server, called JHTTP , that can serve an entire document tree, including images, applets, HTML files, text files, and more. It will be very similar to the SingleFileHTTPServer , except that it pays attention to the GET requests. This server is still fairly lightweight; after looking at the code, we'll discuss other features we might want to add.

Since this server may have to read and serve large files from the filesystem over potentially slow network connections, we'll change its approach. Rather than processing each request as it arrives in the main thread of execution, we'll place incoming connections in a pool. Separate instances of a RequestProcessor class will remove the connections from the pool and process them. Example 10-8 shows the main JHTTP class. As in the previous two examples, the main( ) method of JHTTP handles initialization, but other programs can use this class to run basic web servers.

Example 10-8. The JHTTP web server
 import java.net.*; import java.io.*; import java.util.*; public class JHTTP extends Thread {   private File documentRootDirectory;   private String indexFileName = "index.html";   private ServerSocket server;   private int numThreads = 50;        public JHTTP(File documentRootDirectory, int port,     String indexFileName) throws IOException {          if (!documentRootDirectory.isDirectory( )) {       throw new IOException(documentRootDirectory         + " does not exist as a directory");      }     this.documentRootDirectory = documentRootDirectory;     this.indexFileName = indexFileName;     this.server = new ServerSocket(port);   }   public JHTTP(File documentRootDirectory, int port)     throws IOException {     this(documentRootDirectory, port, "index.html");   }   public JHTTP(File documentRootDirectory) throws IOException {     this(documentRootDirectory, 80, "index.html");   }   public void run( ) {        for (int i = 0; i < numThreads; i++) {       Thread t = new Thread(        new RequestProcessor(documentRootDirectory, indexFileName));       t.start( );        }     System.out.println("Accepting connections on port "       + server.getLocalPort( ));     System.out.println("Document Root: " + documentRootDirectory);     while (true) {       try {         Socket request = server.accept( );         RequestProcessor.processRequest(request);       }       catch (IOException ex) {        }        }        }      public static void main(String[] args) {     // get the Document root     File docroot;     try {       docroot = new File(args[0]);     }     catch (ArrayIndexOutOfBoundsException ex) {       System.out.println("Usage: java JHTTP docroot port indexfile");       return;     }          // set the port to listen on     int port;     try {       port = Integer.parseInt(args[1]);       if (port < 0  port > 65535) port = 80;     }       catch (Exception ex) {       port = 80;     }            try {                   JHTTP webserver = new JHTTP(docroot, port);       webserver.start( );     }     catch (IOException ex) {       System.out.println("Server could not start because of an "         + e.getClass( ));       System.out.println(e);     }      } } 

The main( ) method of the JHTTP class sets the document root directory from args[0] . The port is read from args[1] or 80 is used for a default. Then a new JHTTP thread is constructed and started. The JHTTP thread spawns 50 RequestProcessor threads to handle requests, each of which retrieves incoming connection requests from the RequestProcessor pool as they become available. The JHTTP thread repeatedly accepts incoming connections and puts them in the RequestProcessor pool.

Each connection is handled by the run( ) method of the RequestProcessor class shown in Example 10-9. This method waits until it can get a Socket out of the pool. Once it does that, it gets input and output streams from the socket and chains them to a reader and a writer. The reader reads the first line of the client request to determine the version of HTTP that the client supportswe want to send a MIME header only if this is HTTP/1.0 or laterand the requested file. Assuming the method is GET , the file that is requested is converted to a filename on the local filesystem. If the file requested is a directory (i.e., its name ends with a slash), we add the name of an index file. We use the canonical path to make sure that the requested file doesn't come from outside the document root directory. Otherwise, a sneaky client could walk all over the local filesystem by including . . in URLs to walk up the directory hierarchy. This is all we'll need from the client, although a more advanced web server, especially one that logged hits, would read the rest of the MIME header the client sends.

Next, the requested file is opened and its contents are read into a byte array. If the HTTP version is 1.0 or later, we write the appropriate MIME headers on the output stream. To figure out the content type, we call the guessContentTypeFromName() method to map file extensions such as .html onto MIME types such as text/html. The byte array containing the file's contents is written onto the output stream and the connection is closed. Exceptions may be thrown at various places if, for example, the file cannot be found or opened. If an exception occurs, we send an appropriate HTTP error message to the client instead of the file's contents.

Example 10-9. The thread pool that handles HTTP requests
 import java.net.*; import java.io.*; import java.util.*;      public class RequestProcessor implements Runnable {        private static List pool = new LinkedList( );   private File documentRootDirectory;   private String indexFileName = "index.html";      public RequestProcessor(File documentRootDirectory,     String indexFileName) {              if (documentRootDirectory.isFile( )) {       throw new IllegalArgumentException(        "documentRootDirectory must be a directory, not a file");        }     this.documentRootDirectory = documentRootDirectory;     try {       this.documentRootDirectory         = documentRootDirectory.getCanonicalFile( );     }     catch (IOException ex) {     }     if (indexFileName != null) this.indexFileName = indexFileName;   }      public static void processRequest(Socket request) {          synchronized (pool) {       pool.add(pool.size( ), request);       pool.notifyAll( );     }   }        public void run( ) {              // for security checks     String root = documentRootDirectory.getPath( );            while (true) {              Socket connection;       synchronized (pool) {                  while (pool.isEmpty( )) {           try {             pool.wait( );           }           catch (InterruptedException ex) {           }         }         connection = (Socket) pool.remove(0);        }       try {                     String filename;         String contentType;            OutputStream raw = new BufferedOutputStream(                             connection.getOutputStream( )                            );                  Writer out = new OutputStreamWriter(raw);         Reader in = new InputStreamReader(                      new BufferedInputStream(                       connection.getInputStream( )                      ),"ASCII"                     );         StringBuffer requestLine = new StringBuffer( );         int c;         while (true) {           c = in.read( );           if (c == '\r'  c == '\n') break;           requestLine.append((char) c);         }                  String get = requestLine.toString( );                  // log the request          System.out.println(get);                  StringTokenizer st = new StringTokenizer(get);         String method = st.nextToken( );         String version = "";         if (method.equals("GET")) {           filename = st.nextToken( );           if (filename.endsWith("/")) filename += indexFileName;           contentType = guessContentTypeFromName(filename);           if (st.hasMoreTokens( )) {             version = st.nextToken( );           }           File theFile = new File(documentRootDirectory,             filename.substring(1,filename.length( )));           if (theFile.canRead( )                // Don't let clients outside the document root            && theFile.getCanonicalPath( ).startsWith(root)) {             DataInputStream fis = new DataInputStream(                                    new BufferedInputStream(                                     new FileInputStream(theFile)                                    )                                   );             byte[] theData = new byte[(int) theFile.length( )];             fis.readFully(theData);             fis.close( );             if (version.startsWith("HTTP ")) {  // send a MIME header               out.write("HTTP/1.0 200 OK\r\n");               Date now = new Date( );               out.write("Date: " + now + "\r\n");               out.write("Server: JHTTP/1.0\r\n");               out.write("Content-length: " + theData.length + "\r\n");               out.write("Content-type: " + contentType + "\r\n\r\n");               out.flush( );             }  // end if                      // send the file; it may be an image or other binary data              // so use the underlying output stream              // instead of the writer             raw.write(theData);             raw.flush( );           }  // end if           else {  // can't find the file             if (version.startsWith("HTTP ")) {  // send a MIME header               out.write("HTTP/1.0 404 File Not Found\r\n");               Date now = new Date( );               out.write("Date: " + now + "\r\n");               out.write("Server: JHTTP/1.0\r\n");               out.write("Content-type: text/html\r\n\r\n");             }              out.write("<HTML>\r\n");             out.write("<HEAD><TITLE>File Not Found</TITLE>\r\n");             out.write("</HEAD>\r\n");             out.write("<BODY>");             out.write("<H1>HTTP Error 404: File Not Found</H1>\r\n");             out.write("</BODY></HTML>\r\n");             out.flush( );           }         }         else {  // method does not equal "GET"           if (version.startsWith("HTTP ")) {  // send a MIME header             out.write("HTTP/1.0 501 Not Implemented\r\n");             Date now = new Date( );             out.write("Date: " + now + "\r\n");             out.write("Server: JHTTP 1.0\r\n");             out.write("Content-type: text/html\r\n\r\n");            }                  out.write("<HTML>\r\n");           out.write("<HEAD><TITLE>Not Implemented</TITLE>\r\n");           out.write("</HEAD>\r\n");           out.write("<BODY>");           out.write("<H1>HTTP Error 501: Not Implemented</H1>\r\n");           out.write("</BODY></HTML>\r\n");           out.flush( );         }       }       catch (IOException ex) {       }       finally {         try {           connection.close( );                 }         catch (IOException ex) {}        }            } // end while   } // end run   public static String guessContentTypeFromName(String name) {     if (name.endsWith(".html")  name.endsWith(".htm")) {       return "text/html";     }     else if (name.endsWith(".txt")  name.endsWith(".java")) {       return "text/plain";     }     else if (name.endsWith(".gif")) {       return "image/gif";     }     else if (name.endsWith(".class")) {       return "application/octet-stream";     }     else if (name.endsWith(".jpg")  name.endsWith(".jpeg")) {       return "image/jpeg";     }     else return "text/plain";   }   } // end RequestProcessor 

This server is functional but still rather austere. Here are a few features that could be added:

  • A server administration interface

  • Support for CGI programs and/or the Java Servlet API

  • Support for other request methods, such as POST, HEAD, and PUT

  • A log file in the common web log file format

  • Server-side includes and/or Java Server Pages

  • Support for multiple document roots so individual users can have their own sites

Finally, spend a little time thinking about ways to optimize this server. If you really want to use JHTTP to run a high-traffic site, there are a couple of things that can speed this server up. The first and most important is to use a Just-in-Time (JIT) compiler such as HotSpot. JITs can improve program performance by an order of magnitude or more. The second thing to do is implement smart caching. Keep track of the requests you've received and store the data from the most frequently requested files in a Hashtable so that they're kept in memory. Use a low-priority thread to update this cache. Another option for developers using Java 1.4 or later is to use non-blocking I/O and channels instead of threads and streams. We'll explore this possibility in Chapter 12.



Java Network Programming
Java Network Programming, Third Edition
ISBN: 0596007213
EAN: 2147483647
Year: 2003
Pages: 164

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