A Simple HTTP Server in Java
I often find myself needing a very simple HTTP server for things like mocking out external services for testing purposes. In these cases, Tomcat or even Jetty feel like overkill; I just want something that will start and stop really fast and that will allow me to manipulate any arbitrary byte of the response. I usually re-write the same little dedicated app from scratch each time, but I finally broke down this week and coded it up as a reusable component.

Figure 1: Http Server Overview
Figure 1 illustrates the high-level structure of my simple HTTP server. The main class,
HttpServer
,
is responsible for listening on port 80 (or 8080, if you don't have root privileges) and spawning an
instance of SocketHandler
for each client connection. Its start
method,
shown in listing 1, below, enters an infinite loop, spawning a new thread for each accepted connection
and handing the new socket off to a new SocketHandler
and then awaiting a new client
connection. A "real" web server would use a thread pool to avoid running out of threads and memory
and crashing the whole process, but for simple mock/testing purposes, this works perfectly.
public void start() throws IOException {
ServerSocket socket = new ServerSocket(port);
System.out.println("Listening on port " + port);
Socket client;
while ((client = socket.accept()) != null) {
System.out.println("Received connection from " + client.getRemoteSocketAddress().toString());
SocketHandler handler = new SocketHandler(client, handlers);
Thread t = new Thread(handler);
t.start();
}
}
Listing 1: HttpServer.start
SocketHandler
, in turn, is responsible for implementing the bulk of the HTTP protocol
itself. Its goal is to create a Request
object, fill it in with the parsed method,
path, version and request headers, create a Response
object and hand that off to the
associated Handler
to actually fill in and respond to the client. Most of the actual
parsing of the HTTP request is handed off to the Request
object itself. This
doesn't even try to implement the javax.servlet.http.HttpRequest
interface —
my goal here is to keep things simple and flexible. The request parsing is illustrated in listing 2.
public boolean parse() throws IOException {
String initialLine = in.readLine();
log(initialLine);
StringTokenizer tok = new StringTokenizer(initialLine);
String[] components = new String[3];
for (int i = 0; i < components.length; i++) {
if (tok.hasMoreTokens()) {
components[i] = tok.nextToken();
} else {
return false;
}
}
method = components[0];
fullUrl = components[1];
// Consume headers
while (true) {
String headerLine = in.readLine();
log(headerLine);
if (headerLine.length() == 0) {
break;
}
int separator = headerLine.indexOf(":");
if (separator == -1) {
return false;
}
headers.put(headerLine.substring(0, separator),
headerLine.substring(separator + 1));
}
if (components[1].indexOf("?") == -1) {
path = components[1];
} else {
path = components[1].substring(0, components[1].indexOf("?"));
parseQueryParameters(components[1].substring(
components[1].indexOf("?") + 1));
}
if ("/".equals(path)) {
path = "/index.html";
}
return true;
}
Listing 2: Request.parse
Per the HTTP standard, Request
expects a CR-LF delimited list of lines whose first line
is of the form: VERB PATH VERSION
followed by a variable-length list of headers in the
form NAME: VALUE
and a closing empty line indicating that the header list is complete.
If the VERB
supports an entity-body (like POST or PUT), the rest of the request is that
entity body. I'm only worrying about GET
s here, so I assume there's no entity body.
Once this method completes, assuming everything was syntactically correct, Request
's
internal method, path, fullUrl
and headers
member variables are filled in.
Also, since I almost always need to handle query parameters (that is, the stuff passed in after the
'?' in the URL), I go ahead and parse these here as shown in listing 3. This is a departure from
most other HTTP servers which delegate this sort of parsing to higher-level framework code, but for
my purposes, it's very helpful.
private void parseQueryParameters(String queryString) {
for (String parameter : queryString.split("&")) {
int separator = parameter.indexOf('=');
if (separator > -1) {
queryParameters.put(parameter.substring(0, separator),
parameter.substring(separator + 1));
} else {
queryParameters.put(parameter, null);
}
}
}
Listing 3: Request.parseQueryParameters
Once the request has been successfully parsed, the controlling SocketHandler
can go
ahead and look to see if it has a Handler
for the queried path. This suggests that
there must be a handler already in place; the job of HttpServer
s addHandler
,
shown in listing 4, is to associated a method and a path to a handler.
private Map<String, Map<String, Handler>> handlers;
public void addHandler(String method, String path, Handler handler) {
Map<String, Handler> methodHandlers = handlers.get(method);
if (methodHandlers == null) {
methodHandlers = new HashMap<String, Handler>();
handlers.put(method, methodHandlers);
}
methodHandlers.put(path, handler);
}
Listing 4: HttpServer.addHandler
This is simply implemented as a map of maps of strings to handlers - this way, GET /index.html could
be handled by a different handler than DELETE /index.html. So now that SocketHandler
has parsed out the method and the path that the client has requested, it looks for a handler as
shown in listing 5.
public void run() {
BufferedReader in = null;
OutputStream out = null;
try {
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = socket.getOutputStream();
Request request = new Request(in);
if (!request.parse()) {
respond(500, "Unable to parse request", out);
return;
}
boolean foundHandler = false;
Response response = new Response(out);
Map<String, Handler> methodHandlers = handlers.get(request.getMethod());
if (methodHandlers == null) {
respond(405, "Method not supported", out);
return;
}
for (String handlerPath : methodHandlers.keySet()) {
if (handlerPath.equals(request.getPath())) {
methodHandlers.get(request.getPath()).handle(request, response);
response.send();
foundHandler = true;
break;
}
}
...
Listing 5: SocketHandler.run
If the request doesn't parse correctly, the SocketHandler
returns immediately with an
error code 500; otherwise, it checks to see if a handler for the given method and path. If so,
it hands it the fully parsed Request
and newly instantiated Response
; the
Handler
is responsible for responding to the client through the Response
class. However, to simplify things a little bit, I allow a "default" handler to be installed at path
"/*" as show in listing 6. Here, again, "real" web servers allow a lot more flexibility in associating
paths with handlers — a significant source of complexity that I deliberately sidestepped here.
If you need that sort of flexibility, you're probably better off biting the bullet and running Tomcat
or Jetty.
if (!foundHandler) {
if (methodHandlers.get("/*") != null) {
methodHandlers.get("/*").handle(request, response);
response.send();
} else {
respond(404, "Not Found", out);
}
}
Listing 6: SocketHandler.run default handler
Response
in listing 7 is a relatively passive class; it "HTTP-izes" what the Handler
sends to it but is otherwise fairly simple. The HTTP protocol expects the server to respond with
the text HTTP/1.1 STATUS MESSAGE
, a variable-length list of headers in the from
NAME: VALUE
, a blank line, and then the response body. The Handler
should
set a response code,
invoke addHeader
once for each response header it plans to return, add a body and then
invoke send
to complete the response.
public void setResponseCode(int statusCode, String statusMessage) {
this.statusCode = statusCode;
this.statusMessage = statusMessage;
}
public void addHeader(String headerName, String headerValue) {
this.headers.put(headerName, headerValue);
}
public void addBody(String body) {
headers.put("Content-Length", Integer.toString(body.length()));
this.body = body;
}
public void send() throws IOException {
headers.put("Connection", "Close");
out.write(("HTTP/1.1 " + statusCode + " " + statusMessage + "\r\n").getBytes());
for (String headerName : headers.keySet()) {
out.write((headerName + ": " + headers.get(headerName) + "\r\n").getBytes());
}
out.write("\r\n".getBytes());
if (body != null) {
out.write(body.getBytes());
}
}
Listing 7: Response
This completes the HTTP protocol (pretty simple, isn't it?), but what about the handler itself? The interface is simple enough as shown in listing 8.
public interface Handler {
public void handle(Request request, Response response) throws IOException;
}
Listing 8: Handler
Since each subclass of Handler
is only instantiated once, to be associated with the
method and path it handles, each implementation must be thread safe. In practice, that's easy enough
to do by putting all the processing inside the handle
implementation. Note that I don't
have any provision in here for anything like javax.servlet.http.HttpSession
— again,
that's a little more complex than the stub purposes I'm putting this to. Listing 9 illustrates the
most fundamental of all Http Handlers: the file handler which looks for a file match the path name and
returns it.
public class FileHandler implements Handler {
public void handle(Request request, Response response) throws IOException {
try {
FileInputStream file = new FileInputStream(request.getPath().substring(1));
response.setResponseCode(200, "OK");
response.addHeader("Content-Type", "text/html");
StringBuffer buf = new StringBuffer();
// TODO this is slow
int c;
while ((c = file.read()) != -1) {
buf.append((char) c);
}
response.addBody(buf.toString());
} catch (FileNotFoundException e) {
response.setResponseCode(404, "Not Found");
}
}
}
Listing 9: FileHandler
Finally, listing 10 illustrates a sample use of this simple library which returns a custom HTML response for the path "/hello" and responds to every other request by looking for a file (or returning a 404).
HttpServer server = new HttpServer(8080);
server.addHandler("GET", "/hello", new Handler() {
public void handle(Request request, Response response) throws IOException {
String html = "<body>It works, " + request.getParameter("name") + "</body>";
response.setResponseCode(200, "OK");
response.addHeader("Content-Type", "text/html");
response.addBody(html);
}
});
server.addHandler("GET", "/*", new FileHandler()); // Default handler
server.start();
Listing 10: Simple mock HTTP server
Listing 11 includes all of the source code in this post, along with a few extraneous things I omitted in the prior listings above.
import java.util.Map;
import java.util.HashMap;
import java.io.BufferedReader;
import java.io.IOException;
import java.util.StringTokenizer;
public class Request {
private String method;
private String path;
private String fullUrl;
private Map<String, String> headers = new HashMap<String, String>();
private Map<String, String> queryParameters = new HashMap<String, String>();
private BufferedReader in;
public Request(BufferedReader in) {
this.in = in;
}
public String getMethod() {
return method;
}
public String getPath() {
return path;
}
public String getFullUrl() {
return fullUrl;
}
// TODO support mutli-value headers
public String getHeader(String headerName) {
return headers.get(headerName);
}
public String getParameter(String paramName) {
return queryParameters.get(paramName);
}
private void parseQueryParameters(String queryString) {
for (String parameter : queryString.split("&")) {
int separator = parameter.indexOf('=');
if (separator > -1) {
queryParameters.put(parameter.substring(0, separator),
parameter.substring(separator + 1));
} else {
queryParameters.put(parameter, null);
}
}
}
public boolean parse() throws IOException {
String initialLine = in.readLine();
log(initialLine);
StringTokenizer tok = new StringTokenizer(initialLine);
String[] components = new String[3];
for (int i = 0; i < components.length; i++) {
// TODO support HTTP/1.0?
if (tok.hasMoreTokens()) {
components[i] = tok.nextToken();
} else {
return false;
}
}
method = components[0];
fullUrl = components[1];
// Consume headers
while (true) {
String headerLine = in.readLine();
log(headerLine);
if (headerLine.length() == 0) {
break;
}
int separator = headerLine.indexOf(":");
if (separator == -1) {
return false;
}
headers.put(headerLine.substring(0, separator),
headerLine.substring(separator + 1));
}
// TODO should look for host header, Connection: Keep-Alive header,
// Content-Transfer-Encoding: chunked
if (components[1].indexOf("?") == -1) {
path = components[1];
} else {
path = components[1].substring(0, components[1].indexOf("?"));
parseQueryParameters(components[1].substring(
components[1].indexOf("?") + 1));
}
if ("/".equals(path)) {
path = "/index.html";
}
return true;
}
private void log(String msg) {
System.out.println(msg);
}
public String toString() {
return method + " " + path + " " + headers.toString();
}
}
import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;
import java.util.HashMap;
/**
* Encapsulate an HTTP Response. Mostly just wrap an output stream and
* provide some state.
*/
public class Response {
private OutputStream out;
private int statusCode;
private String statusMessage;
private Map<String, String> headers = new HashMap<String, String>();
private String body;
public Response(OutputStream out) {
this.out = out;
}
public void setResponseCode(int statusCode, String statusMessage) {
this.statusCode = statusCode;
this.statusMessage = statusMessage;
}
public void addHeader(String headerName, String headerValue) {
this.headers.put(headerName, headerValue);
}
public void addBody(String body) {
headers.put("Content-Length", Integer.toString(body.length()));
this.body = body;
}
public void send() throws IOException {
headers.put("Connection", "Close");
out.write(("HTTP/1.1 " + statusCode + " " + statusMessage + "\r\n").getBytes());
for (String headerName : headers.keySet()) {
out.write((headerName + ": " + headers.get(headerName) + "\r\n").getBytes());
}
out.write("\r\n".getBytes());
if (body != null) {
out.write(body.getBytes());
}
}
}
import java.io.IOException;
import java.util.Map;
import java.io.BufferedReader;
import java.io.OutputStream;
/**
* Handlers must be thread safe.
*/
public interface Handler {
public void handle(Request request, Response response) throws IOException;
}
import java.io.IOException;
import java.util.Map;
import java.util.HashMap;
import java.net.ServerSocket;
import java.net.Socket;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.BufferedReader;
class SocketHandler implements Runnable {
private Socket socket;
private Handler defaultHandler;
private Map<String, Map<String, Handler>> handlers;
public SocketHandler(Socket socket,
Map<String, Map<String, Handler>> handlers) {
this.socket = socket;
this.handlers = handlers;
}
/**
* Simple responses like errors. Normal reponses come from handlers.
*/
private void respond(int statusCode, String msg, OutputStream out) throws IOException {
String responseLine = "HTTP/1.1 " + statusCode + " " + msg + "\r\n\r\n";
log(responseLine);
out.write(responseLine.getBytes());
}
public void run() {
BufferedReader in = null;
OutputStream out = null;
try {
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = socket.getOutputStream();
Request request = new Request(in);
if (!request.parse()) {
respond(500, "Unable to parse request", out);
return;
}
// TODO most specific handler
boolean foundHandler = false;
Response response = new Response(out);
Map<String, Handler> methodHandlers = handlers.get(request.getMethod());
if (methodHandlers == null) {
respond(405, "Method not supported", out);
return;
}
for (String handlerPath : methodHandlers.keySet()) {
if (handlerPath.equals(request.getPath())) {
methodHandlers.get(request.getPath()).handle(request, response);
response.send();
foundHandler = true;
break;
}
}
if (!foundHandler) {
if (methodHandlers.get("/*") != null) {
methodHandlers.get("/*").handle(request, response);
response.send();
} else {
respond(404, "Not Found", out);
}
}
} catch (IOException e) {
try {
e.printStackTrace();
if (out != null) {
respond(500, e.toString(), out);
}
} catch (IOException e2) {
e2.printStackTrace();
// We tried
}
} finally {
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void log(String msg) {
System.out.println(msg);
}
}
public class HttpServer {
private int port;
private Handler defaultHandler = null;
// Two level map: first level is HTTP Method (GET, POST, OPTION, etc.), second level is the
// request paths.
private Map<String, Map<String, Handler>> handlers = new HashMap<String, Map<String, Handler>>();
// TODO SSL support
public HttpServer(int port) {
this.port = port;
}
/**
* @param path if this is the special string "/*", this is the default handler if
* no other handler matches.
*/
public void addHandler(String method, String path, Handler handler) {
Map<String, Handler> methodHandlers = handlers.get(method);
if (methodHandlers == null) {
methodHandlers = new HashMap<String, Handler>();
handlers.put(method, methodHandlers);
}
methodHandlers.put(path, handler);
}
public void start() throws IOException {
ServerSocket socket = new ServerSocket(port);
System.out.println("Listening on port " + port);
Socket client;
while ((client = socket.accept()) != null) {
System.out.println("Received connection from " + client.getRemoteSocketAddress().toString());
SocketHandler handler = new SocketHandler(client, handlers);
Thread t = new Thread(handler);
t.start();
}
}
public static void main(String[] args) throws IOException {
HttpServer server = new HttpServer(8080);
server.addHandler("GET", "/hello", new Handler() {
public void handle(Request request, Response response) throws IOException {
String html = "It works, " + request.getParameter("name") + "";
response.setResponseCode(200, "OK");
response.addHeader("Content-Type", "text/html");
response.addBody(html);
}
});
server.addHandler("GET", "/*", new FileHandler()); // Default handler
server.start();
}
}
Listing 11: Full (mini) HTTP server
In my next post, I'll extend this to include support for POST requests and chunked response bodies — that is, response bodies whose lengths aren't known or which can't fit in memory.
Add a comment:
Completely off-topic or spam comments will be removed at the discretion of the moderator.
You may preserve formatting (e.g. a code sample) by indenting with four spaces preceding the formatted line(s)