Some Useful Servers
This section shows several servers you can build with server sockets. It starts with a server you can use to test client responses and requests, much as you use Telnet to test server behavior. Then three different HTTP servers are presented, each with a different special purpose and each slightly more complex than the previous one.
In the previous chapter, you learned how to use Telnet to experiment with servers. There's no equivalent program to test clients, so let's create one. Example 10-5 is a program called Clients are rarely as forgiving about unexpected server responses as servers are about unexpected client responses. If at all possible, try to run the clients that connect to this program on a Unix system or some other platform that is moderately crash-proof. Don't run them on Mac OS 9 or Windows ME, which are less stable.
This program uses two threads: one to handle input from the client and the other to send output from the server. Using two threads allows the program to handle input and output simultaneously: it can send a response to the client while receiving a request-or, more to the point, it can send data to the client while waiting for the client to respond. This is convenient because different clients and servers talk in unpredictable ways. With some protocols, the server talks first; with others, the client talks first. Sometimes the server sends a one-line response; often, the response is much larger. Sometimes the client and the server talk at each other simultaneously. Other times, one side of the connection waits for the other to finish before it responds. The program must be flexible enough to handle all these cases. Example 10-5 shows the code.
The client tester app is split into three classes: Even minimal exploration of clients can reveal some surprising things. For instance, I didn't know until I wrote this example that Netscape Navigator 4.6 can read .gz files just as easily as it can read HTML files. That might be useful for serving large text files full of redundant data. HTTP is a large protocol. As you saw in , a full-featured HTTP server must respond to requests for files, convert URLs into filenames on the local system, respond to POST and GET requests, handle requests for files that don't exist, interpret MIME types, and much, much more. However, many HTTP servers don't need all of these features. For example, many sites simply display an "under construction" message. Clearly, Apache is overkill for a site like this. Such a site is a candidate for a custom server that does only one thing. Java's network class library makes writing simple servers like this almost trivial. Custom servers aren't useful only for small sites. High-traffic sites like Yahoo! are also candidates for custom servers because a server that does only one thing can often be much faster than a general purpose server such as Apache or Microsoft IIS. It is easy to optimize a special purpose server for a particular task; the result is often much more efficient than a general purpose server that needs to respond to many different kinds of requests. For instance, icons and images that are used repeatedly across many pages or on high-traffic pages might be better handled by a server that read all the image files into memory on startup and then served them straight out of RAM, rather than having to read them off disk for each request. Furthermore, this server could avoid wasting time on logging if you didn't want to track the image requests separately from the requests for the pages they were included in. Finally, Java isn't a bad language for full-featured web servers meant to compete with the likes of Apache or IIS. Even if you believe CPU-intensive Java programs are slower than CPU-intensive C and C++ programs (something I very much doubt is true in modern VMs), most HTTP servers are limited by bandwidth, not by CPU speed. Consequently, Java's other advantages, such as its half-compiled/half-interpreted nature, dynamic class loading, garbage collection, and memory protection really get a chance to shine. In particular, sites that make heavy use of dynamic content through servlets, PHP pages, or other mechanisms can often run much faster when reimplemented on top of a pure or mostly pure Java web server. Indeed, there are several production web servers written in Java, such as the W3C's testbed server Jigsaw (http://www.w3.org/Jigsaw/). Many other web servers written in C now include substantial Java components to support the Java Servlet API and Java Server Pages. On many sites, these are replacing the traditional CGIs, ASPs, and server-side includes, mostly because the Java equivalents are faster and less resource-intensive. I'm not going to explore these technologies here since they easily deserve a tutorial of their own. I refer interested readers to Jason Hunter's Java Servlet Programming (Oracle). However, it is important to note that servers in general and web servers in particular are one area where Java really is competitive with C.
Our investigation of HTTP servers begins with a server that always sends out the same file, no matter what the request. It's called The constructors set up the data to be sent along with an HTTP header that includes information about content length and content encoding. The header and the body of the response are stored in byte arrays in the desired encoding so that they can be blasted to clients very quickly. The Redirection is another simple but useful app for a special-purpose HTTP server. In this section, we develop a server that redirects users from one web site to another-for example, from cnet.com to www.cnet.com. Example 10-7 reads a URL and a port number from the command line, opens a server socket on the port, and redirects all requests that it receives to the site indicated by the new URL using a 302 FOUND code. Chances are this server is fast enough not to require multiple threads. Nonetheless, threads might be mildly advantageous, especially for a high volume site on a slow network connection. But really for purposes of example more than anything, I've made the server multithreaded. In this example, I chose to use a new thread rather than a thread pool for each connection. This is perhaps a little simpler to code and understand but somewhat less efficient. In Example 10-8, we'll look at an HTTP server that uses a thread pool.
In order to start the redirector on port 80 and redirect incoming requests to http://www.ibiblio.org/xml/, type:
If you connect to this server via Telnet, this is what you'll see:
If, however, you connect with a reasonably modern web browser, you should be sent to http://www.ibiblio.org/xml/ with only a slight delay. You should never see the HTML added after the response code; this is only provided to support older browsers that don't do redirection automatically. The It is possible that the first word will be POST or PUT instead or that there will be no HTTP version. The second "word" is the file the client wants to retrieve. This must begin with a slash (/). Browsers are responsible for converting relative URLs to absolute URLs that begin with a slash; the server does not do this. The third word is the version of the HTTP protocol the browser understands. Possible values are nothing at all (pre-HTTP/1.0 browsers), HTTP/1.0, or HTTP/1.1. To handle a request like this, This is an HTTP/1.0 response code that tells the client to expect to be redirected. The second line is a Finally, the connection is closed and the thread dies.
Enough special-purpose HTTP servers. This next section develops a full-blown HTTP server, called The This server is functional but still rather austere. Here are a few features that could be added:
Finally, spend a little time thinking about ways to optimize this server. If you really want to use Client Tester
ClientTester
that runs on a port specified on the command line, shows all data sent by the client, and allows you to send a response to the client by typing it on the command line. For example, you can use this program to see the commands that Internet Explorer sends to a server.
Example 10-5. A client tester
import java.net.*;
import java.io.*;
import com.macfaq.io.SafeBufferedReader; // from
public class ClientTester {
public static void main(String[] args) {
int port;
try {
port = Integer.parseInt(args[0]);
} catch (Exception ex) {
port = 0;
}
try {
ServerSocket server = new ServerSocket(port, 1);
System.out.println("Listening for connections on port " + server.getLocalPort( ));
while (true) {
Socket connection = server.accept( );
try {
System.out.println("Connection established with " + connection);
Thread input = new InputThread(connection.getInputStream( ));
input.start( );
Thread output = new OutputThread(connection.getOutputStream( ));
output.start( );
// wait for output and input to finish try {
input.join( );
output.join( );
}
catch (InterruptedException ex) {
}
}
catch (IOException ex) {
System.err.println(ex); }
finally {
try {
if (connection != null) connection.close( );
}
catch (IOException ex) {}
}
}
}
catch (IOException ex) {
e.printStackTrace( );
}
}
}
class InputThread extends Thread {
InputStream in;
public InputThread(InputStream in) {
this.in = in;
}
public void run( ) {
try { while (true) {
int i = in.read( );
if (i == -1) break;
System.out.write(i);
}
}
catch (SocketException ex) {
// output thread closed the socket
}
catch (IOException ex) {
System.err.println(ex);
}
try {
in.close( );
}
catch (IOException ex) { } }
}
class OutputThread extends Thread {
private Writer out;
public OutputThread(OutputStream out) {
this.out = new OutputStreamWriter(out);
}
public void run( ) {
String line;
BufferedReader in = new SafeBufferedReader(new InputStreamReader(System.in));
try {
while (true) {
line = in.readLine( );
if (line.equals(".")) break;
out.write(line +"\r\n");
out.flush( );
} }
catch (IOException ex) { } try {
out.close( );
}
catch (IOException ex) { } }
}
ClientTester
, InputThread
, and OutputThread
. The ClientTester
class reads the port from the command line, opens a ServerSocket
on that port, and listens for incoming connections. Only one connection is allowed at a time, because this program is designed for experimentation, and a slow human being has to provide all responses. Consequently, it sets an unusually short queue length of 1. Further connections will be refused until the first one has been closed. An infinite while
loop waits for connections with the accept( )
method. When a connection is detected, its InputStream
is used to construct a new InputThread
and its OutputStream
is used to construct a new OutputThread
. After starting these threads, the program waits for them to finish by calling their join()
methods. The InputThread
is contained almost entirely in the run( )
method. It has a single field, in
, which is the InputStream
from which data will be read. Data is read from in
one byte at a time. Each byte
read is written on System.out
. The run( )
method ends when the end of stream is encountered or an IOException
is thrown. The most likely exception here is a SocketException
thrown because the corresponding OutputThread
closed the connection. The OutputThread
reads input from the local user sitting at the terminal and sends that data to the client. Its constructor has a single argument, an output stream for sending data to the client. OutputThread
reads input from the user on System.in
, which is chained to an instance of the SafeBufferedReader
class developed in . The OutputStream
that was passed to the constructor is chained to an OutputStreamWriter
for convenience. The run( )
method for OutputThread
reads lines from the SafeBufferedReader
and copies them onto the OutputStreamWriter
, which sends them to the client. A period typed on a line by itself signals the end of user input. When this occurs, run( )
exits the loop and out
is closed. This has the effect of also closing the socket so that a SocketException
is thrown in the input thread, which also exits. For example, here's the output when Netscape Communicator 4.6 for Windows connected to this server:
D:\JAVA\JNP3\examples\10>java ClientTester 80
Listening for connections on port 80
Connection established with Socket[addr=localhost/127.0.0.1,port=1033,localport=80]
GET / HTTP/1.0
Connection: Keep-Alive User-Agent: Mozilla/4.6 [en] (WinNT; I)
Host: localhost Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */*
Accept-Encoding: gzip Accept-Language: en Accept-Charset: iso-8859-1,*,utf-8
<html><body><h1>Hello Client!</h1></body></html>
.
HTTP Servers
A single-file server
SingleFileHTTPServer
and is shown in Example 10-6. The filename, local port, and content encoding are read from the command line. If the port is omitted, port 80 is assumed. If the encoding is omitted, ASCII is assumed.
Example 10-6. An HTTP server that chunks out the same file
import java.net.*;
import java.io.*;
import java.util.*;
public class SingleFileHTTPServer extends Thread {
private byte[] content;
private byte[] header;
private int port = 80;
public SingleFileHTTPServer(String data, String encoding, String MIMEType, int port) throws UnsupportedEncodingException { this(data.getBytes(encoding), encoding, MIMEType, port);
}
public SingleFileHTTPServer(byte[] data, String encoding, String MIMEType, int port) throws UnsupportedEncodingException {
this.content = data;
this.port = port;
String header = "HTTP/1.0 200 OK\r\n"
+ "Server: OneFile 1.0\r\n"
+ "Content-length: " + this.content.length + "\r\n"
+ "Content-type: " + MIMEType + "\r\n\r\n";
this.header = header.getBytes("ASCII");
}
public void run( ) {
try {
ServerSocket server = new ServerSocket(this.port); System.out.println("Accepting connections on port " + server.getLocalPort( ));
System.out.println("Data to be sent:");
System.out.write(this.content);
while (true) {
Socket connection = null;
try {
connection = server.accept( );
OutputStream out = new BufferedOutputStream(
connection.getOutputStream( )
);
InputStream in = new BufferedInputStream(
connection.getInputStream( )
);
// read the first line only; that's all we need
StringBuffer request = new StringBuffer(80);
while (true) {
int c = in.read( );
if (c == '\r' || c == '\n' || c == -1) break;
request.append((char) c);
}
// If this is HTTP/1.0 or later send a MIME header
if (request.toString( ).indexOf("HTTP/") != -1) {
out.write(this.header);
} out.write(this.content);
out.flush( );
} // end try
catch (IOException ex) { }
finally {
if (connection != null) connection.close( ); }
} // end while
} // end try
catch (IOException ex) {
System.err.println("Could not start server. Port Occupied");
}
} // end run
public static void main(String[] args) {
try {
String contentType = "text/plain";
if (args[0].endsWith(".html") || args[0].endsWith(".htm")) {
contentType = "text/html";
}
InputStream in = new FileInputStream(args[0]);
ByteArrayOutputStream out = new ByteArrayOutputStream( );
int b;
while ((b = in.read( )) != -1) out.write(b);
byte[] data = out.toByteArray( );
// set the port to listen on
int port;
try {
port = Integer.parseInt(args[1]);
if (port < 1 || port > 65535) port = 80;
} catch (Exception ex) {
port = 80;
} String encoding = "ASCII";
if (args.length >= 2) encoding = args[2]; Thread t = new SingleFileHTTPServer(data, encoding,
contentType, port);
t.start( ); }
catch (ArrayIndexOutOfBoundsException ex) {
System.out.println(
"Usage: java SingleFileHTTPServer filename port encoding");
}
catch (Exception ex) {
System.err.println(ex);
}
}
}
SingleFileHTTPServer
class itself is a subclass of Thread
. Its run( )
method processes incoming connections. Chances are this server will serve only small files and will support only low-volume web sites. Since all the server needs to do for each connection is check whether the client supports HTTP/1.0 and spew one or two relatively small byte arrays over the connection, chances are this will be sufficient. On the other hand, if you find clients are getting refused, you could use multiple threads instead. A lot depends on the size of the file served, the peak number of connections expected per minute, and the thread model of Java on the host machine. Using multiple threads would be a clear win for a server that was even slightly more sophisticated than this one. The run( )
method creates a ServerSocket
on the specified port. Then it enters an infinite loop that continually accepts connections and processes them. When a socket is accepted, an InputStream
reads the request from the client. It looks at the first line to see whether it contains the string HTTP
. If it sees this string, the server assumes that the client understands HTTP/1.0 or later and therefore sends a MIME header for the file; then it sends the data. If the client request doesn't contain the string HTTP
, the server omits the header, sending the data by itself. Finally, the server closes the connection and tries to accept the next connection. The main( )
method just reads parameters from the command line. The name of the file to be served is read from the first command-line argument. If no file is specified or the file cannot be opened, an error message is printed and the program exits. Assuming the file can be read, its contents are read into the byte
array data
. A reasonable guess is made about the content type of the file, and that guess is stored in the contentType
variable. Next, the port number is read from the second command-line argument. If no port is specified or if the second argument is not an integer from 0 to 65,535, port 80 is used. The encoding is read from the third command-line argument, if present. Otherwise, ASCII is assumed. (Surprisingly, some VMs don't support ASCII, so you might want to pick 8859-1 instead.) Then these values are used to construct a SingleFileHTTPServer
object and start it running. This is only one possible interface. You could easily use this class as part of some other program. If you added a setter method to change the content, you could easily use it to provide simple status information about a running server or system. However, that would raise some additional issues of thread safety that Example 10-6 doesn't have to address because it's immutable. Here's what you see when you connect to this server via Telnet; the specifics depend on the exact server and file:
% telnet macfaq.dialup.cloud9.net 80
Trying 168.100.203.234...
Connected to macfaq.dialup.cloud9.net.
Escape character is '^]'.
GET / HTTP/1.0
HTTP/1.0 200 OK Server: OneFile 1.0
Content-length: 959
Content-type: text/html
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
<HTML>
<HEAD>
<TITLE>Under Construction</TITLE>
</HEAD>
<BODY>
...
A redirector
Example 10-7. An HTTP redirector
import java.net.*;
import java.io.*;
import java.util.*;
public class Redirector implements Runnable {
private int port;
private String newSite;
public Redirector(String site, int port) {
this.port = port;
this.newSite = site;
}
public void run( ) {
try {
ServerSocket server = new ServerSocket(this.port); System.out.println("Redirecting connections on port " + server.getLocalPort( ) + " to " + newSite);
while (true) {
try {
Socket s = server.accept( );
Thread t = new RedirectThread(s);
t.start( );
} // end try
catch (IOException ex) { }
} // end while
} // end try
catch (BindException ex) {
System.err.println("Could not start server. Port Occupied");
} catch (IOException ex) {
System.err.println(ex);
} } // end run
class RedirectThread extends Thread {
private Socket connection;
RedirectThread(Socket s) {
this.connection = s; }
public void run( ) {
try {
Writer out = new BufferedWriter(
new OutputStreamWriter(
connection.getOutputStream( ), "ASCII"
)
);
Reader in = new InputStreamReader(
new BufferedInputStream( connection.getInputStream( )
)
);
// read the first line only; that's all we need
StringBuffer request = new StringBuffer(80);
while (true) {
int c = in.read( );
if (c == '\r' || c == '\n' || c == -1) break;
request.append((char) c);
}
// If this is HTTP/1.0 or later send a MIME header
String get = request.toString( );
int firstSpace = get.indexOf(' ');
int secondSpace = get.indexOf(' ', firstSpace+1);
String theFile = get.substring(firstSpace+1, secondSpace);
if (get.indexOf("HTTP") != -1) {
out.write("HTTP/1.0 302 FOUND\r\n");
Date now = new Date( );
out.write("Date: " + now + "\r\n");
out.write("Server: Redirector 1.0\r\n");
out.write("Location: " + newSite + theFile + "\r\n"); out.write("Content-type: text/html\r\n\r\n"); out.flush( ); }
// Not all browsers support redirection so we need to // produce HTML that says where the document has moved to.
out.write("<HTML><HEAD><TITLE>Document moved</TITLE></HEAD>\r\n");
out.write("<BODY><H1>Document moved</H1>\r\n");
out.write("The document " + theFile + " has moved to\r\n<A HREF=\"" + newSite + theFile + "\">" + newSite + theFile + "</A>.\r\n Please update your tutorialmarks<P>");
out.write("</BODY></HTML>\r\n");
out.flush( );
} // end try
catch (IOException ex) {
}
finally {
try {
if (connection != null) connection.close( );
}
catch (IOException ex) {} } } // end run
}
public static void main(String[] args) {
int thePort;
String theSite;
try {
theSite = args[0];
// trim trailing slash
if (theSite.endsWith("/")) {
theSite = theSite.substring(0, theSite.length( )-1);
}
}
catch (Exception ex) {
System.out.println(
"Usage: java Redirector http://www.newsite.com/ port");
return;
}
try {
thePort = Integer.parseInt(args[1]);
} catch (Exception ex) {
thePort = 80;
} Thread t = new Thread(new Redirector(theSite, thePort));
t.start( );
} // end main
}
D:\JAVA\JNP3\examples\10>java Redirector http://www.ibiblio.org/xml/
Redirecting connections on port 80 to http://www.ibiblio.org/xml/
% telnet macfaq.dialup.cloud9.net 80
Trying 168.100.203.234...
Connected to macfaq.dialup.cloud9.net.
Escape character is '^]'.
GET / HTTP/1.0
HTTP/1.0 302 FOUND Date: Wed Sep 08 11:59:42 PDT 1999
Server: Redirector 1.0
Location: http://www.ibiblio.org/xml/
Content-type: text/html
<HTML><HEAD><TITLE>Document moved</TITLE></HEAD>
<BODY><H1>Document moved</H1>
The document / has moved to
<A href="http://web.archive.org/web/www.ibiblio.org/xml/">http://www.ibiblio.org/xml/</A>.
Please update your tutorialmarks<P></BODY></HTML>
Connection closed by foreign host.
main( )
method provides a very simple interface that reads the URL of the new site to redirect connections to and the local port to listen on. It uses this information to construct a Redirector
object. Then it uses the resulting Runnable
object (Redirector
implements Runnable
) to spawn a new thread and start it. If the port is not specified, Redirector
listens on port 80. If the site is omitted, Redirector
prints an error message and exits. The run()
method of Redirector
binds the server socket to the port, prints a brief status message, and then enters an infinite loop in which it listens for connections. Every time a connection is accepted, the resulting Socket
object is used to construct a RedirectThread
. This RedirectThread
is then started. All further interaction with the client takes place in this new thread. The run( )
method of Redirector
then simply waits for the next incoming connection. The run( )
method of RedirectThread
does most of the work. It begins by chaining a Writer
to the Socket
's output stream and a Reader
to the Socket
's input stream. Both input and output are buffered. Then the run( )
method reads the first line the client sends. Although the client will probably send a whole MIME header, we can ignore that. The first line contains all the information we need. The line looks something like this:
GET /directory/filename.html HTTP/1.0
Redirector
ignores the first word. The second word is attached to the URL of the target server (stored in the field newSite
) to give a full redirected URL. The third word is used to determine whether to send a MIME header; MIME headers are not used for old browsers that do not understand HTTP/1.0. If there is a version, a MIME header is sent; otherwise, it is omitted. Sending the data is almost trivial. The Writer
out
is used. Since all the data we send is pure ASCII, the exact encoding isn't too important. The only trick here is that the end-of-line character for HTTP requests is \r\n--
a carriage return followed by a linefeed. The next lines each send one line of text to the client. The first line printed is:
HTTP/1.0 302 FOUND
Date
: header that gives the current time at the server. This line is optional. The third line is the name and version of the server; this line is also optional but is used by spiders that try to keep statistics about the most popular web servers. (It would be very surprising to ever see Redirector
break into single digits in lists of the most popular servers.) The next line is the Location
: header, which is required for this server. It tells the client where it is being redirected to. Last is the standard Content-type
: header. We send the content type text/html
to indicate that the client should expect to see HTML. Finally, a blank line is sent to signify the end of the header data. Everything after this will be HTML, which is processed by the browser and displayed to the user. The next several lines print a message for browsers that do not support redirection, so those users can manually jump to the new site. That message looks like:
<HTML><HEAD><TITLE>Document moved</TITLE></HEAD>
<BODY><H1>Document moved</H1>
The document / has moved to
<A href="http://web.archive.org/web/www.ibiblio.org/xml/">http://www.ibiblio.org/xml/</A>.
Please update your tutorialmarks<P></BODY></HTML>
A full-fledged HTTP server
JHTTP
, that can serve an entire document tree, including images, applets, HTML files, text files, and more. It will be very similar to the SingleFileHTTPServer
, except that it pays attention to the GET requests. This server is still fairly lightweight; after looking at the code, we'll discuss other features we might want to add. Since this server may have to read and serve large files from the filesystem over potentially slow network connections, we'll change its approach. Rather than processing each request as it arrives in the main thread of execution, we'll place incoming connections in a pool. Separate instances of a RequestProcessor
class will remove the connections from the pool and process them. Example 10-8 shows the main JHTTP
class. As in the previous two examples, the main( )
method of JHTTP
handles initialization, but other programs can use this class to run basic web servers.
Example 10-8. The JHTTP web server
import java.net.*;
import java.io.*;
import java.util.*;
public class JHTTP extends Thread {
private File documentRootDirectory;
private String indexFileName = "index-network-dev-java-programming-language.html.gz";
private ServerSocket server;
private int numThreads = 50;
public JHTTP(File documentRootDirectory, int port, String indexFileName) throws IOException {
if (!documentRootDirectory.isDirectory( )) {
throw new IOException(documentRootDirectory + " does not exist as a directory"); }
this.documentRootDirectory = documentRootDirectory;
this.indexFileName = indexFileName;
this.server = new ServerSocket(port);
}
public JHTTP(File documentRootDirectory, int port) throws IOException {
this(documentRootDirectory, port, "index-network-dev-java-programming-language.html.gz");
}
public JHTTP(File documentRootDirectory) throws IOException {
this(documentRootDirectory, 80, "index-network-dev-java-programming-language.html.gz");
}
public void run( ) {
for (int i = 0; i < numThreads; i++) {
Thread t = new Thread(
new RequestProcessor(documentRootDirectory, indexFileName));
t.start( ); }
System.out.println("Accepting connections on port " + server.getLocalPort( ));
System.out.println("Document Root: " + documentRootDirectory);
while (true) {
try {
Socket request = server.accept( );
RequestProcessor.processRequest(request);
}
catch (IOException ex) { } }
}
public static void main(String[] args) {
// get the Document root
File docroot;
try {
docroot = new File(args[0]);
}
catch (ArrayIndexOutOfBoundsException ex) {
System.out.println("Usage: java JHTTP docroot port indexfile");
return;
}
// set the port to listen on
int port;
try {
port = Integer.parseInt(args[1]);
if (port < 0 || port > 65535) port = 80;
} catch (Exception ex) {
port = 80;
} try { JHTTP webserver = new JHTTP(docroot, port);
webserver.start( );
}
catch (IOException ex) {
System.out.println("Server could not start because of an " + e.getClass( ));
System.out.println(e);
}
}
}
main( )
method of the JHTTP
class sets the document root directory from args[0]
. The port is read from args[1]
or 80 is used for a default. Then a new JHTTP
thread is constructed and started. The JHTTP
thread spawns 50 RequestProcessor
threads to handle requests, each of which retrieves incoming connection requests from the RequestProcessor
pool as they become available. The JHTTP thread repeatedly accepts incoming connections and puts them in the RequestProcessor
pool. Each connection is handled by the run( )
method of the RequestProcessor
class shown in Example 10-9. This method waits until it can get a Socket
out of the pool. Once it does that, it gets input and output streams from the socket and chains them to a reader and a writer. The reader reads the first line of the client request to determine the version of HTTP that the client supports-we want to send a MIME header only if this is HTTP/1.0 or later-and the requested file. Assuming the method is GET
, the file that is requested is converted to a filename on the local filesystem. If the file requested is a directory (i.e., its name ends with a slash), we add the name of an index file. We use the canonical path to make sure that the requested file doesn't come from outside the document root directory. Otherwise, a sneaky client could walk all over the local filesystem by including .
. in URLs to walk up the directory hierarchy. This is all we'll need from the client, although a more advanced web server, especially one that logged hits, would read the rest of the MIME header the client sends. Next, the requested file is opened and its contents are read into a byte array. If the HTTP version is 1.0 or later, we write the appropriate MIME headers on the output stream. To figure out the content type, we call the guessContentTypeFromName()
method to map file extensions such as .html onto MIME types such as text/html. The byte
array containing the file's contents is written onto the output stream and the connection is closed. Exceptions may be thrown at various places if, for example, the file cannot be found or opened. If an exception occurs, we send an appropriate HTTP error message to the client instead of the file's contents.
Example 10-9. The thread pool that handles HTTP requests
import java.net.*;
import java.io.*;
import java.util.*;
public class RequestProcessor implements Runnable {
private static List pool = new LinkedList( );
private File documentRootDirectory;
private String indexFileName = "index-network-dev-java-programming-language.html.gz";
public RequestProcessor(File documentRootDirectory, String indexFileName) {
if (documentRootDirectory.isFile( )) {
throw new IllegalArgumentException(
"documentRootDirectory must be a directory, not a file"); }
this.documentRootDirectory = documentRootDirectory;
try {
this.documentRootDirectory = documentRootDirectory.getCanonicalFile( );
}
catch (IOException ex) {
}
if (indexFileName != null) this.indexFileName = indexFileName;
}
public static void processRequest(Socket request) {
synchronized (pool) {
pool.add(pool.size( ), request);
pool.notifyAll( );
}
} public void run( ) {
// for security checks
String root = documentRootDirectory.getPath( );
while (true) { Socket connection;
synchronized (pool) { while (pool.isEmpty( )) {
try {
pool.wait( );
}
catch (InterruptedException ex) {
}
}
connection = (Socket) pool.remove(0); }
try { String filename;
String contentType; OutputStream raw = new BufferedOutputStream(
connection.getOutputStream( )
); Writer out = new OutputStreamWriter(raw);
Reader in = new InputStreamReader(
new BufferedInputStream(
connection.getInputStream( )
),"ASCII"
);
StringBuffer requestLine = new StringBuffer( );
int c;
while (true) {
c = in.read( );
if (c == '\r' || c == '\n') break;
requestLine.append((char) c);
}
String get = requestLine.toString( );
// log the request System.out.println(get);
StringTokenizer st = new StringTokenizer(get);
String method = st.nextToken( );
String version = "";
if (method.equals("GET")) {
filename = st.nextToken( );
if (filename.endsWith("/")) filename += indexFileName;
contentType = guessContentTypeFromName(filename);
if (st.hasMoreTokens( )) {
version = st.nextToken( );
}
File theFile = new File(documentRootDirectory, filename.substring(1,filename.length( )));
if (theFile.canRead( ) // Don't let clients outside the document root
&& theFile.getCanonicalPath( ).startsWith(root)) {
DataInputStream fis = new DataInputStream(
new BufferedInputStream(
new FileInputStream(theFile)
)
);
byte[] theData = new byte[(int) theFile.length( )];
fis.readFully(theData);
fis.close( );
if (version.startsWith("HTTP ")) { // send a MIME header
out.write("HTTP/1.0 200 OK\r\n");
Date now = new Date( );
out.write("Date: " + now + "\r\n");
out.write("Server: JHTTP/1.0\r\n");
out.write("Content-length: " + theData.length + "\r\n");
out.write("Content-type: " + contentType + "\r\n\r\n");
out.flush( );
} // end if
// send the file; it may be an image or other binary data // so use the underlying output stream // instead of the writer
raw.write(theData);
raw.flush( );
} // end if
else { // can't find the file
if (version.startsWith("HTTP ")) { // send a MIME header
out.write("HTTP/1.0 404 File Not Found\r\n");
Date now = new Date( );
out.write("Date: " + now + "\r\n");
out.write("Server: JHTTP/1.0\r\n");
out.write("Content-type: text/html\r\n\r\n");
} out.write("<HTML>\r\n");
out.write("<HEAD><TITLE>File Not Found</TITLE>\r\n");
out.write("</HEAD>\r\n");
out.write("<BODY>");
out.write("<H1>HTTP Error 404: File Not Found</H1>\r\n");
out.write("</BODY></HTML>\r\n");
out.flush( );
}
}
else { // method does not equal "GET"
if (version.startsWith("HTTP ")) { // send a MIME header
out.write("HTTP/1.0 501 Not Implemented\r\n");
Date now = new Date( );
out.write("Date: " + now + "\r\n");
out.write("Server: JHTTP 1.0\r\n");
out.write("Content-type: text/html\r\n\r\n"); } out.write("<HTML>\r\n");
out.write("<HEAD><TITLE>Not Implemented</TITLE>\r\n");
out.write("</HEAD>\r\n");
out.write("<BODY>");
out.write("<H1>HTTP Error 501: Not Implemented</H1>\r\n");
out.write("</BODY></HTML>\r\n");
out.flush( );
}
}
catch (IOException ex) {
}
finally {
try {
connection.close( ); }
catch (IOException ex) {} }
} // end while
} // end run
public static String guessContentTypeFromName(String name) {
if (name.endsWith(".html") || name.endsWith(".htm")) {
return "text/html";
}
else if (name.endsWith(".txt") || name.endsWith(".java")) {
return "text/plain";
}
else if (name.endsWith(".gif")) {
return "image/gif";
}
else if (name.endsWith(".class")) {
return "app/octet-stream";
}
else if (name.endsWith(".jpg") || name.endsWith(".jpeg")) {
return "image/jpeg";
}
else return "text/plain";
} } // end RequestProcessor
JHTTP
to run a high-traffic site, there are a couple of things that can speed this server up. The first and most important is to use a Just-in-Time (JIT) compiler such as HotSpot. JITs can improve program performance by an order of magnitude or more. The second thing to do is implement smart caching. Keep track of the requests you've received and store the data from the most frequently requested files in a Hashtable
so that they're kept in memory. Use a low-priority thread to update this cache. Another option for developers using Java 1.4 or later is to use non-blocking I/O and channels instead of threads and streams. We'll explore this possibility in .