HTTP server from scratch

Almost everything available on the Internet is served by web server. There are plenty of web server implementations like Apache, Nginx, Express, etc. But ever wondered how to implement a web server from scratch without any external dependencies? I wondered about that and this post is the result of that little experiment using Java.

Goals

Before listing the goals, this post assumes that you have some knowledge of HTTP. Mozilla has a very good overview here. Now the goals

  • Java version 8+
  • No external dependencies
  • Extremely rudimentary simple Web server
  • Servers only one static file (index.html) for path / or returns 404
  • Supports only GET for other methods returns 501

Implementation Overview

Upon launching the server starts listening to port 8080. Whenever a new request hits the server, a new thread is spawned and further processing of the request is the responsibility of this thread. In the run method of the thread, we do some basic checking to see if we can handle this request or not. If the request is method is other than GET, we return 501 or if the request is other than “/” we return 404 otherwise we return 200. In each of these three cases we return a static file.

Static file: index.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Home page</title>
</head>
<body>
<h1>Simple Java HTTP server</h1>
<p>This is the home page.</p>
</body>
</html>
view raw index.html hosted with ❤ by GitHub

Static file: 404.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Not Found</title>
</head>
<body>
<h1>Resource not found on this server.</h1>
</body>
</html>
view raw 404.html hosted with ❤ by GitHub

Static file: 501.html

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Not Implemented</title>
</head>
<body>
<h1>Not Implemented</h1>
</body>
</html>
view raw 501.html hosted with ❤ by GitHub

Parsing the header

Along with each HTTP request the client, in most cases a Browser, will send in some headers. The first header gives the details about the HTTP method, the requested resource and finally the protocol version. A typical request will look like

GET /index.html HTTP/1.1

Clients will also send other headers most often Accept: which tells what MIME types the client can handle. CLI clients like curl will just send */*, which means accepts everything. For purposes of this project we only care about the first header and ignore the rest of the headers.

Show me the code

package blog.devrandom.http;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.StringTokenizer;
public class HttpServer implements Runnable {
private static final File WEB_ROOT = new File(".");
private static final File INDEX_HTML = new File(WEB_ROOT, "index.html");
private static final File NOT_FOUND = new File(WEB_ROOT, "404.html");
private static final File NOT_IMPLEMENTED = new File(WEB_ROOT, "501.html");
private static final DateTimeFormatter HTTP_FORMATTER = DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss z");
private Socket request;
private HttpServer(Socket request) {
this.request = request;
}
@Override
public void run() {
try (BufferedReader in = new BufferedReader(new InputStreamReader(request.getInputStream()));
PrintWriter out = new PrintWriter(request.getOutputStream());
BufferedOutputStream dataOut = new BufferedOutputStream(request.getOutputStream())) {
// read the first header.
String header = in.readLine();
StringTokenizer tokenizer = new StringTokenizer(header);
String method = tokenizer.nextToken().toUpperCase();
String resource = tokenizer.nextToken().toLowerCase();
String protocol = tokenizer.nextToken();
String status;
File file;
if (method.equals("GET")) {
if (resource.endsWith("/")) {
file = INDEX_HTML;
status = " 200 OK";
} else {
file = NOT_FOUND;
status = " 404 Not Found";
}
} else {
file = NOT_IMPLEMENTED;
status = " 501 Not Implemented";
}
ZonedDateTime now = ZonedDateTime.now(ZoneId.of("GMT"));
String date = now.format(HTTP_FORMATTER);
System.out.printf("%s %s%s %s\n", method, resource, status, date);
byte[] data = readFile(file);
// write the headers
out.println(protocol + status);
out.println("Server: HttpServer v1.0");
out.println("Date: " + date);
out.println("Content-Type: text/html; charset=utf-8");
out.println("Content-Length: " + data.length);
out.println();
out.flush();
// write the file contents
dataOut.write(data, 0, data.length);
dataOut.flush();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
private byte[] readFile(File file) throws IOException {
byte[] res;
try (FileInputStream fis = new FileInputStream(file)) {
int length = (int) file.length();
res = new byte[length];
fis.read(res, 0, length);
}
return res;
}
public static void main(String[] args) {
try (ServerSocket serverSocket = new ServerSocket(8080)) {
System.out.println("HttpServer started and listening to port 8080");
// infinite loop
while (true) {
HttpServer server = new HttpServer(serverSocket.accept());
Thread thread = new Thread(server);
thread.start();
}
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}
view raw HttpServer.java hosted with ❤ by GitHub

Response Headers

Similar to the request, the server response also has headers before the content. Of all the headers that the server can set, the Status, Date, Content-Type, and Content-Length are required. The Date has to be always in GMT.

Conclusion

This implementation takes a lot of things for granted, but it is a good place to start. In future posts lets refactor this into more modular server. All of the code can be found on Github at https://github.com/cx0der/http-server.

One thought on “HTTP server from scratch

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s