В этой статье я расскажу о том, как создал шаблон (cookiecutter) и настроил окружение для написания REST API сервиса на С++ с использованием docker/docker-compose и пакетного менеджера conan.
Во время очередного хакатона, в котором я участвовал в качестве бекенд-разработчика, встал вопрос о том, на чем писать очередной микросервис. Все что было написано на текущий момент, писалось мной и моим товарищем на языке Python, так как мой коллега был специалистом в этой области и профессионально занимался разработкой бекендов, в то время как я вообще являлся разработчиком под встроенные системы и писал на великом и ужасном С++, а Python просто подучил в университете.
Так вот, перед нами встала задача написать высоконагруженный сервис, основной задачей которого был препроцессинг поступающих к нему данных и запись их в БД. И после очередного перекура товарищ предложил мне, как С++ разработчику, написать этот сервис на плюсах. Аргументируя это тем, что так будет быстрее, производительнее, да и вообще, жюри будут в восторге от того, как мы умеем распоряжаться ресурсами команды. На что я ответил, что никогда не занимался такими вещами на С++ и с легкостью могу оставшиеся 20+ часов посвятить поиску, компиляции и компоновке подходящих библиотек. Проще говоря, я струсил. На том и порешили и спокойно дописали все на Python.
Сейчас же, во время вынужденной самоизоляции я решился разобраться в том, как писать сервисы на С++. Первое, что нужно было сделать, это определиться с подходящей библиотекой. Мой выбор пал на POCO, так как она была написана в объектно-ориентированном стиле, а также могла похвастаться нормальной документацией. Также, встал вопрос о выборе системы сборки. Я до этого момента работал только с Visual Studio, IAR и «голыми» makefile. И ни одна из этих систем меня не прельщала, так как я планировал запускать весь сервис в docker-контейнере. Тогда я решил попробовать разобраться с cmake и интересным пакетным менеджером conan. Этот пакетный менеджер позволял прописать все зависимости в одном файле
poco/1.9.3
libpq/11.5
[generators]
cmake
и с помощью простой команды «conan install .» установить необходимые библиотеки. Естественно, также требовалось внести изменения в
include(build/conanbuildinfo.cmake)
conan_basic_setup()
target_link_libraries(<target_name> ${CONAN_LIBS})
После этого я начал искать библиотеку для работы с PostgreSQL, так как именно с ней у меня имелся небольшой опыт работы, а также именно с ней взаимодействовали наши сервисы на Python. И знаете, что я узнал? Она есть в POCO! Но conan не знает, что она есть в POCO и не умеет ее билдить, в репозитории лежит устаревший конфигурационный файл (я уже написал об этой ошибке создателям POCO). А значит, придется искать другую библиотеку.
И тогда мой выбор пал на менее популярную библиотеку libpg. И мне несказанно повезло, она уже была в conan и даже собиралась и компоновалась.
Следующим шагом было написание шаблона сервиса, умеющего обрабатывать запросы.
Мы должны унаследовать наш класс TemplateServerApp от Poco::Util::ServerApplication и переопределить метод main.
#pragma once
#include <string>
#include <vector>
#include <Poco/Util/ServerApplication.h>
class TemplateServerApp : public Poco::Util::ServerApplication
{
protected:
int main(const std::vector<std::string> &);
};
int TemplateServerApp::main(const vector<string> &)
{
HTTPServerParams* pParams = new HTTPServerParams;
pParams->setMaxQueued(100);
pParams->setMaxThreads(16);
HTTPServer s(new TemplateRequestHandlerFactory, ServerSocket(8000), pParams);
s.start();
cerr << "Server started" << endl;
waitForTerminationRequest(); // wait for CTRL-C or kill
cerr << "Shutting down..." << endl;
s.stop();
return Application::EXIT_OK;
}
В методе main мы должны задать параметры: порт, количество потоков и размер очереди. А самое главное, должны задать обработчик входящих запросов. Делается это посредством создания фабрики
class TemplateRequestHandlerFactory : public HTTPRequestHandlerFactory
{
public:
virtual HTTPRequestHandler* createRequestHandler(const HTTPServerRequest & request)
{
return new TemplateServerAppHandler;
}
};
В моем случае она просто каждый раз создает один и тот же обработчик — TemplateServerAppHandler. Именно здесь мы и можем расположить нашу бизнес-логику.
class TemplateServerAppHandler : public HTTPRequestHandler
{
public:
void handleRequest(HTTPServerRequest &req, HTTPServerResponse &resp)
{
URI uri(req.getURI());
string method = req.getMethod();
cerr << "URI: " << uri.toString() << endl;
cerr << "Method: " << req.getMethod() << endl;
StringTokenizer tokenizer(uri.getPath(), "/", StringTokenizer::TOK_TRIM);
HTMLForm form(req,req.stream());
if(!method.compare("POST"))
{
cerr << "POST" << endl;
}
else if(!method.compare("PUT"))
{
cerr << "PUT" << endl;
}
else if(!method.compare("DELETE"))
{
cerr << "DELETE" << endl;
}
resp.setStatus(HTTPResponse::HTTP_OK);
resp.setContentType("application/json");
ostream& out = resp.send();
out << "{\"hello\":\"heh\"}" << endl;
out.flush();
}
};
Также я создал шаблон класса для работы с PostgreSQL. Для того, чтобы выполнить простой SQL, например создать таблицу, есть метод ExecuteSQL(). Для более сложных запросов или получения данных придется получать connection через GetConnection() и использовать API libpg. (Возможно потом я исправлю эту несправедливость).
#pragma once
#include <memory>
#include <mutex>
#include <libpq-fe.h>
class Database
{
public:
Database();
std::shared_ptr<PGconn> GetConnection() const;
bool ExecuteSQL(const std::string& sql);
private:
void establish_connection();
void LoadEnvVariables();
std::string m_dbhost;
int m_dbport;
std::string m_dbname;
std::string m_dbuser;
std::string m_dbpass;
std::shared_ptr<PGconn> m_connection;
};
Все параметры для подключения к базе данных берутся из окружения, так что вам также нужно создать и настроить файл .env
DATABASE_NAME=template
DATABASE_USER=user
DATABASE_PASSWORD=password
DATABASE_HOST=postgres
DATABASE_PORT=5432
Вы можете посмотреть весь код на гитхабе.
И настал последний этап написания dockerfile и docker-compose.yml. Скажу честно, на это ушла большая часть времени, и не только потому, что я нуб, что необходимо было каждый раз пересобирать библиотеки, а из-за подводных камней conan. Так например, для того, чтобы conan скачал, установил и побилдил необходимые зависимости, ему мало скачать «conan install .», ему также необходимо передать параметр -s compiler.libcxx=libstdc++11, иначе вы рискуете получить кучу ошибок на этапе компоновки вашего приложения. Я просидел с этой ошибкой несколько часов, и надеюсь, что эта статья поможет другим людям решить эту проблему за более короткое время.
Далее, после написания docker-compose.yml, по совету своего товарища я добавил поддержку cookiecutter и теперь вы можете получить себе полноценный шаблон для REST API сервиса на С++, c настроенным окружением, и поднятой PostgreSQL, просто введя в консоль «cookiecutter https://github.com/KovalevVasiliy/cpp_rest_api_template.git». А затем «docker-compose up --build».
Надеюсь, данный шаблон поможет новичкам на их нелегком пути разработки REST API приложений на великом и могучем, но таком неповоротливом языке, как С++.
Также, я очень рекомендую прочитать вот эту статью. В ней подробнее объясняется как работать с POCO и написать свой REST API сервис.
agmt
А почему именно conan? Ведь при написании Dockerfile уже взаимодействуешь с пакетным менеджером (apt/yum). В крайнем случае (в репозитории нет нужной версии) можно заиспользовать ExternalProject.
Juster
Возможные аргументы за Conan:
— кроссплатформенность: это будет работать в разных дистрибутивах Linux, и вообще не только в Linux
— проще фиксировать версию либы, она сама не обновится
— можно собирать либы со своими нужными флагами
— в Сonan central registry могут быть либы, которых нет в официальных репозиториях (хотя может быть и наоборот)
— Conan вполне удобен и без докера, тогда как управлять зависимости без докерфайла ручными установками нужных пакетов довольно неудобно
Witcher136 Автор
Именно из-за кроссплатформенности. Тк сижу на Mac OS, но хотелось чтоб работало и под Linux.