A Simple Java Web Server
Home ] Up ] HTTP ] [ A Simple Java Web Server ] CGI ] Java Servlets ] Apache Tomcat ] Tomcat Directory Structure ] Creating a Web Application ] Web Application Directory Structure ] What's a Servlet? ] Generating Other Content ] The Servlet Life Cycle ] Servlets & Form Data ] Request Headers ] CGI Variables ] Redirection ]

 

 

To make things perhaps a little clearer, let's implement a very simple web server.

First, here's the main class, HttpServer, which opens a server socket, and responds to client connections

import java.io.IOException;
import java.net.ServerSocket;
import java.util.Date;

/**
 * Java HTTP Server
 *
 * This is a very simple HTTP server.
 *
 */
public class HttpServer
{
  public static boolean VERBOSE = false;

  /**
   * Usage
   */
  private static void usage()
  {
    //print instructions to standard out
    String instructions =
      "usage: java HttpServer [-options] [port-number]\n\n" +
      "where options include:\n" +
      "    -? -help\t print out this message\n" +
      "    -v -verbose\t turn on verbose mode\n\n" +
      "  and port-number is the port number to listen on";

    System.out.println(instructions);
    System.out.flush();
  }

  /**
   * Extract command line arguments
   */
  private static void extractArguments(String[] args)
  {
    if (args.length > 0)
    {
      int arg = 0;
      if (args[arg].equals("-?") || args[arg].equals("-help"))
      {
        usage();
        arg++;
      }

      if (args.length >= arg)
      {
        if (args[arg].equals("-v") || args[arg].equals("-verbose"))
        {
          VERBOSE = true;
          arg++;
        }
      }

      if (args.length >= arg)
      {
        try
        {
          m_port = Integer.parseInt(args[arg]);
        }
        catch (java.lang.NumberFormatException nfe)
        {
          System.err.println("Invalid port number specified: " + args[arg]);
          System.err.println("  Defaulting to: " + m_port);
        }
      }
    }
  }

  /**
   * Main method
   *
   * @param args [-options] [port-number]
   */
  public static void main(String[] args)
  {
    extractArguments(args);

    ServerSocket serverConnect = null;
    try
    {
      System.out.println("\nConnecting to port " + m_port + "...\n");
      serverConnect = new ServerSocket(m_port); //listen on port
      System.out.println("Listening for connections on port " + m_port + "...\n");
      while (true) //listen until user halts execution
      {
        RequestHandler handler = new RequestHandler(serverConnect.accept());
        if (VERBOSE)
        {
          System.out.println("Connection opened. (" + new Date() + ")");
        }
        handler.start(); // Start the request handler thread
      }
    }
    catch (IOException e)
    {
      System.err.println("Server error: " + e);
    }
  }

  ///// Private data /////

  private static int m_port = 8080; //default port value
}

In its main method, it connects to the specified port, and when it accepts a client connection, it creates a new instance of the RequestHandler class, which will receive the request, and process it.

Here's RequestHandler, which is the class that does most of the work:

import java.io.BufferedReader;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.StringTokenizer;
import java.util.Date;

/**
 * HTTP Request Handler
 *
 * This class handles a single request received by the HTTP server.
 * It currently supports only the GET method.
 *
 */
public class RequestHandler extends Thread
{
  // Constructor
  public RequestHandler(Socket connect)
  {
    m_connect = connect;
  }

  /**
   * Services each request in a separate thread
   */
  public void run()
  {
    BufferedReader in = null;
    PrintWriter    out = null;
    BufferedOutputStream dataOut = null;
    try
    {
      // Get character input stream from the client
      in = new BufferedReader(new InputStreamReader(m_connect.getInputStream()));
      // Get output stream to client
      OutputStream clientOut = m_connect.getOutputStream();
      // Get character output stream to client (for headers)
      if (HttpServer.VERBOSE)
      {
        // Insert a TeeWriter to capture the response header
        // output by the rest of the program.
        out = new PrintWriter(
                       new TeeWriter(
                             new OutputStreamWriter(clientOut),
                             new OutputStreamWriter(System.out)
                             )
                       );

      }
      else
      {
        out = new PrintWriter(m_connect.getOutputStream());
      }
      // Get binary output stream to client (for requested data)
      dataOut = new BufferedOutputStream(clientOut);

      // Get first line of request from client
      String input = in.readLine();
      // Save the header for this request
      m_requestHeader = input;
      while (true)
      {
        String line = in.readLine();
        // Read until end of stream, or blank line
        if (line == null || line.length() == 0)
          break;
        m_requestHeader += ("\n<br>" + line);
      }

      // Use StringTokenizer to parse request's first line, which is of the form:
      //    method file-requested HTTP/version-number
      // where:
      //    method is GET, HEAD, POST, etc.
      //    version-number is 1.0, 1.1, etc.
      //
      StringTokenizer parse = new StringTokenizer(input);
      String method = parse.nextToken().toUpperCase();// Parse out method
      String fileRequested = parse.nextToken().toLowerCase();// file

      if (HttpServer.VERBOSE)
      {
        System.out.println("--------Begin Request-----");
        System.out.println(m_requestHeader);
        System.out.println("--------End Request-------");
        System.out.flush();
      }

      // Dispatch, based on method type
      if (method.equals("GET"))
      {
        processGetRequest(fileRequested, in, out, dataOut);
      }
      else
      {
        // Other HTTP methods are not yet implemented in this HTTP server
        unsupportedRequest(method, in, out);
      }
    }
    catch (IOException e)
    {
      System.err.println("Server Error: " + e);
    }
    finally
    {
      // Ensure all streams are closed
      try
      {
        if (in != null)
          in.close();
        if (out != null)
          out.close();
        if (dataOut != null)
          dataOut.close();

        // Ensure the connection is closed
        m_connect.close();
        if (HttpServer.VERBOSE)
        {
          System.out.println("Connection closed.\n");
        }
      }
      catch (IOException ioe)
      {
        // Do nothing -- no recovery possible.
      }
    }
  }

  /**
   * Process a GET request
   */
  private void processGetRequest(String fileRequested,
                                 BufferedReader in,
                                 PrintWriter out,
                                 BufferedOutputStream dataOut)
      throws IOException
  {
    // Handle "special" requests
    int index = fileRequested.indexOf("special/");
    if (index != -1)
    {
      processSpecialGetRequest(fileRequested, in, out);
      return;
    }

    if (fileRequested.endsWith("/"))
    {
      //append default file name to request
      fileRequested += DEFAULT_FILE;
    }

    FileInputStream fileIn = null;
    try
    {
      File file = new File(WEB_ROOT, fileRequested);
      if (!file.exists())
      {
        throw new FileNotFoundException();
      }

      // Get the file's MIME content type
      String content = MIMETypes.getType(fileRequested);

      // Generate HTTP response header
      out.println("HTTP/1.0 200 OK");
      out.println("Server: HttpServer 1.0");
      out.println("Date: " + new Date());
      out.println("Content-type: " + content);
      out.println("Content-length: " + file.length());
      out.println(); // Blank line between headers and content
      out.flush(); // Flush character output stream buffer

      // Now the content...
      // Open input stream on the file
      fileIn = new FileInputStream(file);
      // Create a byte array to store the file contents
      byte[] fileData = new byte[(int)file.length()];
      // Read file into byte array
      fileIn.read(fileData);
       // Write file to stream
      dataOut.write(fileData, 0, (int)file.length());
      dataOut.flush(); //flush binary output stream buffer
      if (HttpServer.VERBOSE)
      {
        System.out.println("File " + fileRequested +
                           " of type " + content + " returned.");
      }
    }
    catch (IOException e)
    {
      // Tell client that file doesn't exist
      fileNotFound(fileRequested, out);
    }
    finally
    {
      // Ensure that the fileIn is closed.
      if (fileIn != null)
        fileIn.close();
    }
  }

  /**
   * Process a "special" GET request.
   * Currently, the only implementation of this is where the user
   * specifies a "special/" directory in the request.  In this case,
   * the special GET request simply returns the contents of the request
   * header in a displayable form on the browser.
   */
  private void processSpecialGetRequest(String fileRequested,
                                        BufferedReader in,
                                        PrintWriter out)
      throws IOException
  {
    String page =
        "<HTML>\n" +
        "<HEAD><TITLE>Request Header Echo</TITLE></HEAD>\n" +
        "<BODY>\n" +
        "<H2>The Header from this Request was:</H2>\n" +
        m_requestHeader + "\n" +
        "</BODY>\n" +
        "</HTML>";

    // Generate HTTP response header
    out.println("HTTP/1.0 200 OK");
    out.println("Server: HttpServer 1.0");
    out.println("Date: " + new Date());
    out.println("Content-type: " + "text/html");
    out.println("Content-length: " + page.length());
    out.println(); // Blank line between headers and content
    out.println(page);
    out.flush(); // Flush character output stream buffer
  }

  /**
   * Process unsupported requests
   */
  private void unsupportedRequest(String method, BufferedReader in, PrintWriter out)
  {
    if (HttpServer.VERBOSE)
    {
      System.out.println("501 Not Implemented: " + method + " method.");
    }

    // Send Not Implemented message to client
    out.println("HTTP/1.0 501 Not Implemented");
    out.println("Server: HttpServer 1.0");
    out.println("Date: " + new Date());
    out.println("Content-Type: text/html");
    out.println(); // Blank line between headers and content
    out.println("<HTML>");
    out.println("<HEAD><TITLE>Not Implemented</TITLE></HEAD>");
    out.println("<BODY>");
    out.println("<H2>501 Not Implemented: " + method + " method.</H2>");
    out.println("</BODY>");
    out.println("</HTML>");
    out.flush(); // Flush character output stream buffer
  }

  /**
   * Informs client that requested file does not exist.
   */
  private void fileNotFound(String fileRequested, PrintWriter out)
    throws IOException
  {
    if (HttpServer.VERBOSE)
    {
      System.out.println("404 File Not Found: " + fileRequested);
    }

    out.println("HTTP/1.0 404 File Not Found");
    out.println("Server: HttpServer 1.0");
    out.println("Date: " + new Date());
    out.println("Content-Type: text/html");
    out.println();  // Blank line between headers and content
    out.println("<HTML>");
    out.println("<HEAD><TITLE>File Not Found</TITLE></HEAD>");
    out.println("<BODY>");
    out.println("<H2>404 File Not Found: " + fileRequested + "</H2>");
    out.println("</BODY>");
    out.println("</HTML>");
    out.flush(); // Flush character output stream buffer
  }

  ///// Private data /////

  // HttpServer root is the current directory
  private static final File WEB_ROOT = new File(".");
  // Default file name for unspecified request filename
  private static final String DEFAULT_FILE = "index.html";

  private Socket m_connect;

  private String m_requestHeader;
}

RequestHandler makes use of some useful utility classes. 

First, it relies on the MIMETypes class to map a filetype to a MIME type:

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * This class provides a mapping from filetype to MIME type.
 */
public class MIMETypes
{
  /**
   *  Returns the MIME type, given a filename
   */
  public static String getType(String fileName)
  {
    String type = null;
    // Extract the filetype from the filename
    int dotIndex = fileName.lastIndexOf('.');
    if (dotIndex != -1)
    {
      String fileType = fileName.substring(dotIndex);
      // Now, look up the filetype to get the MIME type
      type = (String) m_map.get(fileType);
    }
    if (type == null)
      type = UNKNOWN_MIME_TYPE;
    return type;
  }

  /**
   *  Adds a file type/MIME type mapping.
   */
  public static void add(String fileType, String MIMEType)
  {
    m_map.put(fileType, MIMEType);
  }

  /**
   *  Removes a file type/MIME type mapping
   */
  public static void remove(String fileType)
  {
    m_map.remove(fileType);
  }

  /**
  *   Suppress the constructor, since all methods in this class are static.
  */
  private MIMETypes()
  {
  }

  /////// Private data ///////

  // The HashMap used to store the relationships between filetype and MIME type
  private static HashMap m_map = new HashMap();

  // The MIME types this web server is initially aware of
  private static final String[][] m_initialValues =
  {
    { ".class", "application/x-java-vm" },
    { ".css",   "text/css" },
    { ".doc",   "application/msword" },
    { ".gif",   "image/gif" },
    { ".htm",   "text/html" },
    { ".html",  "text/html" },
    { ".jar",   "application/x-java-archive" },
    { ".jpg",   "image/jpeg" },
    { ".jpeg",  "image/jpeg" },
    { ".mpg",   "video/mpeg" },
    { ".mpeg",  "video/mpeg" },
    { ".pdf",   "application/pdf" },
    { ".png",   "image/png" },
    { ".ppt",   "application/vnd.ms-powerpoint" },
    { ".ps",    "application/postscript" },
    { ".ser",   "application/x-java-serialized-object" },
    { ".tiff",  "image/tiff" },
    { ".txt",   "text/plain" },
    { ".xls",   "application/vnd.ms-excel" },
    { ".zip",   "application/zip" },
  };

  // Populate the HashMap on class load.
  static
  {
    for (int i = 0; i < m_initialValues.length; i++)
    {
      String key   = m_initialValues[i][0];
      String value = m_initialValues[i][1];
      m_map.put(key, value);
    }
  }

  private static final String UNKNOWN_MIME_TYPE = "application/octet-stream";

  /**
   *  Main entry point, for testing
   */
  public static void main(String[] args)
  {
    System.out.println("----BEGIN-----");
    for (Iterator iter = m_map.entrySet().iterator(); iter.hasNext(); )
    {
        Map.Entry entry = (Map.Entry) iter.next();
        String key = (String) entry.getKey();
        String value = (String) entry.getValue();
        System.out.println(key + " : " + value);
    }
    System.out.println("-----END-----");

    String type = null;
    type = getType("bar");
    System.out.println("foo type is: " + type);
    type = getType(".foo");
    System.out.println(".foo type is: " + type);
    type = getType(".css");
    System.out.println(".css type is: " + type);
    type = getType("fromage.pdf");
    System.out.println("fromage.pdf type is: " + type);
  }
}

Note that MIMETypes class has only static methods.

The second utility class is TeeWriter, which is a class for inserting a "Tee" into a stream, and "siphoning off" the output sent to the stream.  It is used to capture the response header generated by different parts of the RequestHeader class.  Here it is:

import java.io.FileWriter;
import java.io.FilterWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;

/**
 * This class provides a "Tee" so that anything written to it is sent to
 * both its Writers.  This can be quite useful for logging everything
 * written to a stream.
 */
public class TeeWriter extends FilterWriter
{
  /**
   *  Constructor
   */
  public TeeWriter(Writer out1, Writer out2)
  {
    super(out1);
    m_out = out2;
  }

  /**
   * Write a single character.
   *
   * @exception  IOException  If an I/O error occurs
   */
  public void write(int c) throws IOException
  {
    super.write(c);
    m_out.write(c);
  }

  /**
   * Write a portion of an array of characters.
   *
   * @param  cbuf  Buffer of characters to be written
   * @param  off   Offset from which to start reading characters
   * @param  len   Number of characters to be written
   *
   * @exception  IOException  If an I/O error occurs
   */
  public void write(char cbuf[], int off, int len) throws IOException
  {
      super.write(cbuf, off, len);
      m_out.write(cbuf, off, len);
  }

  /**
   * Write a portion of a string.
   *
   * @param  str  A String
   * @param  off  Offset from which to start writing characters
   * @param  len  Number of characters to write
   *
   * @exception  IOException  If an I/O error occurs
   */
  public void write(String str, int off, int len) throws IOException
  {
    super.write(str, off, len);
    m_out.write(str, off, len);
  }

  /**
   * Flush the stream.
   *
   * @exception  IOException  If an I/O error occurs
   */
  public void flush() throws IOException
  {
    super.flush();
    m_out.flush();
  }

  /**
   * Close the stream.
   *
   * @exception  IOException  If an I/O error occurs
   */
  public void close() throws IOException
  {
    super.close();
    m_out.close();
  }

  /**
   *  Main entry point, for testing.
   */
  public static void main(String[] args)
  {
    PrintWriter pw = null;
    try
    {
      FileWriter fileOut = new FileWriter("teeTest.out");
      OutputStreamWriter stdOut = new OutputStreamWriter(System.out);
      TeeWriter tee = new TeeWriter(fileOut, stdOut);
      pw = new PrintWriter(tee);
      pw.println("   Jabberwocky");
      pw.println("       by ");
      pw.println("  Lewis Carroll");
      pw.println();
      pw.println("Twas brillig, and the slithy toves\n" +
                 "Did gyre and gimble in the wabe:\n" +
                 "All mimsy were the borogoves,\n" +
                 "And the mome raths outgrabe.");
      pw.flush();
    }
    catch (IOException ioe)
    {
      ioe.printStackTrace();
    }
    finally
    {
      pw.close();
    }
  }

  ///// Private data //////

  private Writer m_out;
}
 
The page was last updated February 19, 2008