GitHub - anmonteiro/ocaml-h2: An HTTP/2 implementation written in pure OCaml (original) (raw)

h2

h2 is an implementation of theHTTP/2 specification entirely in OCaml. It is based on the concepts inhttp/af, and therefore uses theAngstrom and Faraday libraries to implement the parsing and serialization layers of the HTTP/2 standard. It also preserves the same API as http/af wherever possible.

Installation

Install the library and its dependencies via OPAM:

Usage

Resources

First of all, the generated documentation liveshere. It is recommended to browse it and get to know the API exposed by H2.

There are also some examples in the examples folder. Most notably, the ALPN example provides an implementation of a common real-world use case:

It sets up a server that listens on 2 ports:

  1. port 8080: redirects all incoming traffic to https://localhost:9443
  2. port 9443: negotiates which protocol to use over the TLSApplication-Layer Protocol Negotiation (ALPN) extension. It supports 2 protocols (in order of preference): h2 and http/1.1.
    If h2 is negotiated, the example sets up a connection handler usingh2-lwt-unix. Otherwise the connection handler will serve HTTP/1.1 traffic using httpun.

The ALPN example also provides a unikernel implementation with the same functionality that runs on MirageOS.

A server example

We present an annotated example below that responds to any GET request and returns a response body containing the target of the request.

open H2

(* This is our request handler. H2 will invoke this function whenever the

(* This is our error handler. Everytime H2 sees a malformed request or an

let () = (* We're going to be using the H2_lwt_unix module from the h2-lwt-unix

A client example

The following annotated client example performs a GET request to example.comand prints the response body as it arrives.

open H2 module Client = H2_lwt_unix.Client

(* This is our response handler. H2 will invoke this function whenever the

let error_handler _error = (* There was an error handling the request. In this simple example, we don't

open Lwt.Infix

let () = let host = "www.example.com" in Lwt_main.run ( (* We start by resolving the address of the host we want to connect to. ) Lwt_unix.getaddrinfo host "443" [ Unix.(AI_FAMILY PF_INET) ] >>= fun addresses -> ( Once the address for the host we want to contact has been resolved, we * need to create the socket through which the communication with the * remote host is going to happen. ) let socket = Lwt_unix.socket Unix.PF_INET Unix.SOCK_STREAM 0 in ( Then, we connect to the socket we just created, on the address we have * previously obtained through name resolution. *) Lwt_unix.connect socket (List.hd addresses).Unix.ai_addr >>= fun () -> let request = Request.create GET "/" (* a scheme pseudo-header is required in HTTP/2 requests, otherwise * the request will be considered malformed. In our case, we're * making a request over HTTPS, so we specify "https" *) ~scheme:"https" ~headers: (* The :authoritypseudo-header is a blurry line in the HTTP/2 * specificiation. It's not strictly required but most * implementations treat a request with a missing:authority * pseudo-header as malformed. That is the case for example.com, so * we include it. *) Headers.(add_list empty [ ":authority", host ]) in (* The H2 API relies on callbacks to allow for a single, stable core to * be used with different I/O runtimes. Because we're using Lwt in this * example, we'll create an Lwt task that is going to help us transform * the callback-calling style of H2 into an Lwt promise whenever we're * done handling the response. * * If you're not familiar with Lwt or itsLwt.waitfunction, it's * recommended you read at least the following bit before moving on: * http://ocsigen.org/lwt/4.1.0/api/Lwt#VALwait. *) let response_received, notify_response_received = Lwt.wait () in (* Partially apply theresponse_handlerfunction that we defined above * to produce one that matches H2's expected signature. After this line, *response_handlernow has the following signature: * * val response_handler: Response.t -> [read ] Body.t -> unit ) let response_handler = response_handler notify_response_received in ( HTTP/2 itself does not define that the protocol must be used with TLS. * In practice, though, TLS is widely used in the Internet today (and * that's a good thing!) and no serious deployments use plaintext HTTP/2. * The following is a good read on why this is the case: * https://http2-explained.haxx.se/content/en/part8.html#844-its-use-of-tls-makes-it-slower * * For us, this means that we need to make our request over TLS. H2, and * more specifically h2-lwt-unix, provide a TLS module for both the * client and the server implementations that rely on an optional * dependency to ocaml-tls. * * We start by creating a connection handler. The create_connection * function takes two arguments: a connection-level error handler (you * can read more about the difference between connection-level and * stream-level in H2 and HTTP/2 in general here: * https://anmonteiro.com/ocaml-h2/h2/H2/Client_connection/index.html#val-create) * and the file descriptor that we created above. ) Client.TLS.create_connection_with_default ~error_handler socket >>= fun connection -> ( Once the connection has been created, we can initiate our request. For * that, we call the request function, which will send the request that * we created to the server, and direct its response to either the * response handler - in case of a successful request / response exchange * - or the (stream-level) error handler, in case our request was * malformed. ) let request_body = Client.TLS.request connection request ~error_handler ~response_handler in ( The request function returns a request body that we can write to, * but in our case just the headers are sufficient. We close the request * body immediately to signal to the underlying HTTP/2 framing layer that * we're done sending our request. ) Body.Writer.close request_body; ( Our call to Lwt_main.run above will wait until this promise is * filled before exiting the program. *) response_received )

Conformance

One of h2's goals is to be 100% compliant with the HTTP/2 specification. There are currently 3 mechanisms in place to verify such conformance:

  1. Unit tests using the HPACK stories in thehttp2jp/hpack-test-caserepository
  2. Unit tests using the test cases provided by thehttp2jp/http2-frame-test-caserepository.
  3. Automated test runs (in CI) using theh2spec conformance testing tool for HTTP/2 implementations.
    • These test all the Reqd.respond_with_* functions for conformance against the specification.

Performance

h2 aims to be a high-performance, memory-efficient, scalable, and easily portable (with respect to different I/O runtimes) implementation. To achieve that, it takes advantage of the unbuffered parsing interface in Angstrom using off-heap buffers wherever possible, for both parsing and serialization.

Below is a plot of H2's latency profile at a sustained rate of 17000 requests per second over 30 seconds, benchmarked using thevegeta load testing tool.

ocaml-h2

Development

This source distribution provides a number of packages and examples. The directory structure is as follows:

Cloning the repository

Use --recurse-submodules to get the test git submodules

$ git clone git@github.com:anmonteiro/ocaml-h2.git --recurse-submodules

Using OPAM

To install development dependencies, pin the package from the root of the repository:

$ opam pin add -n hpack . $ opam pin add -n h2 . $ opam install --deps-only h2

After this, you may install a development version of the library using the install command as usual.

Tests can be run via dune:

License

h2 is distributed under the 3-Clause BSD License, see LICENSE.

This source distribution includes work based onhttp/af. http/af's license file is included in httpaf.LICENSE