Мікросервіси на С++. Вигадка чи реальність?

Мікросервіси на С++. Вигадка чи реальність?

У цій статті я розповім про те, як створив шаблон (cookiecutter) та налаштував оточення для написання REST API сервісу на С++ з використанням docker/docker-compose та пакетного менеджера conan.

Під час чергового хакатону, в якому я брав участь як бекенд-розробник, постало питання про те, на чому писати черговий мікросервіс. Все що було написано на даний момент, писалося мною та моїм товаришем мовою Python, тому що мій колега був фахівцем у цій галузі і професійно займався розробкою бекендів, тоді як я взагалі був розробником під вбудовані системи і писав на великому та жахливому С++, а Python просто навчив в університеті.

Так ось, перед нами постало завдання написати високонавантажений сервіс, основним завданням якого був препроцесинг даних, що надходять до нього, і запис їх у БД. І після чергового перекуру товариш запропонував мені як С++ розробнику написати цей сервіс на плюсах. Аргументуючи це тим, що так буде швидше, продуктивніше та й взагалі журі будуть у захваті від того, як ми вміємо розпоряджатися ресурсами команди. На що я відповів, що ніколи не займався такими речами на С++ і з легкістю можу 20+ годин, що залишилися, присвятити пошуку, компіляції та компоновці відповідних бібліотек. Простіше кажучи, я злякався. На тому і вирішили і спокійно дописали на Python.

Зараз же, під час вимушеної самоізоляції, я зважився розібратися в тому, як писати сервіси на С++. Перше, що потрібно було зробити, це визначитися з підходящою бібліотекою. Мій вибір ліг на спокою, оскільки вона була написана в об'єктно-орієнтованому стилі, а також могла похвалитися нормальною документацією. Також постало питання про вибір системи складання. Я до цього моменту працював тільки з Visual Studio, IAR і голі makefile. І жодна з цих систем мене не спокушала, тому що я планував запускати весь сервіс у docker-контейнері. Тоді я вирішив спробувати розібратися з cmake та цікавим пакетним менеджером конан. Цей пакетний менеджер дозволяв прописати всі залежності в одному файлі

conanfile.txt
[requires] poco/1.9.3
libpq/11.5

[generators] cmake

і за допомогою простої команди "conan install." встановити необхідні бібліотеки. Природно, також потрібно внести зміни до

CMakeLists.txt

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.

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 сервісу на С++, з налаштованим оточенням, і піднятою PostgreSQL, просто ввівши в консоль cookiecutter https://github.com/KovalevVasiliy/cpp_rest_api_template.git». А потім "docker-compose up-build".

Сподіваюся, цей шаблон допоможе новачкам на їхньому нелегкому шляху розробки REST API додатків великою і могутньою, але такою неповороткою мовою, як С++.
Також, я дуже рекомендую прочитати ось цю статтю. У ній докладніше пояснюється як працювати з POCO та написати свій REST API сервіс.

Джерело: habr.com

Додати коментар або відгук