RADIM URBAN

Implementing a simple HTTP multi-threaded web server in Java

Mar 18, 2022

About

Disclaimer: At the beginning of every file in the GitHub Repo of this project, there is a comment on if the code is mine. At some points I was strongly inspired by others who did this already.
I set myself a strict deadline and decided to only work on this in my free time for a week.
In this post:

Please to run this server, run the main method in application package in the GUI class. You will get a window with button to start the server. To configure go to src/main/resources and configure the port or webroot in configuration.json.

I am listing the following links which I used to learn about everything I needed to know to make this work.

How does an HTTP server work?

A server is a computer. It needs to be connected to the network to be able to receive the requests. It will be listening through ports (for example 80 for not encrypted and 443 for encrypted traffic). Usual requests are files or something in the file system (web root). The server will look for the file(s) that match the request and sends a response back to the browser through the established connection. After the browser receives the response, the server closes the connection. There is therefore a new connection established for every single request coming from the browser. We can now sum up what we need to do.

What needs to be implemented

Server must be able to:

  1. listen to a configurable port and therefore to read the configuration files (JSON) and how to write them
  2. establish and handle multiple connection(s) (multi-threaded) between the browser and the server
  3. parse and understand requests messages coming from browser
  4. compose the response based on the request

Implementing and handling the configurations

For keeping the configurations in one place, we will use JSON.
We need to configure:

  1. port (int): so far one, we can configure more later, our server will listen to this port
  2. webroot (String): pathway of where the files are saved

We will now write two classes. One class for handling the configurations and one for its representation (a simple setter and getter of the port and webroot). Both will be in the package of the httpServer.
The actual configuration class is trivial and will look like this:

This where we retrieve the configuration data from. It will basically be an object containing the configuration data.
Now we need to write the configuration handler. Our handler needs to be able to (those will be separate functions):

  1. load the Configuration
  2. get the Configuration (and return it obviously)

Now we need to figure out a way how to parse the JSON file and create the Configuration object. This is a pretty complicated step (at least if you're doing this for the first time).

Parsing JSON in Java

The entire project will use Maven for easily keeping dependencies in order and for the actual parsing of the JSON file the Jackson library which can be used for parsing and generating JSON files. It has an Object Mapper class that can process JSON files and create Java objects out of them, which is exactly what we need.

Defining the dependencies

The pom.xml file looks like this:

Java class handling the JSON files

In this class, that we will make use of the ObjectMapper() which we will import from the jackson.databind. There are two ways to get/use the ObjectMapper. You can either just use the default one or configure your own. If you dont want to configure it at all, you can just use public static ObjectMapper objectMapper = new ObjectMapper();. Otherwise create the function that returns the ObjectMapper and configure it within the function before you return it. Another part of the Jackson library is the JsonNode. This class represents the structure of the original JSON file. We will first want to transform/map the JSON into this class (object). We define the function getJsonNode() to get this JsonNode representation. It will take in one argument - the string representation of the JSON file.

Using this if we for example take our JSON configuration file to test the output of this getJsonNode() function. That is, for the following JSON:

We have string representation "{"port\":8080, "webroot":"web"}" and the ouput of the function getJsonNode() is an JsonNode object on which we can for example call:

At this point we need to move the JsonNode object into the Configuration object/class. For that we need to define another function in the JsonHandler class.
We can do this using the ObjectMapper and its treeToValue(JsonNode, Class <T>). This function returns the class we want the JsonNode to transform into (we pass this as a second argument). At the same time we can define a function that is the inverse to JsonToClass. This function will accept an object as an argument ans will return the JsonNode object. Another thing that can be useful is to get a way to get the Configuration as a string, which is basically a JSON.

Now we have everything we need to process and to work with the JSON files.

Configuration Handler

We can now proceed to implement the methods of the ConfigurationHandler class.

This should be everything we need to be able to work with configurations. Now we just need to import it into the main HttpServer class and instantiate and we can use it.

Assuming the previous JSON Configuration this config would return 8080 on congif.getPort() and "path" on config.getWebrot().

Establishing a connection and using (server) sockets

To establish the connection, we need to have a socket that is going to listen to the port. Java has a net library which provides classes for networking applications. One of them is the Server Socket. A server socket waits for requests to come in over the network. It performs some operation based on that request, and then possibly returns a result to the requester. To create a server socket bounded to a specific port, we construct the object and pass the port number as a parameter. We can easily retrieve this from our Configuration object.

Now we can say we want the socket to wait for the connection to be established. The function accept() listens for a connection to be made to this socket and accepts it. The method blocks until a connection is made. Once a connection is made, Socket is returned. A socket is an endpoint for communication between two machines.
Socket has two methods we will want to use:

Now that we have basically all we need for single-threaded web server - let's make it work in parallel such that multiple connections and requests can handled independently.

Multi-threaded accepting of connections and handling multiple requests

Let's now create a different package tools where we will handle connections in multi-threaded scenario. I will create two classes (both extending thread (and therefore implementing Runnable).

Building multiple connections (Acceptor)

Acceptor will work as follows. The constructor will accept the port and the web-root. As already explained we overrun the method run(), which is basically a main method for this thread started by start(). This method will wait for a connection and once it is made it will start a new thread Executor. There might be some IOE, so we need to wrap it into try/catch code.

Let's build the executor now.

Handling the requests and building the Executor

Based on what we get through the inputStream we are able understand what the browser requests and form an appropriate response. As mentioned earlier, we will assume that the request is an path to a certain file.
An HTTP Request consists of request line, headers and message body. But I will only have enough time to implement basic requuest line requests (not taking headers and so on into consideration). A request line is the first line, it consist of three attributes:

  1. Method (for example GET, POST, ...
  2. URL
  3. an HTTP version

We now override the run() method as we extend the thread class. We can parse the InputStream and read all three properties of a request line.

A typical browser request looks similar to the following example:

Out of that we will only take the first line. That is the desired request line:

Now we have that (mind method="GET", url="/", httpVersion="HTTP/1.1").

Responses

Response in general consists of statusLine, headers, and the message body(= web). These parameters must separated by a SEPARATOR which is by convention \r\n. That will be true for all reponses in general.
We will do the absolute minimum and consider GET and HEADER methods. Let's therefore check if the request uses one of these and if not we will return a 501 Not Implemented error. As soon as I identify an error I will compose an error response.

501 Not Implemented

404 File Not Found

Let's now check if we have the file the request is asking for. For that let's create a new method loadFile which will parse the file into String or return "404" if such file is not in our webroot.

Now let's check the URL as well as the httpVersion in the run() method.

505 HTTP Version Not Supported

Response with no errors

If nothing failed, we obviously send the correct one.

Graphical UI

Last thing (and probably least) I will be able to do is the graphical interface to start the server. I will just create a class GUI implementing the ActionListener and create a main method.
Since this a trivial code, I will just paste the code and the result.

This is all I can manage to do till my deadline. The actual server is working and can be tested on localhost as described at the very beginning.

Window before starting the server

Window after starting the server

Potential To-Do's