CPSC 225, Spring 2016
Lab 11: Web Server

In this week's lab, you will write a simple web server program. This means working with the socket API for networking, as well as working with files and streams. A web server should be multi-threaded, but the one you write for this lab is not. It would not be difficult to add threads, once we learn about them.

You should create a new Lab11 project. You can start with a copy of the sample program NotAWebServer.java, which we looked at in class. You can find it in /classes/cs225/first-socket-examples. This web page contains some additional code that you can use.

This lab is due next week, as usual, by 9:00 AM on Saturday morning, November 19. This is an individual assignment.

About the HTTP Protocol

Web browsers and web servers communicate using the HTTP protocol. This is a "request/response" protocol: The browser sends a request to the server and the server sends back a response. Both the request and the response start with a "header," which consists of one or more lines of text followed by a blank line. The blank line is essential since it marks the end of the header. Lines should be terminated with a CRLF (carriage-return / line-feed, or "\r\n" in Java). The blank line consists of just a CRLF. (At least, that's the standard; most browsers will also accept a plain "\n" to mark an end-of-line. Still, it's better to follow the standard.)

After the blank line that ends the headers, a response can contain data, which can be of any type (text, picture, music, etc.). The data that is sent in the response is what appears in the web browser's window; the headers are not displayed to the user of the browser.

Request and response headers can get complicated, but your server will only have to deal with a few possibilities. For the request, you only need to read the first line, which should contain three tokens and should be of the form:

              GET <path-to-file> HTTP/1.1

In fact, you really only need the first two tokens from this line. The first token is called the method. HTTP supports several different. methods, but you will only implement GET, and you should send back an error message if the method is anything besides GET.

The second token in the request tells you which file you should send back to the browser. This is the essential piece of data that you need, in order to decide what to transmit to the client.

(The third token in the request must be "HTTP/1.1" or, in some very old browsers, "HTTP/1.0". A real web browser should check that this token is correct, but for our purposes, it can be ignored. There will be more lines of data in the request, but again, you can ignore them.)

A web server has a directory that contains the files that it can send to clients. The <path-to-file> tells how to find the requested file, starting in that directory. For example, if the directory is /classes/cs225/graphicsbook and if <path-to-file> is /index.html, then the actual file is /classes/cs225/graphicsbook/index.html. You obtain the actual file path by adding the <path-to-file> onto the server directory.

(A technicality that you can probably ignore: If a file name contains special characters such as spaces, they will be encoded in the HTTP request. The get the real file name from the <path-to-file>, you should really call URLDecoder.decode(<path-to-file>,"UTF-8") to convert the encoded path from the request into the path that you need to find the file.) Another note for Windows users: File paths on the web always use the forward slash "/" as a separator. Windows uses a backslash "\". If you try to run the web server on a windows computer, you need to globally replace "/" with "\" in the <path-to-file> for the server to work. There is a replaceAll method in the String class that you can use for this.)


Once the web server knows what file is being requested, it has to send a response. Assuming that there has been no error and that you are in fact sending a file, the response should look like this, where <mime-type> is the mime type that describes the type of data in the file, and <file-size> is the number of bytes in the file:

                 HTTP/1.1 200 OK
                 Connection: close
                 Content-Type:  <mime-type>
                 Content-Length:  <file-size>

These lines must be followed by a blank line and then by the contents of the file.

If any error occurs, then instead of just failing or sending nonsense, the server should send a response that describes the error. For example, if the requested file does not exist, you might send:

                 HTTP/1.1 404 Not Found
                 Connection: close
                 Content-Type: text/plain
                 
                 Sorry, the file that you requested
                 could not be found.

In this case, the first three lines are the response headers, followed by a blank line as usual. The last two lines in the response are the content of the response, which will be displayed in the web browser window to the user. Remember that the browser only displays the part that comes after the blank line. (A real server would use Content-Type text/html and send HTML-formatted text as the response, but it will be easier for you to send plain text.)

You don't have to implement all possible error responses, but here are some possible errors that you will want to take into account:

Testing A Web Server

A web server program receives "requests" over the network for "URLs" that are available on the server computer. It sends back a "response." The response can be an error message or the file or other data that the requested URL refers to.

The web server does not care that the request comes from a web browser program. Telnet is a simple program that allows you to communicate with any server that works with plain text data. You can use telnet to contact a web server and send it a request. For example, enter

telnet www.hws.edu 80

on the command line to connect to the web server running on host www.hws.edu, on port number 80. Once you are connected, enter:

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

followed by an extra blank line. The server should respond by sending you the main page of the web site.

When you have written your own web server, you can use telnet to test it. This is a good way to see exactly what response your server will send. Let's say that your server listens on port 8080. Run the server and use a command such as

telnet localhost 8080

on the command line. Then type a request such as

GET /index.html HTTP/1.1

The server should send back a correct header followed by a blank line and an error message or the content of the requested file. (Your server won't need the extra lines that are needed when you are sending a request to www.hws.edu.)

I will certainly use telnet to test your program, especially for error handling, and you will likely find it useful to contact your server with telnet during debugging.


Your server should also work with a web browser such as Firefox, Chrome, Safari, Edge, or Internet Explorer. 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, and you would enter a URL something like this:

                http://localhost:8080/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 8080.

The server can also be contacted from a browser running on another computer. In that case, the URL can use the IP address of the computer where the server is running. For example:

                http://172.21.7.101:8080/index.html

You should make sure to test that both files and error messages sent by your server appear correctly in a web browser window.

Writing your Server

Start with a copy of NotAWebServer.java. The main() routine in that program won't have to be changed (except maybe for the port number). You will have to remove some code from handleConenction(), and add a lot more, including a lot of error detection and handling.

The handleConnection method must read the request from the socket's input stream, then write a response to the socket's output stream. As mentioned above, you really only need to read the first two tokens from the input stream. The output stream is a more difficult matter because some of the data that you have to send can be binary data (for example, if the response is an image file). This means that you shouldn't use a PrintWriter. Instead, you should use the output stream directly. To do that, you should copy the following method into your code, and use it for each line of text that you want to send:

/**
 * 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 if an error occurs while transmitting the data
 */
private static void sendAscii(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');
}

For copying the contents of a file to the output stream, you can use the following method, which is similar to the binary copying program that we covered in class. It simply copies all the bytes from an input stream to an output stream. In this case, the input stream should be one that reads from the file, and the output stream is the one from the socket.

/**
 * 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[8192];
    int count = in.read(buffer);
    while (count != -1) {
        out.write(buffer,0,count);
        count = in.read(buffer);
    }
    out.flush();
}

Your interaction with the client must follow the HTTP protocol. Read the first two tokens from the request. If the first token is not GET, send an error. If the first token is GET, you have to decide which file to send. You do this by adding the second token onto the name of the server directory. You can use "/classes/cs225/graphicsbook" as the name of the server directory, if you want; that directory holds a copy of my online computer graphics textbook.

Once you have the full file path, you can create a File object from it. You have to check: first, that the file exists; second, that the file is readable; and third, that the file is not a directory. If any of these fail, you should send an error response.

After you've gotten past the tests that the file exists, is readable, and is not a directory, you can send a response that includes the contents of the file, with the response code "200 OK". You will need to know the length of the file for the response header, which you can also get from the File object. And you will need a FileInputStream to get the contents of the file.

You will also need to know the "mime type" of the file for the response header. The mime type can be determined from the file extension, the last part of the file name. You can use the following method, which will cover almost all of the files that a server is likely to encounter:

private static String getMimeTypeFromFileName(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 if (ext.equals("pdf")) return "application/pdf";
     else if (ext.equals("c")) return "text/x-csrc";
     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.
}

Possible Enhancements

There are many possible enhancements. One of them is to make the server multi-threaded. We might do that in a future lab as an easy exercise. Here are some more ideas that would make the web server a more realistic server. Someone who is considering an enhanced web server as their final project would want to do some or all of these. A lot of them will require more information and help to implement.