CPSC 441, Fall 2018
Lab 3: Writing a Web Server

The assignment for this lab is to write a basic web server. Although you will implement only a part of the HTTP protocol, your server should be capable of serving pages correctly to a web browser such as Firefox and Chrome, and those pages should be displayed correctly in the browser. You should start by writing a single-threaded web server. When that is working you can add multi-treading.

For this lab, you are encouraged but not required to work with a partner. (Working with a partner will require some extra features in the server.) If you work with a partner, make sure that both names are listed in a comment in the Java file that contains the main() routine. Only one person should turn in the work.

The program will be due in two weeks, and you can continue to work on it in next week's lab. However, there will also be a few new small tasks to do next week. When tuning in the program, your work should be placed in a folder named lab3 in your homework folder. I will print out all .java files in the lab3 folder, or in subfolders, except for NetUtil.java and TextIO.java, so please don't include unnecessary files.

Requirements

A basic web server, which is sufficient for a one-person project, must do the following. It must allow a directory for its files to be specified on the command line (as in done in FileSrv.java). It must be able to respond to GET requests for files, either by sending the file in the response or by sending an appropriate error message. It should respond to unimplemented request types (POST, HEAD, etc.) with a "501 Not Implemented" error. If a GET request is for a directory, and if that directory contains a file named index.html, then you should return that file; if no such file exists, a basic web server can simply return a "403 Forbidden" error. You can ignore any request headers, and you can always use the "Connection: close" response header and close the connection immediately after sending the response. When sending a file in the response, you must include "Content-Type:" and "Content-Length:" headers. The server should log (that is, output) some information about connections to standard output. The final web server must use a thread pool to handle connections (but you should probably start with a version that does not use threads). You can find more information and more details below.

There are many ways that the server could be improved, of course. Here are some possibilities. If you are working with a partner, you should do at least one or two of these. If you are working alone, you could still implement some of them for extra credit. Many of the improvements will require reading the request headers. Some will most like require some discussion with me. You might also have other ideas, which you can discuss with me.

FileSrv

I handed out the sample programs FileSrv.java and FileClient.java in class, along with the file NetUtil.java that both programs use. All of these classes are in the package netsrv, and you can find copies in the folder /classes/cs441/netsrv. There is also a .jar file containing all three classes, /classes/cs441/FileTransfer.jar.

You will want to use NetUtil.java as part of your web server. You can use FileSrv.java as a model for the web server. FileSrv reads a simple command, either LIST or GET <filename>, from the client and sends back a response. You might want to try running the file server and connecting to it with telnet. To run the server, you can use this command in a Terminal:

java -cp /classes/cs441/FileTransfer.jar netsrv.FileSrv

The directory that contains the files for the server will be the directory that you are in when you give this command. (Make sure that the current directory contains some non-directory files.) Then, in another window, use the following command to connect to the server:

telnet localhost 33987

You won't see any response because the server waits for a command before doing anything. Type the command LIST and press return. You will see a list of available files. The server closes the connection after sending the list. Try again with a command of the form GET <filename> to retrieve a file from the server, where <filename> is one of the file names that were listed. The contents of the file will simply be displayed in the Terminal window. You might also try giving some bad commands to see the error messages that you get.

Getting Started

Start your program with the basic outline for a network server, similar to the structure of FileSrv.java or DateServer.java from Section 11.4.4 of Javanotes. You can choose any port number. 8000 and 8080 are common choices for web browsers that can't run on port 80.

The program needs a main routine to create the server socket and listen for connections. Write a separate "handleConnection" method to do the communication with a connected client. Make sure that any exceptions that occur during the communication are caught so that they do not crash the server. Log some information about each connection, including the address of the client. Both of the sample programs already do logging.

As soon as you get the basic outline done, you might want to test it by telnetting to your server, even if all it does is immediately close the connection. You can try again at various points in the development to test features as you implement them. Don't forget to test how your server deals with errors in the request, since I will certainly do that!

When you have the usual basic outline, you are ready to start programming HTTP. You should use some of the methods from the NetUtil class to help with the communication. You can find a copy in /classes/cs441/netsrv.

Don't just write everything in one long handleConnection method! You should try to design a well-written program in which responsibilities are divided up appropriately among several methods and possibly even additional classes.

Response Codes and Mime Types

When you send a response to a client, the first line of the response includes the string "HTTP/1.1" followed by a status code and a status message. For example, if you are returning a file that was requested by the client, the first line will be "HTTP/1.1 200 OK".

You should also be prepared to send back some error messages. You are not required to implement a lot of different status codes, but you will need these:

An error response should still contain some data. The content of the message is shown to the user in the browser, even for an error response. The content can be a plain text message describing the error in a human-readable ways. (You could also send an HTML response if you know some HTML.)

You can just close the connection after sending a response. To be nice, when you do that, you should include a "Connection: close" header. You should always have a Content-Type header, and it is nice to have a Content-Length header. That is, the response headers take the form

Connection: close
Content-Type:  <mime-type>
Content-Length:  <file-size>

You can omit the Content-Length when you send an error response. The mime-type for an error response will be text/plain (or text/html). For a file, you can determine the mime-type from the file name extension (converted to lower case). This list of mime types should be sufficient for our purposes:

    EXTENSIONS        MIME TYPE
   --------------    ------------
     .html             text/html
     .txt              text/plain
     .css              text/css
     .js               text/javascript
     .java             text/x-java
     .c                text/x-csrc
     .jpg  .jpeg       image/jpeg
     .png              image/png
     .gif              image/gif
     (anything else)   x-application/x-unknown

The last mime type, x-application/x-unknown, is made up. It should make the browser prompt the user to save the file. If you are interested, you can find a long list of mime types in the file /etc/mime.types.

Reading the Request

To keep things simple, you only really need to look at the first line that a client sends to a server. For a basic server, you will not need to read and parse all the headers in the request, and you can use "Connection: close" in the response so that you can simply close the connection after sending the response. Recall that the first line of a legal HTTP/1.1 request has the form

<method>  <filepath>  HTTP/1.1

The method can be GET, POST, HEAD, and some other things. You are only required to implement the GET method. You can ignore the rest of the headers and any data in the request. Nevertheless, you should send a legal response in all cases, even when the command is not GET and even when the first line does not have the correct form. If the line has the correct form but the first word is not GET, then you can send a 501 error response. If the line is not of the correct form, you can send a 400 error response.

The <filepath> in the request will be a path to a file, or possibly a directory. The protocol allows the path to be followed by a "?" character and some additional characters. For this lab, you can strip them off and discard them (or just ignore the possibility). If the <filepath> string contains special characters, they will be URL encoded, so you should decode the string before using it. (There is a URL decoder method in class NetUtil.)

Finding and Sending the File

A web server is designed to serve files from a certain directory. The directory that is used is part of the web server configuration. Your program should accept the directory name as a command-line argument, just like NetSrv.java. You can use /classes/cs441/graphicsbook as a default directory if you like.

The <target> path from the request starts in the server directory. That is, the target path has to be appended to the directory to get the path to the actual file. You might check out FileSrv.java to see how I handle it there, using the File class.

Once you have a File object representing the location of the file, you can use the File API to check whether the file exists and whether it can be read, and send an appropriate error response if the file can't be sent. If the requested file is a directory, you should look for a file named index.html in the directory; if that file exists, you should send it to the client. That is, if the request is a request for a directory and if the directory contains a file named index.html, then send index.html as the response. If no index.html file exists in the directory, a basic web server can simply send a 403 Forbidden response.

Finally, when you are ready to send a file, you need to create a legal response, with status 200 OK. Remember to include the Content-Type and Content-Length headers. You can use the File API to get the file size.

Adding the Thread Pool

The program DateServerWithThreadPool.java from Section 12.4.4 in javanotes is an example of using a thread pool in a network server. Thread pools themselves are discussed in Section 12.3.2. Another example can be found in the solution to Exercise 12.4.

You will need a subclass of Thread to represent the threads in the thread pool, and you will need an ArrayBlockingQueue to carry connected sockets from the main routine to the worker threads. The run method in the thread class should run in an infinite loop in which it takes a Socket from the queue and handles the connection.

The main() routine needs to create the ArrayBlockingQueue that will be used to pipe connections to the thread pool. Then, main() needs to create the threads that make up the thread pool and start them running. This must be done before any connections can be accepted by the server. As the main() routine accepts connections, it simply puts the connected sockets into the queue. Ordinarily, this will take essentially no time.

That's really all there is to it. The ArrayBlockingQueue is designed to take care of all of the "synchronization" that is needed when several threads all use a shared resource (the queue in this case), and the put() and take() methods in the ArrayBlockingQueue take care of any blocking that is needed when a thread wants to enqueue or dequeue an item.

There is still, however, one synchronization issue: The "log" to which the server writes information about connections is a shared resource, since all the threads write their messages to the same destination. Messages from different threads will be mixed together in the output. To keep all the log messages for a given connection in one place, it is advisable to make a string that contains all the log messages for that connection, and write that string with one call to System.out.println() after handling the connection. (I think that System.out.println() is synchronized to prevent data from two calls to System.out.println() to be intermingled, but I am not completely sure of that. To be safe, you could write the log message in a synchronized method.)