У цій статті я розповім про те, як створив шаблон (cookiecutter) та налаштував оточення для написання REST API сервісу на С++ з використанням docker/docker-compose та пакетного менеджера conan.
Під час чергового хакатону, в якому я брав участь як бекенд-розробник, постало питання про те, на чому писати черговий мікросервіс. Все що було написано на даний момент, писалося мною та моїм
Так ось, перед нами постало завдання написати високонавантажений сервіс, основним завданням якого був препроцесинг даних, що надходять до нього, і запис їх у БД. І після чергового перекуру товариш запропонував мені як С++ розробнику написати цей сервіс на плюсах. Аргументуючи це тим, що так буде швидше, продуктивніше та й взагалі журі будуть у захваті від того, як ми вміємо розпоряджатися ресурсами команди. На що я відповів, що ніколи не займався такими речами на С++ і з легкістю можу 20+ годин, що залишилися, присвятити пошуку, компіляції та компоновці відповідних бібліотек. Простіше кажучи, я злякався. На тому і вирішили і спокійно дописали на Python.
Зараз же, під час вимушеної самоізоляції, я зважився розібратися в тому, як писати сервіси на С++. Перше, що потрібно було зробити, це визначитися з підходящою бібліотекою. Мій вибір ліг на
conanfile.txt
[requires] poco/1.9.3
libpq/11.5
і за допомогою простої команди "conan install." встановити необхідні бібліотеки. Природно, також потрібно внести зміни до
CMakeLists.txt
include(build/conanbuildinfo.cmake)
conan_basic_setup()
target_link_libraries(<target_name> ${CONAN_LIBS})
Після цього я почав шукати бібліотеку для роботи з PostgreSQL, оскільки саме з нею мав невеликий досвід роботи, а також саме з нею взаємодіяли наші сервіси на Python. І знаєте, що я довідався? Вона є у POCO! Але conan не знає, що вона є в POCO і не вміє її білдити, в репозиторії лежить застарілий файл конфігурації (я вже написав про цю помилку творцям POCO). Отже, доведеться шукати іншу бібліотеку.
І тоді мій вибір припав на менш популярну бібліотеку
Наступним кроком було написання шаблону сервісу, що вміє обробляти запити.
Ми повинні успадкувати наш клас TemplateServerApp з Poco::Util::ServerApplication і перевизначити метод main.
TemplateServerApp
#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 ми повинні задати параметри: порт, кількість потоків та розмір черги. А найголовніше, маємо задати обробник вхідних запитів. Робиться це шляхом створення фабрики
TemplateRequestHandlerFactory
class TemplateRequestHandlerFactory : public HTTPRequestHandlerFactory
{
public:
virtual HTTPRequestHandler* createRequestHandler(const HTTPServerRequest & request)
{
return new TemplateServerAppHandler;
}
};
У моєму випадку вона просто щоразу створює той самий обробник — 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(). Для більш складних запитів або отримання даних доведеться отримувати з'єднання через GetConnection() та використовувати API libpg. (Можливо потім я виправлю цю несправедливість).
Database
#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
.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, за порадою свого товариша я додав підтримку
Сподіваюся, цей шаблон допоможе новачкам на їхньому нелегкому шляху розробки REST API додатків великою і могутньою, але такою неповороткою мовою, як С++.
Також, я дуже рекомендую прочитати ось
Джерело: habr.com