10.2 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.
10.2.1 Client Tester
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
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.
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 requestor,
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.
Example 10-5. A client tester
import java.net.*;The client tester application is split into
import java.io.*;
import com.macfaq.io.SafeBufferedReader; // from Chapter 4
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 +"1r1n");
out.flush( );
}
}
catch (IOException ex) {
}
try {
out.close( );
}
catch (IOException ex) {
}
}
}
three classes: 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 Chapter 4. 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:1JAVA1JNP31examples110>java ClientTester 80Even minimal exploration of clients can reveal some surprising
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>
.
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.
10.2.2 HTTP Servers
HTTP is a large protocol. As you saw in
Chapter 3, 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 book of
their own. I refer interested readers to Jason
Hunter's Java Servlet
Programming (O'Reilly). 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.
10.2.2.1 A single-file server
Our
investigation of HTTP servers begins with a server that always sends
out the same file, no matter what the request. It's
called 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.*;The constructors set up the data to be sent along with an HTTP header
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 OK1r1n"
+ "Server: OneFile 1.01r1n"
+ "Content-length: " + this.content.length + "1r1n"
+ "Content-type: " + MIMEType + "1r1n1r1n";
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 == '1r' || c == '1n' || 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("l") || args[0].endsWith("")) {
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);
}
}
}
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
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>
...
10.2.2.2 A redirector
Redirection
is another simple but useful application for a special-purpose HTTP
server. In this section, we develop a server that redirects users
from one web site to anotherfor 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.
Example 10-7. An HTTP redirector
import java.net.*;In order to start the redirector on port 80 and redirect incoming
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 == '1r' || c == '1n' || 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 FOUND1r1n");
Date now = new Date( );
out.write("Date: " + now + "1r1n");
out.write("Server: Redirector 1.01r1n");
out.write("Location: " + newSite + theFile + "1r1n");
out.write("Content-type: text/html1r1n1r1n");
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>1r1n");
out.write("<BODY><H1>Document moved</H1>1r1n");
out.write("The document " + theFile
+ " has moved to1r1n<A HREF=1" + newSite + theFile + "1">"
+ newSite + theFile
+ "</A>.1r1n Please update your bookmarks<P>");
out.write("</BODY></HTML>1r1n");
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
}
requests to java Redirector http://www.ibiblio.org/xml/
Redirecting connections on port 80 to http://www.ibiblio.org/xml/If you connect to this server via Telnet, this is what
you'll see:
% telnet macfaq.dialup.cloud9.net 80If, however, you connect with a reasonably modern web browser, you
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://www.ibiblio.org/xml/">http://www.ibiblio.org/xml/</A>.
Please update your bookmarks<P></BODY></HTML>
Connection closed by foreign host.
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 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/filenamel HTTP/1.0It 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, 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 1r1n--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 FOUNDThis is an HTTP/1.0 response code that tells the client to expect to
be redirected. The second line is a 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>Finally, the connection is closed and the thread dies.
<BODY><H1>Document moved</H1>
The document / has moved to
<A HREF="http://www.ibiblio.org/xml/">http://www.ibiblio.org/xml/</A>.
Please update your bookmarks<P></BODY></HTML>
10.2.2.3 A full-fledged HTTP server
Enough special-purpose HTTP servers. This
next section develops a full-blown HTTP server, called
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.*;The main( ) method of
import java.io.*;
import java.util.*;
public class JHTTP extends Thread {
private File documentRootDirectory;
private String indexFileName = "indexl";
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, "indexl");
}
public JHTTP(File documentRootDirectory) throws IOException {
this(documentRootDirectory, 80, "indexl");
}
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);
}
}
}
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 supportswe
want to send a MIME header only if this is HTTP/1.0 or
laterand 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
l 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.*;This server is functional but still rather austere. Here are a few
import java.io.*;
import java.util.*;
public class RequestProcessor implements Runnable {
private static List pool = new LinkedList( );
private File documentRootDirectory;
private String indexFileName = "indexl";
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 == '1r' || c == '1n') 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 OK1r1n");
Date now = new Date( );
out.write("Date: " + now + "1r1n");
out.write("Server: JHTTP/1.01r1n");
out.write("Content-length: " + theData.length + "1r1n");
out.write("Content-type: " + contentType + "1r1n1r1n");
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 Found1r1n");
Date now = new Date( );
out.write("Date: " + now + "1r1n");
out.write("Server: JHTTP/1.01r1n");
out.write("Content-type: text/html1r1n1r1n");
}
out.write("<HTML>1r1n");
out.write("<HEAD><TITLE>File Not Found</TITLE>1r1n");
out.write("</HEAD>1r1n");
out.write("<BODY>");
out.write("<H1>HTTP Error 404: File Not Found</H1>1r1n");
out.write("</BODY></HTML>1r1n");
out.flush( );
}
}
else { // method does not equal "GET"
if (version.startsWith("HTTP ")) { // send a MIME header
out.write("HTTP/1.0 501 Not Implemented1r1n");
Date now = new Date( );
out.write("Date: " + now + "1r1n");
out.write("Server: JHTTP 1.01r1n");
out.write("Content-type: text/html1r1n1r1n");
}
out.write("<HTML>1r1n");
out.write("<HEAD><TITLE>Not Implemented</TITLE>1r1n");
out.write("</HEAD>1r1n");
out.write("<BODY>");
out.write("<H1>HTTP Error 501: Not Implemented</H1>1r1n");
out.write("</BODY></HTML>1r1n");
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("l") || name.endsWith("")) {
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 "application/octet-stream";
}
else if (name.endsWith(".jpg") || name.endsWith(".jpeg")) {
return "image/jpeg";
}
else return "text/plain";
}
} // end RequestProcessor
features that could be added:A server administration interfaceSupport for CGI programs and/or the Java Servlet APISupport for other request methods, such as POST, HEAD, and PUTA log file in the common web log file formatServer-side includes and/or Java Server PagesSupport for multiple document roots so individual users can have
their own sites
Finally, spend a little time thinking about ways to optimize this
server. If you really want to use 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 Chapter 12.
• Table of Contents• Index• Reviews• Reader Reviews• Errata• AcademicJava Network Programming, 3rd EditionBy
Elliotte Rusty Harold Publisher: O'ReillyPub Date: October 2004ISBN: 0-596-00721-3Pages: 706
Thoroughly revised to cover all the 100+ significant updates
to Java Developers Kit (JDK) 1.5, Java Network
Programming is a complete introduction to
developing network programs (both applets and applications)
using Java, covering everything from networking fundamentals
to remote method invocation (RMI). It includes chapters on
TCP and UDP sockets, multicasting protocol and content
handlers, servlets, and the new I/O API. This is the
essential resource for any serious Java developer.
