Project 1
Due: 2/22/2005
(turn in before midnight)
In this project, you will design and implement a
multi-threaded web server for static pages. At a first approximation, a web
server is a server program that implements the HTTP protocol. Your web server should
be based on a sound, scalable design.
The project consists of three stages:
Overview. Implement
a simple web server. Strictly speaking, you are only required to implement a
tiny subset of the HTTP protocol, so formally your server should not be called
an HTTP server. It should be good enough for most common browsers, though. You only need to support ³simple² HTTP
requests and responses (request: GET
<path>, followed by a carriage return and line feed; response: the
body of the requested document and connection termination afterwards).
Additionally, implement some minimal failure functionality (e.g., return a ³404
Not Found² instead of just dropping the connection). You can find the
specification of the HTTP protocol on the Web, for instance, in:
http://www.w3.org/Protocols/rfc2616/rfc2616.html
You can implement more than the required functionality (some
extra credit opportunities exist), but this is not the focus of the project. An
easy way to implement the tedious parts of HTTP is to get code from an existing
implementation. For instance, micro_httpd (http://www.acme.com/software/micro_httpd/)
implements much of HTTP in about 150 lines of C code! Your server should be runnable with a parameter determining
the port on which it listens for connections.
Architecture. There
are several good designs for high-performance UNIX web servers. The one we will follow consists of a
³boss² thread receiving requests and dispatching them to ³worker² threads. (If
you want to follow a different design, see me!) Worker threads then take over
the rest of the communication until the request is satisfied (i.e., the
requested file is sent). To avoid the overhead of creating new threads for each
incoming connection, you should implement a pool of worker threads that consume
work requests produced by the boss thread. The size of the pool (i.e., the
number of threads) should be a run-time parameter. It is a good idea to have the scheduling scope for all your
threads to be the system scope (i.e., each of your threads is mapped to a
different kernel thread so that you get independent scheduling). This is
something you can experiment with.
Start your server running, preferably on your local machine. Make it
look for files in some directory on the local disk (e.g., /var/tmp/<yourname>). (This is not too
important right now but it will be in later stages, when we will be doing
performance measurements.) For security reasons, make sure your server is
restricted in terms of what files it will return to clients!!! (See ³security², below.) Point your browser to your
server and make sure that it works. That is, if you have installed your server
on machine nonexistent.cc.gatech.edu,
port 8008, give your browser the URL http://nonexistent.cc.gatech.edu:8008/file1.html (assuming you have a file called file1.html in the directory that your server
searches).
Security: It is very
important that you ensure the security of your files while running the server.
Anyone who suspects you might have an unprotected server running can scan the
ports and use the server to get your private files! For instance, if I am
running a web server on the /var/tmp directory
of ocelot.cc.gatech.edu, port 8008,
someone could try to access http://ocelot.cc.gatech.edu:8008/../../net/hc281/fujimoto/private
and grab file private from my directory.
If you had administrator privileges, you could restrict the files your
server can reach through the chroot command or system call (see the man pages).
Unfortunately, this is not an option on the lab machines. The easiest way to
protect yourselves is to scan the filename that the web client is trying to
retrieve and make sure it is an approved one. For instance, you could make sure
that the filename retrieved has no more than one ³/² character.
Overview. Now that
the web server is running, you will need some means for evaluating its
performance as well as the performance of future enhancements to the design.
You probably donıt have access to a few hundreds of human users who can put a
load on your server. Therefore, we
will need to write a client program to simulate multiple user requests. Write
such a web client. It should be a program entirely independent from your web
server, that is, a different executable, runnable on a different machine if
needed. Of course, you may reuse code from the server, if needed. The client should be multithreaded for
performance. The number of threads it will create should be a parameter. Each
thread can access the web server a fixed number of times (e.g., 10), requesting
files uniformly at random. (In practice, uniform random accesses may not be a
good way to evaluate performance. Nevertheless, real-world access patterns are
not the focus of this project.) The set of all files the clients can access
should be a compile-time or run-time parameter. Make sure that your client
keeps track of how many bytes it retrieved from the server. This could be
either a global variable or a per-thread variable. In the end, your program
should report how many bytes it got from the server.
Warning: some
network calls that your client may use are not thread safe but
have thread safe variants. The usual gethostbyname
call is one of these. Overall, you are responsible for ensuring the
thread safety of the calls you use.
Overview. Now test
your server using your client. For the experiments, you would preferably need a
machine where you are the only active user, but this could be any old and slow
workstation in a public lab. Run both the server and the client on the same
machine. Make several copies of a file in /var/tmp/
or any other local directory. Make your client access all the files at
random. Does your server fail when overloaded? Does it just die or does it
gracefully refuse connections? Make sure the server is quite robust. Test its
limits. (Make sure you are running on files stored locally, otherwise you are
only testing the limits of the network connection. :-) Vary the number of
threads in the client and the number of threads in the server. Vary the number
of files and file sizes of the files you are retrieving by a couple of orders
of magnitude. Keep notes of your serverıs throughput in terms of MB/s retrieved
by the client. Do you see any difference for different numbers of server or
client threads? Overall, you are responsible for coming up with a reasonable
test plan. You do not have to demonstrate that many threads are better, but make
sure you experiment along a couple of axes of variation and that you give a
credible explanation for your observations. This is not a one-way process: your explanation should
influence your experimentation and vice-versa.
Experimentation Suggestions and Hints (these are not
requirements!): Most likely you will be
working on a uniprocessor, non-RAID system. The performance benefits of
threading in this environment become evident only on some workloads. Since your
server is multithreaded by nature (it has the boss thread and at least one
worker), the difference between 1 worker thread and 30 may be minimal (e.g.,
< 10%). (You are already getting a performance boost compared to the case of
a single thread that both accepts connections and services requests.) To see
significant benefits from using more threads, you need substantial I/O, but
also some CPU activity to execute while other threads are doing I/O. I/O is a little tricky: since you are
making repeated requests, your files are likely cached, so no disk I/O may
occur. If you make more files (e.g., I tried my client with 2000 copies of the index.html file from the CoC web page), you can
be sure to get a lot of disk I/O in your workload. If you are working on the
console, you will be able to tell right away whether your requests are serviced
from the disk or not, by just listening to the disk noise. Now, if you make
sure that your server threads have something to do on the CPU (e.g., you do a
floating point calculation on each byte of the retrieved file), you will notice
that your performance varies a lot depending on the number of worker threads in
the server. There are other ways
to exploit concurrency by balancing the activity of different subsystems. For instance, you can try to have your
client access a few (20-30) large files (e.g., 2MB or more). (Why does this
(not) work?) Of course, for a really dramatic increase in the observed
performance, you would need a multi-CPU machine with a RAID, but do not try your experiments on server machines that other
students are sharing!!!
You should turn in the following by email:
Good luck! Have fun!!!