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> |
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> |
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> |
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()); | |
} | |
} | |
} |
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”