|
| |
To make things perhaps a little clearer, let's implement a very simple web
server.
First, here's the main class, HttpServer, which opens a
server socket, and responds to client connections
import java.io.IOException;
import java.net.ServerSocket;
import java.util.Date;
/**
* Java HTTP Server
*
* This is a very simple HTTP server.
*
*/
public class HttpServer
{
public static boolean VERBOSE = false;
/**
* Usage
*/
private static void usage()
{
//print instructions to standard out
String instructions =
"usage: java HttpServer [-options] [port-number]\n\n" +
"where options include:\n" +
" -? -help\t print out this message\n" +
" -v -verbose\t turn on verbose mode\n\n" +
" and port-number is the port number to listen on";
System.out.println(instructions);
System.out.flush();
}
/**
* Extract command line arguments
*/
private static void extractArguments(String[] args)
{
if (args.length > 0)
{
int arg = 0;
if (args[arg].equals("-?") || args[arg].equals("-help"))
{
usage();
arg++;
}
if (args.length >= arg)
{
if (args[arg].equals("-v") || args[arg].equals("-verbose"))
{
VERBOSE = true;
arg++;
}
}
if (args.length >= arg)
{
try
{
m_port = Integer.parseInt(args[arg]);
}
catch (java.lang.NumberFormatException nfe)
{
System.err.println("Invalid port number specified: " + args[arg]);
System.err.println(" Defaulting to: " + m_port);
}
}
}
}
/**
* Main method
*
* @param args [-options] [port-number]
*/
public static void main(String[] args)
{
extractArguments(args);
ServerSocket serverConnect = null;
try
{
System.out.println("\nConnecting to port " + m_port + "...\n");
serverConnect = new ServerSocket(m_port); //listen on port
System.out.println("Listening for connections on port " + m_port + "...\n");
while (true) //listen until user halts execution
{
RequestHandler handler = new RequestHandler(serverConnect.accept());
if (VERBOSE)
{
System.out.println("Connection opened. (" + new Date() + ")");
}
handler.start(); // Start the request handler thread
}
}
catch (IOException e)
{
System.err.println("Server error: " + e);
}
}
///// Private data /////
private static int m_port = 8080; //default port value
} |
In its main method, it connects to the specified port, and when it accepts a
client connection, it creates a new instance of the RequestHandler
class, which will receive the request, and process it.
Here's RequestHandler, which is the class that does most
of the work:
import java.io.BufferedReader;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStreamReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.StringTokenizer;
import java.util.Date;
/**
* HTTP Request Handler
*
* This class handles a single request received by the HTTP server.
* It currently supports only the GET method.
*
*/
public class RequestHandler extends Thread
{
// Constructor
public RequestHandler(Socket connect)
{
m_connect = connect;
}
/**
* Services each request in a separate thread
*/
public void run()
{
BufferedReader in = null;
PrintWriter out = null;
BufferedOutputStream dataOut = null;
try
{
// Get character input stream from the client
in = new BufferedReader(new InputStreamReader(m_connect.getInputStream()));
// Get output stream to client
OutputStream clientOut = m_connect.getOutputStream();
// Get character output stream to client (for headers)
if (HttpServer.VERBOSE)
{
// Insert a TeeWriter to capture the response header
// output by the rest of the program.
out = new PrintWriter(
new TeeWriter(
new OutputStreamWriter(clientOut),
new OutputStreamWriter(System.out)
)
);
}
else
{
out = new PrintWriter(m_connect.getOutputStream());
}
// Get binary output stream to client (for requested data)
dataOut = new BufferedOutputStream(clientOut);
// Get first line of request from client
String input = in.readLine();
// Save the header for this request
m_requestHeader = input;
while (true)
{
String line = in.readLine();
// Read until end of stream, or blank line
if (line == null || line.length() == 0)
break;
m_requestHeader += ("\n<br>" + line);
}
// Use StringTokenizer to parse request's first line, which is of the form:
// method file-requested HTTP/version-number
// where:
// method is GET, HEAD, POST, etc.
// version-number is 1.0, 1.1, etc.
//
StringTokenizer parse = new StringTokenizer(input);
String method = parse.nextToken().toUpperCase();// Parse out method
String fileRequested = parse.nextToken().toLowerCase();// file
if (HttpServer.VERBOSE)
{
System.out.println("--------Begin Request-----");
System.out.println(m_requestHeader);
System.out.println("--------End Request-------");
System.out.flush();
}
// Dispatch, based on method type
if (method.equals("GET"))
{
processGetRequest(fileRequested, in, out, dataOut);
}
else
{
// Other HTTP methods are not yet implemented in this HTTP server
unsupportedRequest(method, in, out);
}
}
catch (IOException e)
{
System.err.println("Server Error: " + e);
}
finally
{
// Ensure all streams are closed
try
{
if (in != null)
in.close();
if (out != null)
out.close();
if (dataOut != null)
dataOut.close();
// Ensure the connection is closed
m_connect.close();
if (HttpServer.VERBOSE)
{
System.out.println("Connection closed.\n");
}
}
catch (IOException ioe)
{
// Do nothing -- no recovery possible.
}
}
}
/**
* Process a GET request
*/
private void processGetRequest(String fileRequested,
BufferedReader in,
PrintWriter out,
BufferedOutputStream dataOut)
throws IOException
{
// Handle "special" requests
int index = fileRequested.indexOf("special/");
if (index != -1)
{
processSpecialGetRequest(fileRequested, in, out);
return;
}
if (fileRequested.endsWith("/"))
{
//append default file name to request
fileRequested += DEFAULT_FILE;
}
FileInputStream fileIn = null;
try
{
File file = new File(WEB_ROOT, fileRequested);
if (!file.exists())
{
throw new FileNotFoundException();
}
// Get the file's MIME content type
String content = MIMETypes.getType(fileRequested);
// Generate HTTP response header
out.println("HTTP/1.0 200 OK");
out.println("Server: HttpServer 1.0");
out.println("Date: " + new Date());
out.println("Content-type: " + content);
out.println("Content-length: " + file.length());
out.println(); // Blank line between headers and content
out.flush(); // Flush character output stream buffer
// Now the content...
// Open input stream on the file
fileIn = new FileInputStream(file);
// Create a byte array to store the file contents
byte[] fileData = new byte[(int)file.length()];
// Read file into byte array
fileIn.read(fileData);
// Write file to stream
dataOut.write(fileData, 0, (int)file.length());
dataOut.flush(); //flush binary output stream buffer
if (HttpServer.VERBOSE)
{
System.out.println("File " + fileRequested +
" of type " + content + " returned.");
}
}
catch (IOException e)
{
// Tell client that file doesn't exist
fileNotFound(fileRequested, out);
}
finally
{
// Ensure that the fileIn is closed.
if (fileIn != null)
fileIn.close();
}
}
/**
* Process a "special" GET request.
* Currently, the only implementation of this is where the user
* specifies a "special/" directory in the request. In this case,
* the special GET request simply returns the contents of the request
* header in a displayable form on the browser.
*/
private void processSpecialGetRequest(String fileRequested,
BufferedReader in,
PrintWriter out)
throws IOException
{
String page =
"<HTML>\n" +
"<HEAD><TITLE>Request Header Echo</TITLE></HEAD>\n" +
"<BODY>\n" +
"<H2>The Header from this Request was:</H2>\n" +
m_requestHeader + "\n" +
"</BODY>\n" +
"</HTML>";
// Generate HTTP response header
out.println("HTTP/1.0 200 OK");
out.println("Server: HttpServer 1.0");
out.println("Date: " + new Date());
out.println("Content-type: " + "text/html");
out.println("Content-length: " + page.length());
out.println(); // Blank line between headers and content
out.println(page);
out.flush(); // Flush character output stream buffer
}
/**
* Process unsupported requests
*/
private void unsupportedRequest(String method, BufferedReader in, PrintWriter out)
{
if (HttpServer.VERBOSE)
{
System.out.println("501 Not Implemented: " + method + " method.");
}
// Send Not Implemented message to client
out.println("HTTP/1.0 501 Not Implemented");
out.println("Server: HttpServer 1.0");
out.println("Date: " + new Date());
out.println("Content-Type: text/html");
out.println(); // Blank line between headers and content
out.println("<HTML>");
out.println("<HEAD><TITLE>Not Implemented</TITLE></HEAD>");
out.println("<BODY>");
out.println("<H2>501 Not Implemented: " + method + " method.</H2>");
out.println("</BODY>");
out.println("</HTML>");
out.flush(); // Flush character output stream buffer
}
/**
* Informs client that requested file does not exist.
*/
private void fileNotFound(String fileRequested, PrintWriter out)
throws IOException
{
if (HttpServer.VERBOSE)
{
System.out.println("404 File Not Found: " + fileRequested);
}
out.println("HTTP/1.0 404 File Not Found");
out.println("Server: HttpServer 1.0");
out.println("Date: " + new Date());
out.println("Content-Type: text/html");
out.println(); // Blank line between headers and content
out.println("<HTML>");
out.println("<HEAD><TITLE>File Not Found</TITLE></HEAD>");
out.println("<BODY>");
out.println("<H2>404 File Not Found: " + fileRequested + "</H2>");
out.println("</BODY>");
out.println("</HTML>");
out.flush(); // Flush character output stream buffer
}
///// Private data /////
// HttpServer root is the current directory
private static final File WEB_ROOT = new File(".");
// Default file name for unspecified request filename
private static final String DEFAULT_FILE = "index.html";
private Socket m_connect;
private String m_requestHeader;
} |
RequestHandler makes use of some useful utility
classes.
First, it relies on the MIMETypes class to map a filetype
to a MIME type:
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* This class provides a mapping from filetype to MIME type.
*/
public class MIMETypes
{
/**
* Returns the MIME type, given a filename
*/
public static String getType(String fileName)
{
String type = null;
// Extract the filetype from the filename
int dotIndex = fileName.lastIndexOf('.');
if (dotIndex != -1)
{
String fileType = fileName.substring(dotIndex);
// Now, look up the filetype to get the MIME type
type = (String) m_map.get(fileType);
}
if (type == null)
type = UNKNOWN_MIME_TYPE;
return type;
}
/**
* Adds a file type/MIME type mapping.
*/
public static void add(String fileType, String MIMEType)
{
m_map.put(fileType, MIMEType);
}
/**
* Removes a file type/MIME type mapping
*/
public static void remove(String fileType)
{
m_map.remove(fileType);
}
/**
* Suppress the constructor, since all methods in this class are static.
*/
private MIMETypes()
{
}
/////// Private data ///////
// The HashMap used to store the relationships between filetype and MIME type
private static HashMap m_map = new HashMap();
// The MIME types this web server is initially aware of
private static final String[][] m_initialValues =
{
{ ".class", "application/x-java-vm" },
{ ".css", "text/css" },
{ ".doc", "application/msword" },
{ ".gif", "image/gif" },
{ ".htm", "text/html" },
{ ".html", "text/html" },
{ ".jar", "application/x-java-archive" },
{ ".jpg", "image/jpeg" },
{ ".jpeg", "image/jpeg" },
{ ".mpg", "video/mpeg" },
{ ".mpeg", "video/mpeg" },
{ ".pdf", "application/pdf" },
{ ".png", "image/png" },
{ ".ppt", "application/vnd.ms-powerpoint" },
{ ".ps", "application/postscript" },
{ ".ser", "application/x-java-serialized-object" },
{ ".tiff", "image/tiff" },
{ ".txt", "text/plain" },
{ ".xls", "application/vnd.ms-excel" },
{ ".zip", "application/zip" },
};
// Populate the HashMap on class load.
static
{
for (int i = 0; i < m_initialValues.length; i++)
{
String key = m_initialValues[i][0];
String value = m_initialValues[i][1];
m_map.put(key, value);
}
}
private static final String UNKNOWN_MIME_TYPE = "application/octet-stream";
/**
* Main entry point, for testing
*/
public static void main(String[] args)
{
System.out.println("----BEGIN-----");
for (Iterator iter = m_map.entrySet().iterator(); iter.hasNext(); )
{
Map.Entry entry = (Map.Entry) iter.next();
String key = (String) entry.getKey();
String value = (String) entry.getValue();
System.out.println(key + " : " + value);
}
System.out.println("-----END-----");
String type = null;
type = getType("bar");
System.out.println("foo type is: " + type);
type = getType(".foo");
System.out.println(".foo type is: " + type);
type = getType(".css");
System.out.println(".css type is: " + type);
type = getType("fromage.pdf");
System.out.println("fromage.pdf type is: " + type);
}
} |
Note that MIMETypes class has only static methods.
The second utility class is TeeWriter, which is a class
for inserting a "Tee" into a stream, and "siphoning off" the
output sent to the stream. It is used to capture the response header
generated by different parts of the RequestHeader
class. Here it is:
import java.io.FileWriter;
import java.io.FilterWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Writer;
/**
* This class provides a "Tee" so that anything written to it is sent to
* both its Writers. This can be quite useful for logging everything
* written to a stream.
*/
public class TeeWriter extends FilterWriter
{
/**
* Constructor
*/
public TeeWriter(Writer out1, Writer out2)
{
super(out1);
m_out = out2;
}
/**
* Write a single character.
*
* @exception IOException If an I/O error occurs
*/
public void write(int c) throws IOException
{
super.write(c);
m_out.write(c);
}
/**
* Write a portion of an array of characters.
*
* @param cbuf Buffer of characters to be written
* @param off Offset from which to start reading characters
* @param len Number of characters to be written
*
* @exception IOException If an I/O error occurs
*/
public void write(char cbuf[], int off, int len) throws IOException
{
super.write(cbuf, off, len);
m_out.write(cbuf, off, len);
}
/**
* Write a portion of a string.
*
* @param str A String
* @param off Offset from which to start writing characters
* @param len Number of characters to write
*
* @exception IOException If an I/O error occurs
*/
public void write(String str, int off, int len) throws IOException
{
super.write(str, off, len);
m_out.write(str, off, len);
}
/**
* Flush the stream.
*
* @exception IOException If an I/O error occurs
*/
public void flush() throws IOException
{
super.flush();
m_out.flush();
}
/**
* Close the stream.
*
* @exception IOException If an I/O error occurs
*/
public void close() throws IOException
{
super.close();
m_out.close();
}
/**
* Main entry point, for testing.
*/
public static void main(String[] args)
{
PrintWriter pw = null;
try
{
FileWriter fileOut = new FileWriter("teeTest.out");
OutputStreamWriter stdOut = new OutputStreamWriter(System.out);
TeeWriter tee = new TeeWriter(fileOut, stdOut);
pw = new PrintWriter(tee);
pw.println(" Jabberwocky");
pw.println(" by ");
pw.println(" Lewis Carroll");
pw.println();
pw.println("Twas brillig, and the slithy toves\n" +
"Did gyre and gimble in the wabe:\n" +
"All mimsy were the borogoves,\n" +
"And the mome raths outgrabe.");
pw.flush();
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
finally
{
pw.close();
}
}
///// Private data //////
private Writer m_out;
} |
|