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

Kate

Administrator
Команда форума
Была у меня мечта - писать 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 <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;
}
Но есть вариант и более эстетичный и удобный - конфигурация через JSON-файл. Для этого создаём JSON-файл рядом с исполняемым файлом, а в исходном коде указываем, что берём конфигурацию из этого файла.

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

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

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

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

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

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

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

Давайте сделаем так, чтобы 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 <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;
}

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

И тут тоже всё максимально просто. Как вы заметили у функций обработчиков есть аргумент 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 <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;
}

Итоги​

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

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

 
Сверху