Simple C ++ backend: is it possible?

I had a dream – to write backend on the C ++… But I didn’t want to understand unix sockets, TCP, multithreaded / asynchronous request processing and many other things. I didn’t believe that there are still no minimalistic frameworks. And today I will tell you how you can simply do HTTP API microservice on C ++ using a framework Drogon

Drogon framework logo from its GitHub repository
Drogon framework logo from its GitHub repository

Drogon Framework

DrogonHTTP-framework for creating server applications in C ++ 14/17/20. Named after the dragon from the Game of Thrones TV series. Supports non-blocking I / O, coroutines, asynchronous work with the database (MySQL, PostgreSQL), ORM, Websocket and much more. A full list of features can be found on the website documentation or in wiki on GitHub.

There are many options for installing this framework, ranging from compiling the sources and installing on the system to downloading docker-image. Choose the method that suits you and let’s go!

Configuration

For configuration Drogon there are two ways. The first and the simplest is to specify the settings in the aspect-oriented style:

#include <stdlib.h>
#include <drogon/drogon.h>

using namespace drogon;

int main() {
    app()
        // Слушаем адрес 0.0.0.0 с портом 3000
        .addListener("0.0.0.0", 3000)
        // Выставляем кол-во I/O-потоков
        .setThreadNum(8)
        // Отключаем HTTP заголовок с названием сервера
        .enableServerHeader(false)
        // Запускаем приложение
        .run();

    return EXIT_SUCCESS;
}

But there is an option that is more aesthetic and convenient – configuration via Json-file. For this we create Json-file next to the executable file, and in the source code we indicate that we take the configuration from this file.

{
  "listeners": [
    {
      "address": "0.0.0.0",
      "port": 3000,
      "https": false
    }
  ],
  "app": {
    "number_of_threads": 8,
    "server_header_field": ""
  }
}
#include <stdlib.h>
#include <drogon/drogon.h>

using namespace drogon;

int main() {
    app()
        .loadConfigFile("./config.json")
        .run();

    return EXIT_SUCCESS;
}

It is worth clarifying that, of course, the configuration is read once before launching and you cannot change it on the fly without restarting the application.

Registering handlers

The framework offers two ways to register handlers HTTP-requests: AOP handlers (inspired by express.js) and controllers (from MVC template). Since I am showing you a simple example of a microservice, we will use the first option.

This is done very simply. For the application, we register a handler by passing path, a processing function and restrictions in the form of HTTP methods:

#include <stdlib.h>
#include <drogon/drogon.h>

using namespace drogon;

typedef std::function<void(const HttpResponsePtr &)> Callback;

void indexHandler(const HttpRequestPtr &request, Callback &&callback) {
    // Код обработчика
  	// Вызов обратной функции для передачи управления фреймворку
  	callback();
}

int main() {
    app()
      	// Регистрируем обработчик indexHandler
        // для запроса
        // GET /
        .registerHandler("/", &indexHandler, {Get})
        .loadConfigFile("./config.json")
        .run();

    return EXIT_SUCCESS;
}

Creating a handler

Let’s make it so indexHandler returned a JSON object to the client:

{
  "message": "Hello, world!"
}

To do this, create a JSON object in a function indexHandler and assign by key message meaning Hello, world!:

Json::Value jsonBody;
jsonBody["message"] = "Hello, world!";

Next, we need to form an HTTP response with the desired status code and headers, for this there is a method newHttpJsonResponse at the class HttpResponse:

auto response = HttpResponse::newHttpJsonResponse(jsonBody);

It forms a response of the form:

HTTP/1.0 200 OK
Content-Type: application/json; charset=UTF-8
Content-Length: 28

{"message":"Hello, world!"}

And all that remains is to send the generated HTTP response to the client. passing response v callback:

callback(response);

As a result, we get the following code:

#include <stdlib.h>
#include <drogon/drogon.h>

using namespace drogon;

typedef std::function<void(const HttpResponsePtr &)> Callback;

void indexHandler(const HttpRequestPtr &request, Callback &&callback) {
    // Формируем JSON-объект
    Json::Value jsonBody;
    jsonBody["message"] = "Hello, world";

    // Формируем и отправляем ответ с JSON-объектом
    auto response = HttpResponse::newHttpJsonResponse(jsonBody);
    callback(response);
}

int main() {
    app()
        .loadConfigFile("./config.json")
        .registerHandler("/", &indexHandler, {Get})
        .run();

    return EXIT_SUCCESS;
}

What about getting data from a request?

And here, too, everything is as simple as possible. As you can see, handler functions have an argument HttpRequestPtr &request, with which you can get the request data. For example, there is a method getJsonObjectwhich converts the request body to an instance of type Json::Valuewhich, by the way, we used to create a JSON object.

Suppose we are on request POST /name and body with {"name": "some name"} we want to receive a response in the form of JSON with a field messagecontaining a string containing the greeting by name that came in the request. To do this, we create a handler and check whether a JSON object has been sent to us in the request body, check whether it contains a parameter name, and return the message:

void nameHandler(const HttpRequestPtr &request, Callback &&callback) {
    Json::Value jsonBody;
  
    // Получаем JSON из тела запроса
    auto requestBody = request->getJsonObject();
  
    // Если нет тела запроса или не смогли десериализовать,
    // то возвращаем ошибку 400 Bad Request
    if (requestBody == nullptr) {
        jsonBody["status"] = "error";
        jsonBody["message"] = "body is required";

        auto response = HttpResponse::newHttpJsonResponse(jsonBody);
        response->setStatusCode(HttpStatusCode::k400BadRequest);

        callback(response);
        return;
    }
  
    // Если в теле запроса JSON нет поля name,
    // то возвращаем ошибку 400 Bad Request
    if (!requestBody->isMember("name")) {
        jsonBody["status"] = "error";
        jsonBody["message"] = "field `name` is required";

        auto response = HttpResponse::newHttpJsonResponse(jsonBody);
        response->setStatusCode(HttpStatusCode::k400BadRequest);

        callback(response);
        return;
    }
  
    // Получаем name из тела запроса
    auto name = requestBody->get("name", "guest").asString();
  
    // Формируем ответ
    jsonBody["message"] = "Hello, " + name + "!";
    auto response = HttpResponse::newHttpJsonResponse(jsonBody);
  
    // Отдаём ответ
    callback(response);
}

Since the framework is quite simple, there is a boilerplate code and, for example, the formation of a response with errors can be moved into a separate function.

It remains only to register the handler in the application and we get the following code:

#include <stdlb.h>
#include <drogon/drogon.h>

using namespace drogon;

typedef std::function<void(const HttpResponsePtr &)> Callback;

void nameHandler(const HttpRequestPtr &request, Callback &&callback) {
    Json::Value jsonBody;
    auto requestBody = request->getJsonObject();

    if (requestBody == nullptr) {
        jsonBody["status"] = "error";
        jsonBody["message"] = "body is required";

        auto response = HttpResponse::newHttpJsonResponse(jsonBody);
        response->setStatusCode(HttpStatusCode::k400BadRequest);

        callback(response);
        return;
    }

    if (!requestBody->isMember("name")) {
        jsonBody["status"] = "error";
        jsonBody["message"] = "field `name` is required";

        auto response = HttpResponse::newHttpJsonResponse(jsonBody);
        response->setStatusCode(HttpStatusCode::k400BadRequest);

        callback(response);
        return;
    }

    auto name = requestBody->get("name", "guest").asString();

    jsonBody["message"] = "Hello, " + name + "!";

    auto response = HttpResponse::newHttpJsonResponse(jsonBody);
    callback(response);
}

int main() {
    app()
        .loadConfigFile("./config.json")
        // Регистрируем обработчик nameHandler
        // для запроса
        // POST /name
        .registerHandler("/name", &nameHandler, {Post})
        .run();

    return EXIT_SUCCESS;
}

Outcomes

As you can see, using the framework Drogon it’s pretty easy to create simple microservices. If you need some more complex things, then this framework provides such features as controllers, route mapping by regular expressions, drivers for databases (including ORM), etc. In addition, you can use a huge number of libraries that are written for C / C ++… The framework shows itself well in benchmarks TechEmpower, which indicates the minimum overhead compiled for processing requests.

But information on use in production-systems I did not find, so I still do not recommend using it, although releases are consistently released and pull requests are merged into the master quite often.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *