HTTP server library and a mandelbrot apllication using it.

This commit is contained in:
Bart Beumer 2026-01-20 21:27:02 +00:00
parent 1998ea770d
commit b8bea1e27e
21 changed files with 1306 additions and 2 deletions

View File

@ -7,3 +7,4 @@ add_subdirectory(applications)
add_subdirectory(bmrshared) add_subdirectory(bmrshared)
add_subdirectory(bmrshared-freetype) add_subdirectory(bmrshared-freetype)
add_subdirectory(bmrshared-magic) add_subdirectory(bmrshared-magic)
add_subdirectory(bmrshared-web)

View File

@ -1 +1,2 @@
add_subdirectory(http-mandelbrot)
add_subdirectory(text2image) add_subdirectory(text2image)

View File

@ -0,0 +1,28 @@
find_package(Boost 1.90.0 REQUIRED COMPONENTS headers CONFIG)
find_package(JPEG REQUIRED)
project(http-mandelbrot)
add_executable(
${PROJECT_NAME}
./src/main.cpp
./src/mandelbrot.cpp
)
set_property(
TARGET ${PROJECT_NAME}
PROPERTY CXX_STANDARD 23
)
target_link_libraries(
${PROJECT_NAME}
PRIVATE
bmrshared-web
Boost::headers
JPEG::JPEG
)
install(
TARGETS ${PROJECT_NAME}
RUNTIME DESTINATION bin
)

View File

@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base target="_top">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Quick Start - Leaflet</title>
<link rel="shortcut icon" type="image/x-icon" href="docs/images/favicon.ico" />
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<style>
html, body {
height: 100%;
margin: 0;
}
.leaflet-container {
height: 400px;
width: 600px;
max-width: 100%;
max-height: 100%;
}
</style>
</head>
<body>
<div id="map" style="width: 100%; height: 100%;">
</div>
<script>
const map = L.map('map', {crs: L.CRS.Simple, zoomSnap: 0}).setView([0.0, 0.0], 1);
const tiles = L.tileLayer('/render_z{z}x{x}y{y}w256h256ar2.0ai0.0maxiter100.jpg', {
maxZoom: 1000
}).addTo(map);
</script>
</body>
</html>

View File

@ -0,0 +1,191 @@
// GNU Lesser General Public License v3.0
// Copyright (c) 2026 Bart Beumer <bart@4beumer.nl>
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License v3.0 as published by
// the Free Software Foundation.
//
#include <bmrshared/server.hpp>
#include <bmrshared/request_handler_interface.hpp>
#include <bmrshared/directory_request_handler.hpp>
#include <bmrshared/request_response.hpp>
#include <bmrshared/color.hpp>
#include <boost/asio/signal_set.hpp>
#include <boost/gil/image.hpp>
#include <boost/gil/image_view.hpp>
#include <boost/gil/extension/io/jpeg.hpp>
#include <memory>
#include <sstream>
#include <thread>
#include <vector>
#include <complex>
#include <regex>
#include "mandelbrot.hpp"
namespace
{
const std::regex regex_renderpath(R"REGEX(\/render_z(\d+)x(-?\d+)y(-?\d+)w(\d+)h(\d+)ar(-?\d+\.\d+)ai(-?\d+\.\d+)maxiter(\d+).jpg)REGEX");
constexpr unsigned int config_max_simultanious_requests = 10;
constexpr uint64_t config_request_body_limit = (10 * 1024);
constexpr std::chrono::seconds config_request_timeout(3);
const boost::asio::ip::tcp::endpoint listen_endpoint(boost::asio::ip::tcp::v4(), 9800);
class split_req : public bmrshared::web::request_handler_interface
{
public:
split_req(const std::filesystem::path& root_dir, boost::asio::io_context& ioc) :
m_dir(root_dir),
m_ioc(ioc)
{}
~split_req() override = default;
void handle_request_http(bmrshared::web::request_response rs) override
{
auto& req = rs.get_request();
std::string target = req.target();
if (req.method() != boost::beast::http::verb::get)
{
// Other methods are not supported for directory acces
//
auto& bad_request = rs.create_response<boost::beast::http::response<boost::beast::http::string_body>>(boost::beast::http::status::bad_request, req.version());
bad_request.set(boost::beast::http::field::server, BOOST_BEAST_VERSION_STRING);
bad_request.set(boost::beast::http::field::content_type, "text/plain");
bad_request.prepare_payload();
bad_request.body() = "Bad request type.\nOnly GET and HEAD are expected for this URL.";
bad_request.keep_alive(req.keep_alive());
return;
}
std::cout << "GET: " << target << std::endl;
std::smatch base_match;
std::string tmp(target);
if (std::regex_match(tmp, base_match, regex_renderpath) && base_match.size() == 9)
{
int z = std::stoi(base_match[1]);
int offset_y = std::stoi(base_match[2]);
int offset_x = std::stoi(base_match[3]);
int width = std::stoi(base_match[4]);
int height = std::stoi(base_match[5]);
double a_real = std::stof(base_match[6]);
double a_img = std::stof(base_match[7]);
int max_iter = std::stoi(base_match[8]);
std::complex<float> factor_a(a_real, a_img);
auto renderfn = [offset_y, offset_x, z, width, height, factor_a, max_iter, req, target, rs, tmp]() mutable
{
int pixel_width = width;
int pixel_height = height;
int max_iterations = max_iter;
boost::gil::rgb8_image_t image(pixel_width,pixel_height);
auto view = boost::gil::view(image);
boost::gil::fill_pixels(view, boost::gil::rgb8_pixel_t(0));
auto painter = [&](int x, int y, int num_iter) -> void
{
if(x >= 0 && y >= 0 && x < pixel_width && y < pixel_height)
{
bmrshared::color c({.h=static_cast<double>((num_iter*2)%360),.s=1.0, .v=1.0});
auto crgb = c.rgb();
uint8_t r = crgb.r * 255;
uint8_t g = crgb.g * 255;
uint8_t b = crgb.b * 255;
const auto iter = view.at({x, y});
boost::gil::color_convert(boost::gil::rgb8_pixel_t(r,g,b), *iter);
}
};
const float zoomfactor = 1.0/std::pow(2, z);
window<float> src_window{offset_x * zoomfactor, (offset_x + 1)*zoomfactor, offset_y * zoomfactor ,(offset_y + 1)*zoomfactor};
window<float> dst_window{0.0, static_cast<float>(pixel_width), 0.0, static_cast<float>(pixel_height)};
mandelbrot<float> fractal(src_window, dst_window, max_iterations, factor_a);
fractal.apply(painter);
std::stringstream out_buffer( std::ios_base::out | std::ios_base::binary );
boost::gil::write_view(out_buffer,
boost::gil::view(image),
boost::gil::image_write_info<boost::gil::jpeg_tag>(90));
auto& ok = rs.create_response<boost::beast::http::response<boost::beast::http::string_body>>(boost::beast::http::status::ok, req.version());
ok.set(boost::beast::http::field::server, BOOST_BEAST_VERSION_STRING);
ok.set(boost::beast::http::field::content_type, "image/jpeg");
ok.body() = out_buffer.str();
ok.keep_alive(req.keep_alive());
ok.prepare_payload();
};
boost::asio::post(m_ioc, renderfn);
}
else
{
m_dir.handle_request_http(rs);
}
}
void handle_request_websocket_upgrade(const address_type& addr, const request_type& req, boost::asio::ip::tcp::socket socket) override
{
m_dir.handle_request_websocket_upgrade(addr, req, std::move(socket));
; }
private:
bmrshared::web::directory_request_handler m_dir;
boost::asio::io_context& m_ioc;
};
}
int main(int argc, char **argv)
{
std::filesystem::path root_dir = "/workspaces/network-experiment/applications/http-mandelbrot/html";
boost::asio::io_context ioc;
split_req request_handler(root_dir, ioc);
boost::asio::signal_set signals(ioc, SIGINT, SIGTERM);
std::shared_ptr<bmrshared::web::server> httpserver;
signals.async_wait([&ioc](const boost::system::error_code& error, int signal_number)
{
ioc.stop();
});
boost::asio::post(ioc, [&ioc, &httpserver, &request_handler]{
httpserver = std::make_shared<bmrshared::web::server>(
ioc,
listen_endpoint,
config_request_timeout,
config_request_body_limit,
config_max_simultanious_requests,
request_handler);
});
std::vector<std::jthread> threads;
while(threads.size() < 4)
{
threads.emplace_back([&ioc]{ioc.run();});
}
ioc.run();
threads.clear();
}

View File

@ -0,0 +1,8 @@
// GNU Lesser General Public License v3.0
// Copyright (c) 2026 Bart Beumer <bart@4beumer.nl>
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License v3.0 as published by
// the Free Software Foundation.
//
#include "mandelbrot.hpp"

View File

@ -0,0 +1,71 @@
// GNU Lesser General Public License v3.0
// Copyright (c) 2026 Bart Beumer <bart@4beumer.nl>
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License v3.0 as published by
// the Free Software Foundation.
//
#include <complex>
#include <functional>
#include "window.hpp"
template<typename TFloat>
class mandelbrot
{
public:
using painter_fn = std::function<void(int x, int y, int iterations)>;
using escape_fn = std::function<std::complex<TFloat>(std::complex<TFloat>, std::complex<TFloat>)>;
mandelbrot(
window<TFloat> source_window,
window<TFloat> destination_window,
int max_iterations,
std::complex<TFloat> factor_a)
: m_source_window(source_window)
, m_destination_window(destination_window)
, m_max_iterations(max_iterations)
, m_factor_a(factor_a)
{}
void apply(const painter_fn& painter) const
{
auto fract_fn = [factor = m_factor_a](std::complex <TFloat> z, std::complex<TFloat> c) -> std::complex<TFloat>
{
return std::pow<TFloat>(z, factor) + c;
};
const TFloat dst_factor_x = 1.0 / m_destination_window.width() * m_source_window.width();
const TFloat dst_factor_y = 1.0 / m_destination_window.height() * m_source_window.height();
for (int x = 0; x < m_destination_window.width(); ++x)
{
for (int y = 0; y < m_destination_window.height(); ++y)
{
const auto _x = x * dst_factor_x + m_source_window.x_min();
const auto _y = y * dst_factor_y + m_source_window.y_min();
const int iterations = escape(std::complex<TFloat>(_y, _x), fract_fn);
painter(y,x,iterations);
}
}
}
int escape(const std::complex<TFloat>& c,
const escape_fn& func) const
{
std::complex<TFloat> z(0);
int iter = 0;
while (abs(z) < 2.0 && iter < m_max_iterations) {
z = func(z, c);
iter++;
}
return iter;
}
private:
window<TFloat> m_source_window;
window<TFloat> m_destination_window;
int m_max_iterations;
std::complex<TFloat> m_factor_a;
};

View File

@ -0,0 +1,41 @@
// GNU Lesser General Public License v3.0
// Copyright (c) 2026 Bart Beumer <bart@4beumer.nl>
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License v3.0 as published by
// the Free Software Foundation.
//
#pragma once
template <typename T>
class window
{
public:
window(T x_min, T x_max, T y_min, T y_max)
: m_x_min(x_min), m_x_max(x_max), m_y_min(y_min), m_y_max(y_max)
{}
T size() const {
return width() * height();
}
T width() const {
return m_x_max - m_x_min;
}
T height() const {
return m_y_max - m_y_min;
}
T x_min() const {return m_x_min;}
T x_max() const {return m_x_max;}
T y_min() const {return m_y_min;}
T y_max() const {return m_y_max;}
private:
T m_x_min;
T m_x_max;
T m_y_min;
T m_y_max;
};

View File

@ -1,5 +1,4 @@
cmake_minimum_required(VERSION 3.20) find_package(Boost 1.89.0 REQUIRED COMPONENTS program_options headers CONFIG)
find_package(Boost 1.90.0 REQUIRED COMPONENTS program_options headers CONFIG)
find_package(JPEG REQUIRED) find_package(JPEG REQUIRED)
project(text2image) project(text2image)

View File

@ -0,0 +1 @@
add_subdirectory(lib)

View File

@ -0,0 +1,29 @@
// GNU Lesser General Public License v3.0
// Copyright (c) 2025 Bart Beumer <bart@4beumer.nl>
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License v3.0 as published by
// the Free Software Foundation.
//
#pragma once
#include "request_handler_interface.hpp"
#include <filesystem>
namespace bmrshared::web
{
class directory_request_handler : public request_handler_interface
{
public:
explicit directory_request_handler(const std::filesystem::path& document_root);
~directory_request_handler() override;
void handle_request_http(request_response rs) override;
void handle_request_websocket_upgrade(const address_type& address,
const request_type& req,
boost::asio::ip::tcp::socket socket) override;
private:
std::filesystem::path m_document_root;
};
} // namespace bmrshared::web

View File

@ -0,0 +1,26 @@
// GNU Lesser General Public License v3.0
// Copyright (c) 2025 Bart Beumer <bart@4beumer.nl>
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License v3.0 as published by
// the Free Software Foundation.
//
#pragma once
#include "server.hpp"
#include <bmrshared/request_response.hpp>
namespace bmrshared::web
{
class request_handler_interface
{
public:
using address_type = server::address_type;
using request_type = server::request_type;
virtual ~request_handler_interface() = default;
virtual void handle_request_http(request_response rs) = 0;
virtual void handle_request_websocket_upgrade(const address_type&, const request_type&, boost::asio::ip::tcp::socket) = 0;
};
} // namespace bmrshared::web

View File

@ -0,0 +1,104 @@
// GNU Lesser General Public License v3.0
// Copyright (c) 2025 Bart Beumer <bart@4beumer.nl>
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License v3.0 as published by
// the Free Software Foundation.
//
#pragma once
#include "server.hpp"
#include <boost/beast.hpp>
#include <functional>
#include <memory>
namespace bmrshared::web
{
class request_response final
{
public:
using response_sender = std::function<void(boost::beast::tcp_stream& stream)>;
using request_type = bmrshared::web::server::request_type;
using callback_on_finalize = std::function<void(response_sender send_response)>;
private:
class response_writer_interface
{
public:
virtual ~response_writer_interface();
virtual void write_response(boost::beast::tcp_stream&) = 0;
};
template<typename response_type>
class response_writer final : public response_writer_interface
{
public:
template<typename... TArgs>
response_writer(TArgs&&... args)
: m_response(std::forward<TArgs>(args)...)
{}
~response_writer() override = default;
void write_response(boost::beast::tcp_stream& stream) override
{
boost::beast::http::write(stream, m_response);
}
response_type& response()
{
return m_response;
}
private:
response_type m_response;
};
class internal_request_response final
{
public:
// Disabling copying and moving.
internal_request_response() = delete;
internal_request_response(const internal_request_response&) = delete;
internal_request_response(internal_request_response&&) = delete;
internal_request_response& operator=(const internal_request_response&) = delete;
internal_request_response& operator=(internal_request_response&&) = delete;
internal_request_response(request_type request, callback_on_finalize cb_finalize);
~internal_request_response();
const request_type& get_request() const;
void set_response_sender(std::unique_ptr<response_writer_interface> response_writer);
private:
request_type m_request;
callback_on_finalize m_cb_finalize;
std::unique_ptr<response_writer_interface> m_response_writer;
};
public:
request_response() = delete;
request_response(const request_response&) = default;
request_response(request_response&&) = default;
request_response& operator=(const request_response&) = default;
request_response& operator=(request_response&&) = default;
request_response(request_type request, callback_on_finalize cb_finalize);
~request_response();
template<typename response_type, typename... arg_types>
response_type& create_response(arg_types&&... args)
{
auto created = std::make_unique<response_writer<response_type>>(std::forward<arg_types>(args)...);
auto& resp = created->response();
m_internal->set_response_sender(std::move(created));
return resp;
}
const request_type& get_request() const;
private:
std::shared_ptr<internal_request_response> m_internal;
};
}

View File

@ -0,0 +1,45 @@
// GNU Lesser General Public License v3.0
// Copyright (c) 2025 Bart Beumer <bart@4beumer.nl>
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License v3.0 as published by
// the Free Software Foundation.
//
#pragma once
#include <boost/asio/io_context.hpp>
#include <boost/asio/ip/address.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/beast/http/message.hpp>
#include <boost/beast/http/string_body.hpp>
#include <chrono>
#include <memory>
namespace bmrshared::web
{
namespace detail
{
class internal_server;
}
class response_promise;
class request_handler_interface;
class server final
{
public:
using address_type = boost::asio::ip::address;
using request_type = boost::beast::http::request<boost::beast::http::string_body>;
server(boost::asio::io_context& io_context,
const boost::asio::ip::tcp::endpoint& listen_endpoint,
std::chrono::seconds incoming_request_timeout,
uint64_t request_body_limit,
unsigned int max_simultaneous_requests,
request_handler_interface& handler);
~server();
private:
std::shared_ptr<detail::internal_server> m_internal;
};
} // namespace bmrshared::web

View File

@ -0,0 +1,29 @@
find_package(Boost 1.90.0 REQUIRED COMPONENTS headers CONFIG)
project(bmrshared-web)
add_library(
${PROJECT_NAME}
directory_request_handler.cpp
internal_server.cpp
request_response.cpp
server.cpp
)
set_property(
TARGET ${PROJECT_NAME}
PROPERTY CXX_STANDARD 23
)
target_include_directories(
${PROJECT_NAME}
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../include
)
target_link_libraries(
${PROJECT_NAME}
PUBLIC
bmrshared
bmrshared-magic
Boost::headers
)

View File

@ -0,0 +1,159 @@
// GNU Lesser General Public License v3.0
// Copyright (c) 2025 Bart Beumer <bart@4beumer.nl>
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License v3.0 as published by
// the Free Software Foundation.
//
#include <bmrshared/directory_request_handler.hpp>
#include <algorithm>
#include <array>
#include <bmrshared/magic_file_info.hpp>
#include <filesystem>
#include <future>
#include <iomanip>
#include <iostream>
#include <optional>
#include <string_view>
namespace beast = boost::beast;
namespace http = beast::http;
namespace
{
struct ExtensionMimeMapping
{
std::string_view extension;
std::string_view mime;
};
bmrshared::magic_file_info mfi("magic.mgc");
} // namespace
using bmrshared::web::directory_request_handler;
directory_request_handler::directory_request_handler(const std::filesystem::path& document_root)
: m_document_root(std::filesystem::absolute(document_root))
{
}
directory_request_handler::~directory_request_handler() = default;
void directory_request_handler::handle_request_http(request_response rs)
{
const auto& req = rs.get_request();
if ((req.method() != http::verb::get) && (req.method() != http::verb::head))
{
// Other methods are not supported for directory acces
//
auto& bad_request = rs.create_response<http::response<http::string_body>>(http::status::bad_request, req.version());
bad_request.set(http::field::server, BOOST_BEAST_VERSION_STRING);
bad_request.set(http::field::content_type, "text/plain");
bad_request.prepare_payload();
bad_request.body() = "Bad request type.\nOnly GET and HEAD are expected for this URL.";
bad_request.keep_alive(req.keep_alive());
return;
}
std::string_view target = req.target();
{
target.remove_prefix(std::min(target.find_first_not_of(R"(/\)"), target.size()));
}
std::filesystem::path requested_file = m_document_root / std::filesystem::path(target);
std::optional<std::filesystem::path> found_file;
// See if requested path is a directory, use index.html if that is the case.
if (std::filesystem::is_directory(requested_file))
{
auto indexPath = requested_file / "index.html";
if (std::filesystem::is_regular_file(indexPath))
{
found_file = indexPath;
}
}
// See if requested path is a file.
if (std::filesystem::is_regular_file(requested_file))
{
found_file = requested_file;
}
if (!found_file)
{
auto& not_found = rs.create_response<http::response<http::string_body>>(http::status::not_found, req.version());
not_found.set(http::field::server, BOOST_BEAST_VERSION_STRING);
not_found.set(http::field::content_type, "text/plain");
not_found.prepare_payload();
not_found.body() = "File not found.";
not_found.keep_alive(req.keep_alive());
return;
}
beast::error_code ec;
http::file_body::value_type body;
body.open(found_file->native().c_str(), beast::file_mode::scan, ec);
if (ec)
{
auto& internal = rs.create_response<http::response<http::string_body>>(http::status::internal_server_error, req.version());
internal.set(http::field::server, BOOST_BEAST_VERSION_STRING);
internal.set(http::field::content_type, "text/plain");
internal.prepare_payload();
internal.body() = "Internal server error.";
internal.keep_alive(req.keep_alive());
return;
}
const auto file_size = body.size();
const auto file_last_modified_utc = std::chrono::time_point_cast<std::chrono::seconds>(std::filesystem::last_write_time(*found_file));
const std::string last_modified = std::format("{0:%a}, {0:0>2%d} {0:0>2%e} {0:0>4%Y} {0:%H}:{0:%M}:{0:%S} GMT", file_last_modified_utc);
const std::string mime_type = mfi.get_mime(*found_file);
if (req.count(http::field::if_modified_since) != 0 && (req[http::field::if_modified_since] == last_modified))
{
auto& head = rs.create_response<http::response<http::string_body>>(http::status::not_modified, req.version());
head.set(http::field::server, BOOST_BEAST_VERSION_STRING);
head.keep_alive(req.keep_alive());
return;
}
// Okay, we have a file so let us send the correct response.
if (req.method() == http::verb::head)
{
auto& head = rs.create_response<http::response<http::string_body>>(http::status::ok, req.version());
head.set(http::field::server, BOOST_BEAST_VERSION_STRING);
head.set(http::field::content_type, mime_type);
head.set(http::field::last_modified, last_modified);
head.set(http::field::cache_control, "public, max-age=360");
head.set(http::field::content_length, std::to_string(file_size));
head.keep_alive(req.keep_alive());
return;
}
else if (req.method() == http::verb::get)
{
http::response<http::file_body> head(std::piecewise_construct,
std::make_tuple(std::move(body)),
std::make_tuple(http::status::ok, req.version()));
head.set(http::field::server, BOOST_BEAST_VERSION_STRING);
head.set(http::field::content_type, mime_type);
head.set(http::field::last_modified, last_modified);
head.set(http::field::cache_control, "public, max-age=360");
head.set(http::field::content_length, std::to_string(file_size));
head.keep_alive(req.keep_alive());
rs.create_response<http::response<http::file_body>>(std::move(head));
return;
}
}
void directory_request_handler::handle_request_websocket_upgrade(const address_type&,
const request_type&,
boost::asio::ip::tcp::socket)
{
// We ignore websocket upgrades. throw away the socket to close connection.
}

View File

@ -0,0 +1,226 @@
// GNU Lesser General Public License v3.0
// Copyright (c) 2025 Bart Beumer <bart@4beumer.nl>
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License v3.0 as published by
// the Free Software Foundation.
//
#include "internal_server.h"
#include <bmrshared/request_handler_interface.hpp>
#include <algorithm>
#include <iostream>
using bmrshared::web::detail::internal_server;
internal_server::internal_server(boost::asio::io_context& io_context,
const boost::asio::ip::tcp::endpoint& listen_endpoint,
std::chrono::seconds incoming_request_timeout,
uint64_t request_body_limit,
unsigned int max_simultaneous_requests,
request_handler_interface& handler)
: m_io_context(io_context)
, m_strand(io_context)
, m_endPoint(listen_endpoint)
, m_incoming_request_timeout(incoming_request_timeout)
, m_max_simultaneous_requests(max_simultaneous_requests)
, m_request_body_limit(request_body_limit)
, m_acceptor(io_context)
, m_handler(handler)
, m_sessions()
{
}
internal_server::~internal_server() = default;
void internal_server::run()
{
m_acceptor.open(m_endPoint.protocol());
m_acceptor.set_option(boost::asio::socket_base::reuse_address(true));
m_acceptor.bind(m_endPoint);
m_acceptor.listen(boost::asio::socket_base::max_listen_connections);
async_accept();
}
void internal_server::async_accept()
{
// Start accepting incoming connections asynchronously.
m_acceptor.async_accept(
[weak_server = weak_from_this()](boost::system::error_code ec, boost::asio::ip::tcp::socket socket)
{
auto shared_server = weak_server.lock();
if(shared_server)
{
boost::asio::post(shared_server->m_strand, [weak_server, ec, s = std::make_shared<boost::asio::ip::tcp::socket>(std::move(socket))]
{
auto shared_server = weak_server.lock();
if (shared_server)
{
shared_server->do_accept(ec, std::move(*s));
}
});
}
});
}
void internal_server::do_accept(boost::system::error_code ec, boost::asio::ip::tcp::socket socket)
{
if (ec)
{
// Error while accepting incoming connection. Ignore.
// TODO: something needs to be done here.
}
else
{
auto sessionptr = std::make_shared<http_session>(std::move(socket));
m_sessions.push_back(sessionptr);
}
async_accept();
session_process_queued();
}
void internal_server::session_process_queued()
{
// Determine how many outstanding requests we have.
std::size_t count_responding = 0;
for(const auto& session : m_sessions)
{
if (session->m_state == session_state::wait_response)
{
count_responding++;
}
}
// Send out all processed responses.
for (auto& session : m_sessions)
{
if (session)
{
http_session& s = *session;
auto& our = s.m_outstanding_request;
if (our && our->m_response_sender)
{
(*our->m_response_sender)(s.stream);
our.reset();
s.m_state = session_state::queue_reading;
}
}
}
// Allow for more outstanding requests unless we are already over the maximum.
if (count_responding < m_max_simultaneous_requests)
{
for(auto& session : m_sessions)
{
if (!session)
{
// do nothing.
}
else if (session->m_state == session_state::queue_reading)
{
http_session& s = *session;
s.m_state = session_state::reading;
s.parser.emplace(); // Create empty parser object.
s.parser->body_limit(m_request_body_limit);
s.stream.expires_after(m_incoming_request_timeout);
auto handler = [weak_server = weak_from_this(), session](boost::beast::error_code ec, std::size_t size)
{
auto shared_server = weak_server.lock();
if (shared_server)
{
boost::asio::post(shared_server->m_strand, [weak_server, session, ec, size]
{
auto shared_server = weak_server.lock();
if (shared_server)
{
shared_server->session_on_read(session, ec, size);
}
});
}
};
boost::beast::http::async_read(s.stream, s.buffer, *s.parser, handler);
}
}
}
}
void internal_server::session_on_read(std::shared_ptr<http_session> session,
boost::beast::error_code ec,
std::size_t size)
{
http_session& s = *session;
if (ec)
{
// Any other kind of error, also shutdown and remove http_session
s.stream.socket().shutdown(boost::asio::ip::tcp::socket::shutdown_send, ec);
session_remove(session);
}
else if (boost::beast::websocket::is_upgrade(s.parser->get()))
{
// Received a websocket upgrade. pass socket to request handler, remove session from our administration.
const auto& address = s.stream.socket().remote_endpoint().address();
m_handler.handle_request_websocket_upgrade(address, s.parser->get(), s.stream.release_socket());
session_remove(session);
}
else
{
std::shared_ptr<http_request> req = std::make_shared<http_request>();
s.m_outstanding_request = req;
// A regular HTTP request.
auto finalize_response = [session, weak_server = weak_from_this(), req](const request_response::response_sender& rs)
{
// We have a response, add it to our spot in the queue.
auto shared_server = weak_server.lock();
if (shared_server)
{
// Queue processing of the queue so we can continue.
boost::asio::post(shared_server->m_strand,
[shared_server, req, rs]
{
req->m_response_sender = rs;
shared_server->session_process_queued();
});
}
};
const auto& address = session->stream.socket().remote_endpoint().address();
const auto& request = session->parser->get();
auto rs = request_response(request, finalize_response);
auto& internal = rs.create_response<boost::beast::http::response<boost::beast::http::string_body>>(boost::beast::http::status::internal_server_error, request.version());
internal.set(boost::beast::http::field::server, BOOST_BEAST_VERSION_STRING);
internal.set(boost::beast::http::field::content_type, "text/plain");
internal.prepare_payload();
internal.body() = "Internal server error.";
internal.keep_alive(request.keep_alive());
m_handler.handle_request_http(rs);
s.m_state = session_state::wait_response;
}
session_process_queued();
}
void internal_server::session_finalize_response(std::shared_ptr<http_session> session)
{
if(session)
{
session->m_state = session_state::queue_reading;
session_process_queued();
}
}
void internal_server::session_remove(std::shared_ptr<http_session> session)
{
m_sessions.erase(std::remove_if(m_sessions.begin(),
m_sessions.end(),
[session](const std::shared_ptr<http_session>& s)
{
return session.get() == s.get();
}),
m_sessions.end());
}

View File

@ -0,0 +1,85 @@
// GNU Lesser General Public License v3.0
// Copyright (c) 2025 Bart Beumer <bart@4beumer.nl>
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License v3.0 as published by
// the Free Software Foundation.
//
#pragma once
#include <bmrshared/server.hpp>
#include <bmrshared/request_response.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/strand.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <chrono>
#include <memory>
#include <queue>
#include <iostream>
namespace bmrshared::web {
namespace detail {
enum class session_state {
queue_reading,
reading,
wait_response
};
struct http_request
{
std::optional<request_response::response_sender> m_response_sender;
};
struct http_session {
explicit http_session(boost::asio::ip::tcp::socket s)
: stream(std::move(s))
{
}
~http_session()
{
}
boost::beast::tcp_stream stream;
boost::beast::flat_buffer buffer;
boost::optional<boost::beast::http::request_parser<boost::beast::http::string_body>> parser;
session_state m_state = session_state::queue_reading;
std::shared_ptr<http_request> m_outstanding_request;
};
class internal_server final : public std::enable_shared_from_this<internal_server> {
public:
using address_type = bmrshared::web::server::address_type;
using request_type = bmrshared::web::server::request_type;
public:
internal_server(boost::asio::io_context& io_context,
const boost::asio::ip::tcp::endpoint& listen_endpoint,
std::chrono::seconds incoming_request_timeout,
uint64_t request_body_limit,
unsigned int max_simultaneous_requests,
request_handler_interface& handler);
~internal_server();
void run();
private:
void async_accept();
void do_accept(boost::system::error_code ec, boost::asio::ip::tcp::socket socket);
void session_process_queued();
void session_on_read(std::shared_ptr<http_session> session, boost::beast::error_code ec, std::size_t size);
void session_finalize_response(std::shared_ptr<http_session> session);
void session_remove(std::shared_ptr<http_session> session);
private:
boost::asio::io_context& m_io_context;
boost::asio::io_context::strand m_strand;
boost::asio::ip::tcp::endpoint m_endPoint;
std::chrono::seconds m_incoming_request_timeout;
unsigned int m_max_simultaneous_requests;
uint64_t m_request_body_limit;
boost::asio::ip::tcp::acceptor m_acceptor;
request_handler_interface& m_handler;
std::vector<std::shared_ptr<http_session>> m_sessions;
};
} // namespace detail
} // namespace bmrshared::web

View File

@ -0,0 +1,56 @@
// GNU Lesser General Public License v3.0
// Copyright (c) 2025 Bart Beumer <bart@4beumer.nl>
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License v3.0 as published by
// the Free Software Foundation.
//
#include <bmrshared/request_response.hpp>
#include <iostream>
using bmrshared::web::request_response;
request_response::response_writer_interface::~response_writer_interface() = default;
request_response::internal_request_response::internal_request_response(request_type request, callback_on_finalize cb_finalize)
: m_request(std::move(request))
, m_cb_finalize(std::move(cb_finalize))
, m_response_writer()
{
}
request_response::internal_request_response::~internal_request_response()
{
if (m_response_writer)
{
std::shared_ptr<response_writer_interface> shared_writer(std::move(m_response_writer));
m_cb_finalize(
[shared_writer](boost::beast::tcp_stream& stream)
{
shared_writer->write_response(stream);
});
}
}
const request_response::request_type& request_response::internal_request_response::get_request() const
{
return m_request;
}
void request_response::internal_request_response::set_response_sender(std::unique_ptr<response_writer_interface> response_writer)
{
m_response_writer = std::move(response_writer);
}
request_response::request_response(request_type request, callback_on_finalize cb_finalize)
: m_internal(std::make_unique<internal_request_response>(std::move(request), std::move(cb_finalize)))
{
}
request_response::~request_response() = default;
const request_response::request_type& request_response::get_request() const
{
return m_internal->get_request();
}

View File

@ -0,0 +1,25 @@
// GNU Lesser General Public License v3.0
// Copyright (c) 2025 Bart Beumer <bart@4beumer.nl>
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License v3.0 as published by
// the Free Software Foundation.
//
#include "internal_server.h"
#include <bmrshared/server.hpp>
using bmrshared::web::server;
using bmrshared::web::detail::internal_server;
server::server(boost::asio::io_context& io_context,
const boost::asio::ip::tcp::endpoint& listen_endpoint,
std::chrono::seconds incoming_request_timeout,
uint64_t request_body_limit,
unsigned int max_simultaneous_requests,
request_handler_interface& handler)
: m_internal(std::make_shared<internal_server>(io_context, listen_endpoint, incoming_request_timeout, request_body_limit, max_simultaneous_requests, handler))
{
m_internal->run();
}
server::~server() = default;

View File

@ -0,0 +1,138 @@
// GNU Lesser General Public License v3.0
// Copyright (c) 2026 Bart Beumer <bart@4beumer.nl>
//
// This program is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License v3.0 as published by
// the Free Software Foundation.
//
#pragma once
#include <cmath>
#include <optional>
#include <utility>
namespace bmrshared
{
class color
{
public:
struct color_hsv
{
double h;
double s;
double v;
};
struct color_rgb
{
double r;
double g;
double b;
};
color() = delete;
~color() = default;
constexpr color(const color&) = default;
constexpr color(color&&) = default;
constexpr color& operator=(const color&) = default;
constexpr color& operator=(color&&) = default;
constexpr color(const color_hsv& c)
: m_rgb()
, m_hsv(c)
{}
constexpr color(const color_rgb& c)
: m_rgb(c)
, m_hsv()
{}
constexpr const color_rgb& rgb() const
{
if (!m_rgb)
{
m_rgb = calculate_rgb(*m_hsv);
}
return *m_rgb;
}
constexpr const color_hsv& hsv() const
{
if (!m_hsv)
{
m_hsv = calculate_hsv(*m_rgb);
}
return *m_hsv;
}
private:
static constexpr color_hsv calculate_hsv(const color_rgb& c)
{
color_hsv r{.h = 0.0, .s = 0.0, .v = 0.0};
double min = std::min(c.r, std::min(c.g,c.b));
r.v = std::max(c.r, std::max(c.g,c.b));
double delta = r.v - min;
r.s = (r.v <= 0.0) ? 0.0 : (delta / r.v);
if (r.s <= 0.0)
{
r.h = 0.0;
}
else
{
if (c.r == r.v)
{
r.h = (c.g - c.b) / delta;
}
else if (c.g == r.v)
{
r.h = 2.0 + (c.b - c.r) / delta;
}
else if (c.b == r.v)
{
r.h = 4.0 + (c.r - c.g) / delta;
}
r.h*=60.0;
if (r.h < 0.0)
{
r.h +=360.0;
}
}
return r;
}
static constexpr color_rgb calculate_rgb(const color_hsv& in)
{
double C = in.s * in.v;
double X = C*(1-std::fabs(std::fmod(in.h/60.0, 2)-1));
double m = in.v-C;
color_rgb result{.r = 0.0, .g = 0.0, .b = 0.0};
if(in.h >= 0.0 && in.h < 60.0){
result = {.r = C, .g = X, .b = 0.0};
}
else if(in.h >= 60.0 && in.h < 120.0){
result = {.r = X, .g = C, . b = 0.0};
}
else if(in.h >= 120.0 && in.h < 180.0){
result = {.r = 0.0, .g = C, .b = X};
}
else if(in.h >= 180.0 && in.h < 240.0){
result = {.r = 0.0, .g = X, .b = C};
}
else if(in.h >= 240.0 && in.h < 300.0){
result = {.r = X, .g = 0.0, .b = C};
}
else{
result = {.r = C, .g = 0.0, .b = X};
}
result.r+=m;
result.g+=m;
result.b+=m;
return result;
}
mutable std::optional<color_rgb> m_rgb;
mutable std::optional<color_hsv> m_hsv;
};
}