To continue to my HTTP protocol posts, here is a sample program which acts as a web server. Very basic program to print the headers when client sends a request and responding the bytes if the requested resource exists in the current working directory. Click here to learn more about HTTP.

SimpleWebServer.java

import java.io.*;
import java.net.*;
import java.util.*;

/**
* An example of a very simple, multi-threaded HTTP server.
* and also as comments in the source code.
* @author Santhosh Reddy Mandadi
* @since 17-June-2009
* @version 1.0
*/
class SimpleWebServer implements HttpConstants {

/* static class data/methods */

/* print to stdout */
protected static void p(String s) {
System.out.println(s);
}

/* print to the log file */
protected static void log(String s) {
synchronized (log) {
log.println(s);
log.flush();
}
}

static PrintStream log = null;
/* our server's configuration information is stored
* in these properties
*/
protected static Properties props = new Properties();

/* Where worker threads stand idle */
static Vector threads = new Vector();

/* the web server's virtual root */
static File root;

/* timeout on client connections */
static int timeout = 0;

/* max # worker threads */
static int workers = 5;


/* load www-server.properties from java.home */
static void loadProps() throws IOException {
File f = new File
(System.getProperty("java.home")+File.separator+
"lib"+File.separator+"www-server.properties");
if (f.exists()) {
InputStream is =new BufferedInputStream(new
FileInputStream(f));
props.load(is);
is.close();
String r = props.getProperty("root");
if (r != null) {
root = new File(r);
if (!root.exists()) {
throw new Error(root + " doesn't exist as server root");
}
}
r = props.getProperty("timeout");
if (r != null) {
timeout = Integer.parseInt(r);
}
r = props.getProperty("workers");
if (r != null) {
workers = Integer.parseInt(r);
}
r = props.getProperty("log");
if (r != null) {
p("opening log file: " + r);
log = new PrintStream(new BufferedOutputStream(
new FileOutputStream(r)));
}
}

/* if no properties were specified, choose defaults */
if (root == null) {
root = new File(System.getProperty("user.dir"));
}
if (timeout <= 1000) {
timeout = 5000;
}
if (workers < 25) {
workers = 5;
}
if (log == null) {
p("logging to stdout");
log = System.out;
}
}

static void printProps() {
p("root="+root);
p("timeout="+timeout);
p("workers="+workers);
}

public static void main(String[] a) throws Exception {
int port = 9090;
if (a.length > 0) {
port = Integer.parseInt(a[0]);
}
loadProps();
printProps();
/* start worker threads */
for (int i = 0; i < workers; ++i) {
Worker w = new Worker();
(new Thread(w, "worker #"+i)).start();
threads.addElement(w);
}

ServerSocket ss = new ServerSocket(port);
while (true) {

Socket s = ss.accept();

Worker w = null;
synchronized (threads) {
if (threads.isEmpty()) {
Worker ws = new Worker();
ws.setSocket(s);
(new Thread(ws, "additional worker")).start();
} else {
w = (Worker) threads.elementAt(0);
threads.removeElementAt(0);
w.setSocket(s);
}
}
}
}
}

class Worker extends SimpleWebServer implements HttpConstants, Runnable {
final static int BUF_SIZE = 2048;

static final byte[] EOL = {(byte)'\r', (byte)'\n' };

/* buffer to use for requests */
byte[] buf;
/* Socket to client we're handling */
private Socket s;

Worker() {
buf = new byte[BUF_SIZE];
s = null;
}

synchronized void setSocket(Socket s) {
this.s = s;
notify();
}

public synchronized void run() {
while(true) {
if (s == null) {
/* nothing to do */
try {
wait();
} catch (InterruptedException e) {
/* should not happen */
continue;
}
}
try {
handleClient();
} catch (Exception e) {
e.printStackTrace();
}
/* go back in wait queue if there's fewer
* than numHandler connections.
*/
s = null;
Vector pool = SimpleWebServer.threads;
synchronized (pool) {
if (pool.size() >= SimpleWebServer.workers) {
/* too many threads, exit this one */
return;
} else {
pool.addElement(this);
}
}
}
}

void handleClient() throws IOException {
InputStream is = new BufferedInputStream(s.getInputStream());
PrintStream ps = new PrintStream(s.getOutputStream());
/* we will only block in read for this many milliseconds
* before we fail with java.io.InterruptedIOException,
* at which point we will abandon the connection.
*/
// s.setSoTimeout(SimpleWebServer.timeout);
s.setTcpNoDelay(true);
/* zero out the buffer from last time */
for (int i = 0; i < BUF_SIZE; i++) {
buf[i] = 0;
}
try {
/* We only support HTTP GET/HEAD, and don't
* support any fancy HTTP options,
* so we're only interested really in
* the first line.
*/
int nread = 0, r = 0;

outerloop:
while (nread < BUF_SIZE) {
r = is.read(buf, nread, BUF_SIZE - nread);
if (r == -1) {
/* EOF */
return;
}
int i = nread;
nread += r;
for (; i < nread; i++) {
if (buf[i] == (byte)'\n' || buf[i] == (byte)'\r') {
/* read one line */
break outerloop;
}
}
}
String h = new String(buf,0,nread);
log("..."+h);
/* are we doing a GET or just a HEAD */
boolean doingGet;
/* beginning of file name */
int index;
if (buf[0] == (byte)'G' &&
buf[1] == (byte)'E' &&
buf[2] == (byte)'T' &&
buf[3] == (byte)' ') {
doingGet = true;
index = 4;
} else if (buf[0] == (byte)'H' &&
buf[1] == (byte)'E' &&
buf[2] == (byte)'A' &&
buf[3] == (byte)'D' &&
buf[4] == (byte)' ') {
doingGet = false;
index = 5;
} else {
log("unsupported method");
//let us try to display everything here
// byte tmp[] = new byte[9000];
// int t = is.read(tmp, 0,800);
// String rem = new String (tmp,0,t);
// log(rem);
/* we don't support this method */
ps.print("HTTP/1.0 " + HTTP_BAD_METHOD +
" unsupported method type: ");
ps.write(buf, 0, 5);
ps.write(EOL);
ps.flush();
s.close();
return;
}

int i = 0;
/* find the file name, from:
* GET /foo/bar.html HTTP/1.0
* extract "/foo/bar.html"
*/
for (i = index; i < nread; i++) {
if (buf[i] == (byte)' ') {
break;
}
}
String fname = (new String(buf, 0, index,
i-index)).replace('/', File.separatorChar);
if (fname.startsWith(File.separator)) {
fname = fname.substring(1);
}
File targ = new File(SimpleWebServer.root, fname);
if (targ.isDirectory()) {
File ind = new File(targ, "index.html");
if (ind.exists()) {
targ = ind;
}
}
boolean OK = printHeaders(targ, ps);
if (doingGet) {
if (OK) {
sendFile(targ, ps);
} else {
send404(targ, ps);
}
}
} finally {
s.close();
}
}

boolean printHeaders(File targ, PrintStream ps) throws IOException {
boolean ret = false;
int rCode = 0;
if (!targ.exists()) {
rCode = HTTP_NOT_FOUND;
ps.print("HTTP/1.0 " + HTTP_NOT_FOUND + " not found");
ps.write(EOL);
ret = false;
} else {
rCode = HTTP_OK;
ps.print("HTTP/1.0 " + HTTP_OK+" OK");
ps.write(EOL);
ret = true;
}
log("From " +s.getInetAddress().getHostAddress()+": GET " +
targ.getAbsolutePath()+"-->"+rCode);
ps.print("Server: Simple java");
ps.write(EOL);
ps.print("Date: " + (new Date()));
ps.write(EOL);
if (ret) {
if (!targ.isDirectory()) {
ps.print("Content-length: "+targ.length());
ps.write(EOL);
ps.print("Last Modified: " + (new
Date(targ.lastModified())));
ps.write(EOL);
String name = targ.getName();
int ind = name.lastIndexOf('.');
String ct = null;
if (ind > 0) {
ct = (String) map.get(name.substring(ind));
}
if (ct == null) {
ct = "unknown/unknown";
}
ps.print("Content-type: " + ct);
ps.write(EOL);
} else {
ps.print("Content-type: text/html");
ps.write(EOL);
}
}
return ret;
}

void send404(File targ, PrintStream ps) throws IOException {
ps.write(EOL);
ps.write(EOL);
ps.println("Not Found\n\n"+
"The requested resource was not found.\n");
}

void sendFile(File targ, PrintStream ps) throws IOException {
InputStream is = null;
ps.write(EOL);
if (targ.isDirectory()) {
listDirectory(targ, ps);
return;
} else {
is = new FileInputStream(targ.getAbsolutePath());
}

try {
int n;
while ((n = is.read(buf)) > 0) {
ps.write(buf, 0, n);
}
} finally {
is.close();
}
}

/* mapping of file extensions to content-types */
static java.util.Hashtable map = new java.util.Hashtable();

static {
fillMap();
}
static void setSuffix(String k, String v) {
map.put(k, v);
}

static void fillMap() {
setSuffix("", "content/unknown");
setSuffix(".uu", "application/octet-stream");
setSuffix(".exe", "application/octet-stream");
setSuffix(".ps", "application/postscript");
setSuffix(".zip", "application/zip");
setSuffix(".sh", "application/x-shar");
setSuffix(".tar", "application/x-tar");
setSuffix(".snd", "audio/basic");
setSuffix(".au", "audio/basic");
setSuffix(".wav", "audio/x-wav");
setSuffix(".gif", "image/gif");
setSuffix(".jpg", "image/jpeg");
setSuffix(".jpeg", "image/jpeg");
setSuffix(".htm", "text/html");
setSuffix(".html", "text/html");
setSuffix(".text", "text/plain");
setSuffix(".c", "text/plain");
setSuffix(".cc", "text/plain");
setSuffix(".c++", "text/plain");
setSuffix(".h", "text/plain");
setSuffix(".pl", "text/plain");
setSuffix(".txt", "text/plain");
setSuffix(".java", "text/plain");
}

void listDirectory(File dir, PrintStream ps) throws IOException {
ps.println("<TITLE>Directory listing</TITLE><P>\n");
ps.println("<A HREF=\"..\">Parent Directory</A><BR>\n");
String[] list = dir.list();
for (int i = 0; list != null && i < list.length; i++) {
File f = new File(dir, list[i]);
if (f.isDirectory()) {
ps.println("<A HREF=\""+list[i]+"/\">"+list[i]+"/</A><BR>");
} else {
ps.println("<A HREF=\""+list[i]+"\">"+list[i]+"</A><BR");
}
}
ps.println("<P><HR><BR><I>" + (new Date()) + "</I>");
}

}

interface HttpConstants {
/** 2XX: generally "OK" */
public static final int HTTP_OK = 200;
public static final int HTTP_CREATED = 201;
public static final int HTTP_ACCEPTED = 202;
public static final int HTTP_NOT_AUTHORITATIVE = 203;
public static final int HTTP_NO_CONTENT = 204;
public static final int HTTP_RESET = 205;
public static final int HTTP_PARTIAL = 206;

/** 3XX: relocation/redirect */
public static final int HTTP_MULT_CHOICE = 300;
public static final int HTTP_MOVED_PERM = 301;
public static final int HTTP_MOVED_TEMP = 302;
public static final int HTTP_SEE_OTHER = 303;
public static final int HTTP_NOT_MODIFIED = 304;
public static final int HTTP_USE_PROXY = 305;

/** 4XX: client error */
public static final int HTTP_BAD_REQUEST = 400;
public static final int HTTP_UNAUTHORIZED = 401;
public static final int HTTP_PAYMENT_REQUIRED = 402;
public static final int HTTP_FORBIDDEN = 403;
public static final int HTTP_NOT_FOUND = 404;
public static final int HTTP_BAD_METHOD = 405;
public static final int HTTP_NOT_ACCEPTABLE = 406;
public static final int HTTP_PROXY_AUTH = 407;
public static final int HTTP_CLIENT_TIMEOUT = 408;
public static final int HTTP_CONFLICT = 409;
public static final int HTTP_GONE = 410;
public static final int HTTP_LENGTH_REQUIRED = 411;
public static final int HTTP_PRECON_FAILED = 412;
public static final int HTTP_ENTITY_TOO_LARGE = 413;
public static final int HTTP_REQ_TOO_LONG = 414;
public static final int HTTP_UNSUPPORTED_TYPE = 415;

/** 5XX: server error */
public static final int HTTP_SERVER_ERROR = 500;
public static final int HTTP_INTERNAL_ERROR = 501;
public static final int HTTP_BAD_GATEWAY = 502;
public static final int HTTP_UNAVAILABLE = 503;
public static final int HTTP_GATEWAY_TIMEOUT = 504;
public static final int HTTP_VERSION = 505;
}


Here's how it works: The main thread initializes the server and starts a number of worker threads that will handle client connections. The worker threads simply wait around idle until there's a client to service. The main thread then accepts connections from clients, passes off the connection for a worker thread to handle, and continues accepting new connections.

In Java, there's a fair amount of overhead associated with initializing threads. So for performance and efficiency reasons, I initialize a pool of worker threads once at startup time, rather than on demand. Because the worker threads are usually in an idle state (Object.wait()), they don't consume much CPU power. (We re-use worker threads over many client connections.)

The following are my notes on the classes and other code used to implement the server.

WebServer.loadProps()
In this method, I load configuration properties for the server from a file called www-server.properties. This file needs to be in the lib subdirectory relative to JAVA_HOME, which is where the Java interpreter lives on the local disk. If the file doesn't exists, it will take the current folder as a default.

root
This is the local directory where the HTTP server looks for the files it serves. The root directory name is prefixed to the path of all files requested from clients.

workers
This tells the server how many worker threads in the pool of worker threads to start on initialization.

timeout
This describes the time, in milliseconds, that a worker thread should block while reading from a client connection, before it times out and closes a connection. Without this timeout, a worker thread could be tied up indefinitely waiting for a client to issue a request.

log
This is the name of the log file, where the server will record which clients requested which files. If no log is specified, logging is done on standard output.

WebServer.main()
This is where the server initializes itself. It loads properties, initializes a pool of worker threads, binds a ServerSocket to the local port for our HTTP server, then enters a loop. In this loop, it accepts client connections, and passes the connections off to worker threads in the pool.

class Worker
This class implements java.lang.Runnable. It runs in a worker thread to do the actual work of serving files to clients. Memory allocation in java can be a performance hit at runtime. It is best to reuse allocated objects whenever possible. Our worker threads need a buffer (a byte array) to read and write files to clients. We allocate this buffer once in the constructor and reuse it, rather than needlessly re-allocating new buffers.

Worker.run()
The worker thread spends most of its time idle, at line 162, in wait(). When a new connection is to be serviced, the wait will wake up, and the worker has at it.

Worker.setSocket()
When the main thread has accepted a connection from a client, it finds an idle thread in the worker pool (lines 121-131). It then calls setSocket() on the Worker, which also does a notify() on the Worker. This wakes up the worker thread in wait(), to inform that thread that it's time to work. Note that setSocket() must be synchronized in order to call notify().

Worker.handleClient()
This is the loop where the worker reads the first line of the client's HTTP request. This line is usually of the form GET /foo/bar/baz.html HTTP/1.0. This code shows how to break out of nested loops in java, since java has no goto statement like other languages. What we're trying to find here is the name of the file that the client is requesting (for example, "/foo/bar/baz.html"). There are two nested loops in this code snippet. The outer while() loop is the read loop, and the inner for() loop iterates over the bytes looking for end-of-line characters. The break outerloop statement, though in the inner loop, actually breaks out of the outer loop as well.

class Worker
It is important to always close sockets and files when you're done with them, even if something went wrong. Your Java program, like any program, can have only a finite number of open sockets and files before it can't open any more. So wrap all IO operations in a code segment as following:

try
{ /* open socket or file and do IO */ .... }
finally {
/* cleanup under all exit conditions. */
/* Our finally clause is guaranteed
*to be executed */
/* even if there's a pending exception. */
socket.close();
file.close();
}


Worker.handleClient()
At this point, we're recording the IP address of the client in the server's log, and which file the client requested. The string we're recording is the IP address (that is, "129.144.125.157") of the client, not the hostname (for example, "monkey.eng.sun.com"). We do this for better performance. We do this by calling s.getInetAddress().getHostAddress(), instead of s.getInetAddress().getHostName(). We already know the IP address of the client machine that connected, but we don't know the hostname. If we had asked for the host name at this point, the worker thread might have blocked for a long time while trying to do a reverse DNS look-up out on the Internet. Just recording the IP address in the log is enough. If, at some later point we want to find the hostnames that connected to our server, we can later run a tool over the log file to do this, when time isn't as important.

Click here to check out other programs that I've developed.