Skip to content

simple but functional implementation of a http-server in vanilla java

License

Notifications You must be signed in to change notification settings

LukasHavemann/vanilla-http-server

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

37 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🍦 vanilla-http-server

Simple but functional implementation of a http-server without a http framework in vanilla java. Why vanilla?

🐋 How to launch?

The vanilla-http-server is available as a docker image on DockerHub and can be started with the following command. After startup the container will start serving files from a sample directory inside the docker image on localhost:8080.

docker run -d -p 8080:8080 lukashavemann/vanilla-http-server:latest

To serve files from the host system, you can use a bind mount. In the following example the vanilla-http-server will serve files from the current working directory.

docker run -d \
    -p 8080:8080 \
    --mount type=bind,source="$(pwd)",target=/basedir \
    lukashavemann/vanilla-http-server:latest

If you want to override the default configuration of the vanilla-http-server container instance, you can pass configuration properties as command line arguments. In the following example the http connection keep-alive-timeout will be increased to 25 seconds. See here for a full documentation of all possible configuration parameters.

docker run -d \
    -p 8080:8080 lukashavemann/vanilla-http-server:latest \
    --vanilla.server.http.keepAliveTimeout=25s

📦 Dependencies

The vanilla-http-server uses spring boot starter for dependency injection and configuration management. The web context of spring boot is disabled. The http protocol implementation was built from scratch. The html testing framework jsoup is used for automated acceptance testing.

🏠 Architecture

The vanilla-http-server uses a simple blocking execution model. The main components and their interaction with each other is shown in the following sequence diagrams.

Client Connection Handling

Architecture overview of the client socket handling part

The ConnectionAcceptorService accepts new tcp connections. After the successful initialization of the client socket, the socket is passed to a ClientSocketDispatcher , which is responsible for handling the client socket. The UnlimitedThreadDispatcher class is the only implementation of the ClientSocketDispatcher interface at the moment. The UnlimitedThreadDispatcher works pretty simple and spawns a new ClientConnectionHandlerThread which is responsible for handling the client socket and managing the http keep-alive feature.

Client HTTP Request Handling

Architecture overview of the client res

The ClientConnectionHandlerThread passes successfully parsed HttpRequest to an instance of ClientRequestProcessor . The SearchServiceRequestProcessor is the only implementation of said interface. The SearchServiceRequestProcessor does handle the eTag evaluation and uses the ContentSearchService instance to find the requested resource. After receiving the search result SearchServiceRequestProcessor generates a Http Response and returns so that ClientRequestProcessor can send the http response. FilesystemContentSearchService is the only implementation of the ContentSearchService interface at the moment.

HTTP Protocol Implementation

Implemenatation of HTTP Protocol Parsing

The ClientConnectionHandlerThread encapsulates the whole http protocol parsing and response generation process. HttpRequestBuffer is a stateful class which determines the end of a http request and calls HttpRequestParser to parse the received http data. HttpRequestParser is a recursive descent style like parser, which is capable of providing context-aware parsing errors. HttpResponseWriter encapsulates the outputStream to the requesting client. HttpResponseWriter does manage the conversion and transportation of data from a inputstream to chunked encoded data to a outputstream.

Summary

In summary the architecture of the vanilla-http-server foresees the following possible extension points:

  • A more intelligent ClientSocketDispatcher could be implemented, which uses a dynamic thread pool which grows and shrinks according to the load but always has a specific number of active client socket handling threads.

  • A different ClientRequestProcessor could be implemented, to build a simple application server for dynamic content generation.

  • A different ContentSearchService could be implemented, to search for resources in a database or s3 service etc.

🧪 Unit Tests & Acceptance Test

Critical classes are tested with JUnit 5, Mockito and AssertJ. All requirements are validated end-to-end with automated acceptance tests. For the acceptance testing the html framework jsoup was used. The capability to stream large files was manually tested.

🔫 Load Testing

To validate that the vanilla-http-server can serve multiple concurrent request, a simple Gatling load test scenario is included in the project . The load test driver can be started with mvn gatling:test. When the load test has completed a html report with the result of the load test can be found under target/gatling/simple*/index.html.

On a Macbook Air M1, 2020, 16GB, macOS big sur 11.2 the following load test result could be achieved. The vanilla-http-server was able to serve 3000 req/sec with a 99th percentile of 13ms. On higher request rates requests started to fail due to many open files. There seams to be no resource leak, and the response times were good, so no further tuning and testing was done.

In the following histogram you can see the distribution of the response-time percentiles. During the ramp-up phase, the vanilla-http-server spawns a lot of new client connection handling threads. The spawning leads to response percentile spikes, which could be prevented by a more intelligent ClientSocketDispatcher .

Distribution of response times

🔨 Development, Build & Pipeline

For local analyzing and debugging of the http protocol implementation Wireshark was used.

The execution of unit and acceptance tests were automated with GitHub Actions. The docker image build is automated with DockerHub. As soon as a new merge to master happens, a new docker image is built by DockerHub cloud and provided with latest tag in the DockerHub registry .

The docker image is built in a multi-stage docker build. The maven build process produces a layered jar . During the build process the layered jar gets unpacked and added as separate layers, to make use of the docker layer deduplication feature to reduce server startup time.

📖 Used Online Resources

During development the following online resources were used.