Dracon
A small, fast HTTP/HTTPS server for Linux.
Dracon is written from scratch in C++23 on top of epoll and
Boost.Coroutine2 — no Boost.Asio, no framework. The server itself only speaks
HTTP: every request is answered by a plugin, a shared library it
loads at start up.
Each connection runs inside its own coroutine, so a handler is written as plain sequential code while every read and write that would block yields back to the event loop. There is one event loop per hardware thread and the connections are spread across them, which is what lets a handful of threads carry a very large number of concurrent ones.
A plugin, in full
#include <dracon/http.h>
#include <dracon/plugin.h>
PLUGIN_EXPORT uint32_t plugin_order() { return 0; }
PLUGIN_EXPORT Dracon::HttpSession create_session(const Dracon::Request &req)
{
if (req.url() != "/hello")
return {}; // not ours, the server asks the next plugin
return [](Dracon::AbstractStream &stream, Dracon::Request &req) {
stream >> req; // read the rest of the request
stream << Dracon::Response{200, "hello\n"};
};
}
Drop the resulting .so into the plugins directory and it is live on
the next start. The whole API is the header-only include/dracon/:
requests and responses, streams and chunked output, RESTful routing, and worker
threads for the slow work that must not block an event loop.
…and one with routes
For an API, restful.h does the URL matching. The handler is
the session, and the resources a route captures arrive as ordinary arguments in
the order it declares them — so one function can serve a whole family of
routes, and the shorter ones simply hand over empty strings.
#include <dracon/plugin.h>
#include <dracon/restful.h>
Dracon::RESTfulRouterType router{"/api/v1/"};
static void customers(Dracon::AbstractStream &stream, Dracon::Request &req,
const std::string &customerId = {},
const std::string &licenseId = {})
{
stream >> req;
stream << Dracon::Response{200, "customer " + customerId +
", license " + licenseId + "\n"};
}
PLUGIN_EXPORT bool init_plugin(const std::string &/*confDir*/)
{
for (const auto route : {"customers",
"customers/{customerId}",
"customers/{customerId}/licenses/{licenseId}"})
router.createRoute(route)->addMethodHandler("GET", customers);
return true;
}
PLUGIN_EXPORT uint32_t plugin_order() { return 0; }
PLUGIN_EXPORT Dracon::HttpSession create_session(const Dracon::Request &req)
{
return router.createHandler(req.url(), req.method());
}
Which answers all three, filling in what each one captured:
- GET /api/v1/customers
- customer , license
- GET /api/v1/customers/42
- customer 42, license
- GET /api/v1/customers/42/licenses/7
- customer 42, license 7
An unknown method on a known route is answered with a 405 and the right
Allow header, without the handler being involved. Take a
Dracon::ParsedRoute instead of the captures when the query strings or
the allowed methods are wanted too.
At a glance
- Language
- C++23 — GCC ≥ 13 or Clang ≥ 17
- Built on
epoll, Boost.Coroutine2, OpenSSL- Platform
- Linux only, and unapologetically so
- Handlers
- Plugins loaded with
dlopenat start up - Concurrency
- One coroutine per connection, one event loop per thread
- License
- AGPL, with an exception so plugins may use any license
Installing
Debian packages build straight out of the tree: dracon-server for the
daemon, its systemd unit and the default configuration,
dracon-mod-staticcontent for the plugin serving this page, and
libdracon-dev for the headers you build your own against. A Certbot
plugin, python3-certbot-dracon, obtains and installs the certificate:
certbot run --configurator dracon -d example.com