Simple but functional implementation of a http-server without a http framework in vanilla java. Why vanilla?
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
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.
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.
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.
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.
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.
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.
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.
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
.
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.
During development the following online resources were used.
- maven github actions setup
- jetbrains gitignore
- maven gitignore
- docker github actions setup
- switch to multi stage build
- switch to layered jar
- RFC 2616
- RFC 7232
- Mozilla http header documentation
- and multiple stackoverflow answers to some minor implementation details