Skip to content

a lightweight, ultra-performant, zero dependency JDK http server implementation designed for embedding and optimized for virtual threads

License

Notifications You must be signed in to change notification settings

robaho/httpserver

Repository files navigation

httpserver

Zero-dependency implementation of the JDK com.sun.net.httpserver.HttpServer specification with a few significant enhancements.

  • WebSocket support using modified source code from nanohttpd.
  • Server-side proxy support using ProxyHandler. (Tunneling proxies are also supported using CONNECT for https.)
  • HTTP/2 RFC 9113 support
  • Performance enhancements such as proper HTTP pipelining, optimized String parsing, etc.

All async functionality has been removed. All synchronized blocks were removed in favor of other Java concurrency concepts.

The end result is an implementation that easily integrates with Virtual Threads available in JDK 21 - simply set a virtual thread based ExecutorService.

Improves performance by more than 10x over the JDK implementation.

Designed for embedding with only a 200kb jar and zero dependencies.

background

The built-in JDK httpserver implementation has no support for connection upgrades, so it is not possible to add websocket support.

Additionally, the code still has a lot of async - e.g. using SSLEngine to provide SSL support - which makes it more difficult to understand and enhance.

The thread-per-connection synchronous design simplifies the code substantially.

testing/compliance

Nearly all tests from the JDK are included, so this version should be highly compliant and reliable.

Additional proxy and websockets tests are included.

The http2 implementation passes all specification tests in h2spec

maven

Maven Central

<dependency>
  <groupId>io.github.robaho</groupId>
  <artifactId>httpserver</artifactId>
  <version>use version from badge above without leading v</version>
</dependency>

using

The JDK will automatically use robaho.net.httpserver.DefaultHttpServerProvider instead of the JDK implementation when the jar is placed on the class/module path. If there are multiple HttpServer providers on the classpath, the com.sun.net.httpserver.HttpServerProvider system property can be used to specify the correct one:

Eg. -Dcom.sun.net.httpserver.HttpServerProvider=robaho.net.httpserver.DefaultHttpServerProvider

Alternatively, you can instantiate the server directly using this.

Example Usage

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

public class Test {

  public static void main(String[] args) throws Exception {
    HttpServer server = HttpServer.create(new InetSocketAddress(8000), 0);
    server.createContext("/", new MyHandler());
    server.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); // sets virtual thread executor
    server.start();
    }

  static class MyHandler implements HttpHandler {
    @Override
    public void handle(HttpExchange exchange) throws IOException {
      String response = "This is the response";
      byte[] bytes = response.getBytes();

      // -1 means no content, 0 means unknown content length
      var contentLength = bytes.length == 0 ? -1 : bytes.length;

      try (OutputStream os = exchange.getResponseBody()) {
        exchange.sendResponseHeaders(200, contentLength);
        os.write(bytes);
      }
    }
  }
}

There is a simple file server that can be used to for basic testing. It has download, echo, and "hello" capabilities. Use

gradle runSimpleFileServer

logging

All logging is performed using the Java System Logger

enable Http2

Http2 support is enabled via Java system properties.

Use -Drobaho.net.httpserver.http2OverSSL=true to enable Http2 only via SSL connections.

Use -Drobaho.net.httpserver.http2OverNonSSL=true to enable Http2 on Non-SSL connections (which requires prior knowledge). The Http2 upgrade mechanism was deprecated in RFC 9113 so it is not supported.

See the additional Http2 options in ServerConfig.java

performance

This version performs more than 10x faster than the JDK version when tested using the Tech Empower Benchmarks on an identical hardware/work setup with the same JDK 23 version.1

The frameworks were also tested using go-wrk2

1The robaho version has been submitted to the Tech Empower benchmarks project for 3-party confirmation.
2go-wrk does not use http pipelining so, the large number of connections is the limiting factor.

Performance tests against the latest Jetty version were run. The robaho httpserver outperformed the Jetty http2 by 3x, and http1 by 5x.

The Javalin/Jetty project is available here

vs JDK performance details

robaho tech empower

robertengels@macmini go-wrk % wrk -H 'Host: imac' -H 'Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7' -H 'Connection: keep-alive' --latency -d 60 -c 64 --timeout 8 -t 2 http://imac:8080/plaintext -s ~/pipeline.lua -- 16
Running 1m test @ http://imac:8080/plaintext
  2 threads and 64 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     1.20ms    9.22ms 404.09ms   85.37%
    Req/Sec   348.78k    33.28k  415.03k    71.46%
  Latency Distribution
     50%    0.98ms
     75%    1.43ms
     90%    0.00us
     99%    0.00us
  41709198 requests in 1.00m, 5.52GB read
Requests/sec: 693983.49
Transfer/sec:     93.98MB

jdk 23 tech empower

robertengels@macmini go-wrk % wrk -H 'Host: imac' -H 'Accept: text/plain,text/html;q=0.9,application/xhtml+xml;q=0.9,application/xml;q=0.8,*/*;q=0.7' -H 'Connection: keep-alive' --latency -d 60 -c 64 --timeout 8 -t 2 http://imac:8080/plaintext -s ~/pipeline.lua -- 16
Running 1m test @ http://imac:8080/plaintext
  2 threads and 64 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency     2.91ms   12.01ms 405.70ms   63.71%
    Req/Sec   114.30k    18.07k  146.91k    87.10%
  Latency Distribution
     50%    4.06ms
     75%    0.00us
     90%    0.00us
     99%    0.00us
  13669748 requests in 1.00m, 1.72GB read
Requests/sec: 227446.87
Transfer/sec:     29.28MB

robaho go-wrk

robertengels@macmini go-wrk % ./go-wrk -c=1024 -d=30 -T=100000 http://imac:8080/plaintext
Running 30s test @ http://imac:8080/plaintext
  1024 goroutine(s) running concurrently
3252278 requests in 30.118280233s, 387.70MB read
Requests/sec:		107983.52
Transfer/sec:		12.87MB
Overall Requests/sec:	105891.53
Overall Transfer/sec:	12.62MB
Fastest Request:	83µs
Avg Req Time:		9.482ms
Slowest Request:	1.415359s
Number of Errors:	0
10%:			286µs
50%:			1.018ms
75%:			1.272ms
99%:			1.436ms
99.9%:			1.441ms
99.9999%:		1.442ms
99.99999%:		1.442ms
stddev:			35.998ms

jdk 23 go-wrk

robertengels@macmini go-wrk % ./go-wrk -c=1024 -d=30 -T=100000 http://imac:8080/plaintext
Running 30s test @ http://imac:8080/plaintext
  1024 goroutine(s) running concurrently
264198 requests in 30.047154195s, 29.73MB read
Requests/sec:		8792.78
Transfer/sec:		1013.23KB
Overall Requests/sec:	8595.99
Overall Transfer/sec:	990.55KB
Fastest Request:	408µs
Avg Req Time:		116.459ms
Slowest Request:	1.930495s
Number of Errors:	0
10%:			1.166ms
50%:			1.595ms
75%:			1.725ms
99%:			1.827ms
99.9%:			1.83ms
99.9999%:		1.83ms
99.99999%:		1.83ms
stddev:			174.373ms

vs Jetty performance details

The server is an iMac 4ghz quad-core i7 running OSX 13.7.2. JVM used is JDK 23.0.1. The h2load client was connected via a 20Gbs lightening network from an M1 Mac Mini.

Using h2load -n 1000000 -m 1000 -c 16 [--h1] http://imac:<port>

Jetty jetty-11.0.24 Javalin version 6.4.0

Jetty 11 http2

starting benchmark...
spawning thread #0: 16 total client(s). 1000000 total requests
Application protocol: h2c
finished in 3.47s, 288284.80 req/s, 10.17MB/s
requests: 1000000 total, 1000000 started, 1000000 done, 1000000 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 1000000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 35.29MB (37002689) total, 7.63MB (8001809) headers (space savings 90.12%), 10.49MB (11000000) data
                     min         max         mean         sd        +/- sd
time for request:       94us    381.85ms      6.42ms     21.51ms    96.90%
time for connect:      389us      5.88ms      3.15ms      1.75ms    62.50%
time to 1st byte:     6.61ms     11.74ms      7.85ms      1.24ms    87.50%
req/s           :   18020.94    23235.01    19829.09     1588.94    75.00%

Jetty 11 http1

starting benchmark...
spawning thread #0: 16 total client(s). 1000000 total requests
Application protocol: http/1.1
finished in 3.63s, 275680.69 req/s, 36.02MB/s
requests: 1000000 total, 1000000 started, 1000000 done, 1000000 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 1000021 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 130.65MB (137000000) total, 86.78MB (91000000) headers (space savings 0.00%), 10.49MB (11000000) data
                     min         max         mean         sd        +/- sd
time for request:     1.59ms    336.00ms     53.17ms     51.56ms    85.36%
time for connect:      422us      2.57ms      1.54ms       632us    62.50%
time to 1st byte:     2.98ms    314.97ms     26.14ms     77.12ms    93.75%
req/s           :   17232.15    21230.14    18780.35     1130.32    68.75

robaho http2

starting benchmark...
spawning thread #0: 16 total client(s). 1000000 total requests
Application protocol: h2c
finished in 1.03s, 966710.36 req/s, 40.57MB/s
requests: 1000000 total, 1000000 started, 1000000 done, 1000000 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 1000000 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 41.96MB (44000480) total, 5.72MB (6000000) headers (space savings 76.92%), 10.49MB (11000000) data
                     min         max         mean         sd        +/- sd
time for request:      457us     71.41ms     14.71ms      8.63ms    73.09%
time for connect:      336us      5.77ms      3.13ms      1.73ms    62.50%
time to 1st byte:     6.59ms     15.30ms     10.40ms      3.32ms    50.00%
req/s           :   60461.71    66800.04    62509.79     1544.65    75.00%

robaho http1

starting benchmark...
spawning thread #0: 16 total client(s). 1000000 total requests
Application protocol: http/1.1
finished in 776.64ms, 1287592.88 req/s, 106.83MB/s
requests: 1000000 total, 1000000 started, 1000000 done, 1000000 succeeded, 0 failed, 0 errored, 0 timeout
status codes: 1000123 2xx, 0 3xx, 0 4xx, 0 5xx
traffic: 82.97MB (87000000) total, 46.73MB (49000000) headers (space savings 0.00%), 10.49MB (11000000) data
                     min         max         mean         sd        +/- sd
time for request:      376us    380.30ms      9.12ms     32.43ms    99.20%
time for connect:      240us      2.51ms      1.50ms       720us    62.50%
time to 1st byte:     3.04ms     18.85ms      8.93ms      5.77ms    68.75%
req/s           :   80530.13   167605.46   122588.82    42385.59    87.50%

server statistics

The server tracks some basic statistics. To enable the access endpoint /__stats, set the system property robaho.net.httpserver.EnableStatistics=true.

Sample usage:

$ curl http://localhost:8080/__stats
Connections: 4264
Active Connections: 2049
Requests: 2669256
Requests/sec: 73719
Handler Exceptions: 0
Socket Exceptions: 0
Mac Connections Exceeded: 0
Idle Closes: 0
Reply Errors: 0

The counts and rates for non "Total" statistics are reset with each pull of the statistics.

performance notes

Http2 performance has not been fully optimized. The http2 version is about 20-30% slower than http1. I expect this to be the case with most http2 implementations due to the complexity. http2 outperforms http1 when sending multiple simultaneous requests from the client with payloads, as most servers and clients do not implement http pipelining when payloads are involved.

TODO: sending hpack headers does not use huffman encoding or dynamic table management. see the following paper https://www.mew.org/~kazu/doc/paper/hpack-2017.pdf for optimizing the implementation further.

The most expensive operations involve converting strings to URI instances. Unfortunately, since using URI is part of the HttpExchange API little can be done in this regard. It could be instantiated lazily, but almost all handlers need access to the URI components (e.g. path, query, etc.)

The standard JDK Headers implementation normalizes all headers to be first character capitalized and the rest lowercase. To ensure optimum performance, client code should use the same format to avoid the normalization cost, i.e.

Use

var value = request.getFirst("Content-length");

instead of

var value = request.getFirst("content-length"); 
var value = request.getFirst("CONTENT-LENGTH");

About

a lightweight, ultra-performant, zero dependency JDK http server implementation designed for embedding and optimized for virtual threads

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 3

  •  
  •  
  •