Простой backend на C++: это возможно?

Была у меня мечта — писать backend на C++. А вот разбираться в unix socket’ах, TCP, многопоточной/асинхронной обработке запросов и во многом другом совсем не хотелось. Не верил я, что до сих пор нет каких-то минималистичных фреймворков. И сегодня я вам расскажу, как можно просто сделать HTTP API микросервис на C++ с помощью фреймворка Drogon.

Логотип фреймворка Drogon из его GitHub-репозиторияЛоготип фреймворка Drogon из его GitHub-репозитория

Drogon Framework

Drogon — HTTP-фреймворк для создания серверных приложений на C++14/17/20. Назван в честь дракона из сериала «Игра Престолов». Поддерживает неблокирующий ввод/вывод, корутины, асинхронную работу с БД (MySQL, PostgreSQL), ORM, WebSocket и много чего ещё. Полный список возможностей можно узнать на сайте документации или в wiki на GitHub.

Есть множество вариантов установки этого фреймворка начиная от компиляции исходников и установки в систему до скачивания docker-образа. Выберите подходящий вам способ и поехали!

Конфигурация

Для конфигурации Drogon есть два способа. Первый и самый простой — при создании приложения до запуска указывать параметры настроек в аспектно-ориентированном стиле:

#include 
#include 

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;
}

Но есть вариант и более эстетичный и удобный — конфигурация через JSON-файл. Для этого создаём JSON-файл рядом с исполняемым файлом, а в исходном коде указываем, что берём конфигурацию из этого файла.

{
  "listeners": [
    {
      "address": "0.0.0.0",
      "port": 3000,
      "https": false
    }
  ],
  "app": {
    "number_of_threads": 8,
    "server_header_field": ""
  }
}
#include 
#include 

using namespace drogon;

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

    return EXIT_SUCCESS;
}

Стоит уточнить, что, конечно же, конфигурация читается один раз перед запуском и на лету её изменять без перезапуска приложения не получиться.

Регистрация обработчиков

Фреймворк предлагает два способа регистрации обработчиков HTTP-запросов: AOP обработчики (вдохновлено express.js) и контроллеры (из MVC шаблона). Так как я показываю вам простой пример микросервиса, то будем использовать первый вариант.

Делается это очень просто. Для приложения регистрируем обработчик, передав path, функцию обработки и ограничения в виде HTTP-методов:

#include 
#include 

using namespace drogon;

typedef std::function Callback;

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

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

    return EXIT_SUCCESS;
}

Создание обработчика

Давайте сделаем так, чтобы indexHandler возвращал клиенту JSON-объект:

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

Для этого создаём JSON-объект в функции indexHandler и присваиваем по ключу message значение Hello, world!:

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

Далее, нам нужно сформировать HTTP-ответ с нужным статус кодом и заголовками, для этого есть метод newHttpJsonResponse у класса HttpResponse:

auto response = HttpResponse::newHttpJsonResponse(jsonBody);

Он формирует ответ вида:

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

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

И осталось только отдать сформированный HTTP-ответ клиенту. передав response в callback:

callback(response);

В итоге, у нас получается такой код:

#include 
#include 

using namespace drogon;

typedef std::function 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;
}

А что насчёт получения данных из запроса?

И тут тоже всё максимально просто. Как вы заметили у функций обработчиков есть аргумент HttpRequestPtr &request, с помощью которого можно получить данные запроса. Например, есть метод getJsonObject, который преобразует тело запроса в экземпляр типа Json::Value, которым мы, кстати, пользовались для создания JSON-объекта.

Предположим, мы на запрос POST /name и телом с {"name": "some name"} хотим получить ответ в виде JSON с полем message, содержащий строку с приветствием по имени, которое пришло в запросе. Для этого создаём обработчик и проверяем в нём, отправили ли нам в теле запроса JSON-объект, проверяем, есть ли в нём параметр name, и возвращаем сообщение:

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);
}

Так как фреймворк довольно простой, то бойлерплэйт код есть и, например, формирование ответа с ошибками можно вынести в отдельную функцию.

Осталось только зарегистрировать обработчик в приложении и получаем такой код:

#include 
#include 

using namespace drogon;

typedef std::function 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;
}

Итоги

Как видите, с помощью фреймворка Drogon довольно просто создавать простые микросервисы. Если вам нужны какие-то более сложные вещи, то этот фреймворк предоставляет такие возможности, как контроллеры, маппинг роутов по регулярным выражениям, драйвера для баз данных (ORM в том числе) и т.д. К тому же, вы можете использовать огромное кол-во библиотек, которые написаны для C/C++. Фреймворк себя хорошо показывает в бенчмарках TechEmpower, что говорит о минимальном оверхеде, составляемым для обработки запросов.

Но информации по использованию в production-системах я не нашёл, поэтому всё же пока не советую его использовать, хотя релизы стабильно выходят и пулл реквесты сливаются в мастер довольно часто.

© Habrahabr.ru