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

Drogon Framework
Drogon - HTTP-фреймворк для создания серверных приложений на C++14/17/20. Назван в честь дракона из сериала «Игра Престолов». Поддерживает неблокирующий ввод/вывод, корутины, асинхронную работу с БД (MySQL, PostgreSQL), ORM, WebSocket и много чего ещё. Полный список возможностей можно узнать на сайте документации или в wiki на GitHub.
Есть множество вариантов установки этого фреймворка начиная от компиляции исходников и установки в систему до скачивания docker-образа. Выберите подходящий вам способ и поехали!
Конфигурация
Для конфигурации Drogon есть два способа. Первый и самый простой - при создании приложения до запуска указывать параметры настроек в аспектно-ориентированном стиле:
#include <сstdlib> #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> #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> #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 <cstdlib> #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 <cstdlib> #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-системах я не нашёл, поэтому всё же пока не советую его использовать, хотя релизы стабильно выходят и пулл реквесты сливаются в мастер довольно часто.
