CPSC 225, Spring 2010
Lab 12: Web Server


The goal of this week's lab is to write a multi-threaded web server that can make a directory full of files available to real web browsers such as Firefox and Internet Explorer. We have already discussed this project in some detail in class, so some of the instructions here will be brief.

You should start a new project named lab12 in your Eclipse workspace to use for this lab. You will want to keep a copy of this page open in a web browser so that you can copy-and-paste some of the code from this page into your program. Your program will be somewhat similar to the simple FileServer example that we looked at in class. You should make sure that you understand that example.

The lab is due next week at the beginning of the next lab.

You can work with a partner on this lab, but if you do, you will have to complete some of the "improvements" that are discussed in the last section of this web page.


Testing Your Server

When your server is running, you should be able to contact it from any web browser. You just need to enter an appropriate URL in the location box of the browser. For the URL, you need to know the host where the server is running and the port number where it is listening. When you are running the browser on the same computer as the server, you can use localhost as the host name, so you get a URL something like this:

      localhost:4080/index.html

This is a request for the file named index.html on the top level of the server's directory. It assumes that the port number is 4080.

There is another way to test your server. You can contact it directly from the command line, using the telnet program. Telnet is a simple program that is often used for testing text-based network programs. For example, to connect to the web server that is runnign on port 80 on math.hws.edu, you can use this command in a terminal window:

      telnet math.hws.edu 80

Once connected, you can type in a request by hand to be sent to the server. Try this:

      GET /index.html HTTP/1.1
      Host: math.hws.edu
      Connection: close

with an extra blank line at the the end. You can see the exact text that the server sends in response. You can try this with your own server as well, using a command such as

     telnet localhost 4080

In that case, you don't have to be quite so careful about what you type, except for the first line. This is a good way to tell exactly how your server is responding to the request.


Server Setup and Connection Handling

Create a new class for your server. Add constants to represent the PORT on which the server will listen and the DIRECTORY that contains the files that are made available on the server. For the DIRECTORY, you can use "/classes/s10/cs225/javanotes" is you want; that directory is copy of the textbook for this course. There are also some faculty web sites that you could use, such as "/home/scottyorr/www" or "/home/mcorliss/www". You could also use your own www directory, if you have a web site

Your program needs a main() routine and a method for handling a connection. The main routine for a server is pretty standard, and you can use this code:

    public static void main(String[] args) {
        try {
            ServerSocket server = new ServerSocket(PORT);
            System.out.println("LISTENING ON PORT NUMBER " + PORT);
            while (true) {
                Socket socket = server.accept();
                handleConnection(socket);
            }
        }
        catch (IOException e) {
            System.out.println("Some Error Occurred.  Shutting down server.");
            System.out.println("Error: " + e);
        }
    }

For the handleConnection method, you can follow the outline that we went over in class. You need to get the input and output streams from the socket. You need to read the first three tokens from the input stream, using a Scanner. Once you know that you have a legal request, you can take the fileName from the request and use it to look for the file in the DIRECTORY. To get the full name of the file that you want, do

       fileName = DIRECTORY + fileName;

Check that (1) the file exists, (2) you can read the file, and (3) the file is not a directory. (Directories can't just be sent like normal files. You should consider a request for a directory to be an error of type 403.) Once you know that you have a good file you can send out the response on the output stream.

The response, assuming there was no error, must consist of a set of headers, followed by a blank line, followed by the contents of the file. The format for the headers should be:

         HTTP/1.1 200 OK
         Connection: close
         Content-Type:  (INSERT VALUE)
         Content-Length  (INSERT VALUE)

The Content-Length can be determined using the method file.length() from the File class. For the Content-Type, you can use the following method to determine the content type based on the extension in the file name:

    private static String getMimeTypeFromFileExtension(String fileName) {
         int pos = fileName.lastIndexOf('.');
         if (pos < 0)  // no file extension in name
             return "x-application/x-unknown";
         String ext = fileName.substring(pos+1).toLowerCase();
         if (ext.equals("txt")) return "text/plain";
         else if (ext.equals("html")) return "text/html";
         else if (ext.equals("htm")) return "text/html";
         else if (ext.equals("css")) return "text/css";
         else if (ext.equals("js")) return "text/javascript";
         else if (ext.equals("java")) return "text/x-java";
         else if (ext.equals("jpeg")) return "image/jpeg";
         else if (ext.equals("jpg")) return "image/jpeg";
         else if (ext.equals("png")) return "image/png";
         else if (ext.equals("gif")) return "image/gif"; 
         else if (ext.equals("ico")) return "image/x-icon";
         else if (ext.equals("class")) return "application/java-vm";
         else if (ext.equals("jar")) return "application/java-archive";
         else if (ext.equals("zip")) return "application/zip";
         else if (ext.equals("xml")) return "application/xml";
         else if (ext.equals("xhtml")) return"application/xhtml+xml";
         else return "x-application/x-unknown";
            // Note:  x-application/x-unknown  is something made up;
            // it will probably make the browser offer to save the file.
    }

Headers should be sent in a very exact format, consisting of ASCII characters only, followed by a carriage return ('\r') and line feed ('\n'). To make things easier for you, here is a method that will send one line:

    /**
     * Sends one line of text to an OutputStream, in proper format for HTTP.
     * A carriage return and line feed are added to serve as end-of-line.
     * @param out  The stream where the text will be written.
     * @param text  The text that will be written, which should consist
     *    of ASCII characters only.  If text is null, no characters are
     *    transmitted, but the end-of-line is still sent.
     * @throws IOException
     */
    private static void sendLineOfAsciiText(OutputStream out, String text) 
                                                           throws IOException {
        if (text != null) {
            for (int i = 0; i < text.length(); i++)
                out.write(text.charAt(i));
        }
        out.write('\r');
        out.write('\n');
    }

Note that you can use this method to send a blank line. For sending the contents of the file itself, you need to create a FileInputStream for reading from the file. You can then copy the data from that stream into the socket's output stream by calling the copy method that we looked at in class:

    /**
     * Copies bytes from an input stream to an output stream until end-of-stream is detected.
     * @throws IOException if an IOExcption occurs during copying
     */
    private static void copy(InputStream in, OutputStream out) throws IOException {
        byte[] buffer = new byte[4096];
        while (true) {
            int count = in.read(buffer);
            if (count < 0)
                break;
            out.write(buffer,0,count);
        }
        out.flush();
    }

Error Reporting

Your server has to check for possible errors along the way. If it finds an error, it should send an error response back to the browser. The error response consists of some header lines, a blank line, and then an error message that will be displayed to the user. The user of the browser sees only the error message, not the headers. To keep things easy, we can keep send the error message as plain text. The first line of the headers has information for the browser about what error occurred. As we discussed in class, possible first lines include:

       HTTP/1.1 404 Not Found
       HTTP/1.1 403 Forbidden
       HTTP/1.1 400 Bad Request
       HTTP/1.1 501 Not Implemented

The headers should also tell the browser what type of data is being sent in the error message. In our case, that is text/plain. So, for example, a complete 404 error response could look like:

       HTTP/1.1 404 Not Found
       Content-Type: text/plain
     
       Sorry, but the file that you requested
       does not exist on this server.

You should make sure that your server can handle the four type of errors listed above (404, 403, 400, 501). It might be a good idea to write a method for sending error messages.


Adding Threading

Threads

The server that you have written is single-threaded. It can only handle one request at a time. If a second request comes in while you are working on another request, the second request will have to wait until you are finished with the first request, even if that takes a long time because you are sending a large file over a slow network. This is not acceptable for a real server. A real server should be multi-threaded, with several threads to handle connections.

An easy way to write a multi-threaded server is to start a new thread to handle each connection request. New requests can be handled as they arrive, even if previous requests are still being handled by other threads. (Note that this solution is still not acceptable for real servers, because starting a new thread is a relatively time-consuming thing, and because you don't want to have the possibility of having too many threads running at the same time.)

To make your server into a multi-threaded server, you will need a subclass of Thread. The class needs a run() method to specify the task that the thread will perform. In this case, it should handle one connection request. We can pass the socket for that connection to the constuctor of the class. Here's the class:

      private static class ConnectionThread extends Thread {
         Socket connection;
         ConnectionThread(Socket connection) {
            this.connection = connection;
         }
         public void run() {
            handleConnection(connection);
         }
      }

Now, in the main routine, instead of calling handleConnection directly, you will create and start a thread of type ConnectionThread. That's all there is to it! With this change, you should have a minimal but functional multi-threaded web server.


Improvements

There are a lot of improvements that could be made to the server, ranging from fairly simple to very complex. You would probably need some hints or help from me to do any of them. These improvements are not a required part of the lab -- except that if you are working with a partner you are required to do at least a couple of them -- but you might want to add some improvements for extra credit. Here are a few ideas:


David Eck, for CPSC 225