/beauty

A Simple C++ Http server/client above Boost.Beast

Primary LanguageC++MIT LicenseMIT

A Rose
A simple Http server and client above Boost.Beast

Beauty is a layer above Boost.Beast which provide facilities to create Http server or client. Beauty allows the creation of synchronous or asynchronous server and client, and adds some signals and timer management based on Boost.Asio

Features

  • Http or Http/s server or client side
  • Websocket (no TLS yet) for server and client (still experimental)
  • Synchronous or Asynchronous API
  • Timeout support
  • Postponed response from server support
  • Easy routing server with placeholders
  • Timers and signals support included
  • Startable and stoppable application event loop
  • Customizable thread pool size
  • Work-in-progress: Swagger description API

Examples

  • a server

A more complete example is available in examples/server.cpp

#include <beauty/beauty.hpp>

int main()
{
    // Create a server
    beauty::server server;

    // Add a default '/' route
    server.add_route("/")
        .get([](const auto& req, auto& res) {
            res.body() = "It's work ;) ... it works! :)";
        });

    // Add a '/person/:id' route
    server.add_route("/person/:id")
        .get([](const auto& req, auto& res) {
            auto id = req.a("id").as_string();
            res.body() = "You asked for the person id: " + id;
        });

    // Open the listening port
    server.listen(8085);
        // Listen will automatically start the loop in a separate thread

    // Wait for the server to stop
    server.wait();
}
  • a synchronous client
#include <beauty/beauty.hpp>

#include <iostream>

int main()
{
    // Create a client
    beauty::client client;

    // Request an URL
    auto[ec, response] = client.get("http://127.0.0.1:8085");

    // Check the result
    if (!ec) {
        if (response.is_status_ok()) {
            // Display the body received
            std::cout << response.body() << std::endl;
        } else {
            std::cout << response.status() << std::endl;
        }   
    } else {
        // An error occurred
        std::cout << ec << ": " << ec.message() << std::endl;
    }
}
  • an asynchronous client
#include <beauty/beauty.hpp>

#include <iostream>
#include <chrono>

int main()
{
    // Create a client
    beauty::client client;

    // Request an URL
    client.get("http://127.0.0.1:8085",
               [](auto ec, auto&& response) {
                   // Check the result
                   if (!ec) {
                       if (response.is_status_ok()) {
                           // Display the body received
                           std::cout << response.body() << std::endl;
                       } else {
                           std::cout << response.status() << std::endl;
                       }
                   } else {
                       // An error occurred
                       std::cout << ec << ": " << ec.message() << std::endl;
                   }
               });

    // Need to wait a little bit to received the response
    for (int i = 0; i < 10; ++i) {
        std::cout << '.'; std::cout.flush();
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
    std::cout << std::endl;
}
  • timers
#include <beauty/beauty.hpp>

#include <iostream>

int main()
{
    // Launch a repeatable timer each 250ms
    int timer_count = 4;
    beauty::repeat(0.250, [&timer_count]() {
            std::cout << "Tick..." << std::endl;
            if (--timer_count == 0) {
                std::cout << "Dring !" << std::endl;
                beauty::stop();
            }
        });

    // Launch a one shot timer after 600ms
    beauty::after(0.600, [] {
            std::cout << "Snooze !" << std::endl;
    });

    // Wait for the end
    beauty::wait();
}
  • signals
#include <beauty/beauty.hpp>

#include <iostream>

int main()
{
    // Catch the small signals
    beauty::signal({SIGUSR1, SIGUSR2}, [](int s) {
        std::cout << "Shot miss..." << std::endl;
    });

    // Catch the big one
    beauty::signal(SIGINT, [](int s) {
        std::cout << "Head shot !" << std::endl;
        beauty::stop();
    });

    // Wait for the end
    beauty::wait();
}
  • Websocket server

Here an example of a simple chat server using websocket, use ws://127.0.0.1:8085/chat/MyRoom to connect to a room named MyRoom.

#include <beauty/beauty.hpp>

#include <string>
#include <unordered_map>

using Sessions = std::unordered_map<std::string /* UUID */, std::weak_ptr<beauty::websocket_session>>;
using Rooms = std::unordered_map<std::string /* ROOM */,  Sessions>;

Rooms rooms;

int
main(int argc, char* argv[])
{
    beauty::server server;

    server.add_route("/chat/:room")
        .ws(beauty::ws_handler{
            .on_connect = [](const beauty::ws_context& ctx) {
                rooms[ctx.attributes["room"].as_string()][ctx.uuid] = ctx.ws_session;
            },
            .on_receive = [](const beauty::ws_context& ctx, const char* data, std::size_t size, bool is_text) {
                for (auto& [uuid, session] : rooms[ctx.attributes["room"].as_string()]) {
                    if (auto s = session.lock(); s) {
                        s->send(std::string(data, size));
                    }
                }
            },
            .on_disconnect = [](const beauty::ws_context& ctx) {
                for (auto& [name, room] : rooms) {
                    room.erase(ctx.uuid);
                }
            }
        });

    server.listen(8085);

    beauty::wait();
}
  • Websocket client

Here an example of a simple client that connect to the previous chat server, use ws://127.0.0.1:8085/chat/MyRoom to connect to a room named MyRoom.

int
main(int argc, char* argv[])
{
    beauty::client client;

    client.ws(argv[1], beauty::ws_handler{
        .on_connect = [](const beauty::ws_context& ctx) {
            std::cout << "--- Connected --- to: " << ctx.remote_endpoint << std::endl;
        },
        .on_receive = [](const beauty::ws_context& ctx, const char* data, std::size_t size, bool is_text) {
            std::cout << "--- Received:\n";
            std::cout.write(data, size);
            std::cout << "\n---" << std::endl;

        },
        .on_disconnect = [&client](const beauty::ws_context& ctx) {
            std::cout << "--- Disconnected ---" << std::endl;
        },
        .on_error = [&client](boost::system::error_code ec, const char* what) {
            std::cout << "--- Error: " << ec << ", " << ec.message() << ": " << what << std::endl;

            std::cout << "Retrying connection on error in 1s..." << std::endl;
            beauty::after(1.0, [&client] {
                std::cout << "Trying connection..." << std::endl;
                client.ws_connect();
            });
        }
    });

    std::cout << "> ";
    std::cout.flush();
    for(std::string line; getline(std::cin, line);) {
        if (line == "q") break;
        std::cout << "> ";
        std::cout.flush();
        client.ws_send(std::move(line));
    }
}

Further examples can be found into the binaries directory at the root of the project.

Build

Beauty depends Boost.Beast and OpenSsl. You can rely on Conan to get the package or only the FindPackage from CMake.

Linux

For Conan, you need to provide a profile, here, using default as profile.

git clone https://github.com/dfleury2/beauty.git
cd beauty
mkdir build && cd build
conan install .. -pr default -pr:b default -b missing -of ..
cmake .. -DCMAKE_BUILD_TYPE=Release
cmake --build . -j4

If you do not want to use Conan, you can try:

git clone https://github.com/dfleury2/beauty.git
cd beauty
mkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DCONAN=false
cmake --build . -j4

Hope your the dependencies are found on your Linux.

If you want to disable openssl:

conan install .. -pr default -pr:b default -o beauty:openssl=False -of . -r conancenter -b missing

Linux Makefile

For those who want just a simple Makefile without bothering with dependency management, in the docs directory, there is an example of a simple one-shot Makefile to create a library archive to be used in another project. This Makefile must used (and moved) at the at the project root.

LIB  = libeauty.a
OBJS = src/acceptor.o \
       src/application.o \
       src/attributes.o \
       src/client.o \
       src/exception.o \
       src/route.o \
       src/router.o \
       src/server.o \
       src/sha1.o \
       src/signal.o \
       src/swagger.o \
       src/timer.o \
       src/url.o \
       src/utils.o

.cpp.o:
	g++ -std=c++17 -Wall -O2 -c -o $@ $< -I./include -I./build/include

$(LIB): version.hpp $(OBJS)
	ar -r $@ $(OBJS)

version.hpp:
	VERSION=1.0.0-rc1 envsubst < src/version.hpp.in > ./include/beauty/version.hpp

clean:
	rm -f libeauty.a ./src/*.o ./include/beauty/version.hpp

Windows

You must have a valid profile for VS Build. At this time conan center provide only pre-built packages for VS2019 (compiler.version = 16) and x86_64 mode (arch=x86_64). Boost and OpenSSL can be compiled automatically for VS2022.

git clone https://github.com/dfleury2/beauty.git
cd beauty
mkdir build-vs2019 && cd build-vs2019
conan install .. -pr vs2019 -pr:b vs2019 -b missing -of .
cmake ..
cmake --build . --config Release

The binaries will be created in the examples\Release directory.cd ..

or for Visual Studio 2022. Unfortunately at this time, I did not succeed to compile openssl with compiler.version = 17...

To be improved...