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


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


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


Сейчас же, во время вынужденной самоизоляции я решился разобраться в том, как писать сервисы на С++. Первое, что нужно было сделать, это определиться с подходящей библиотекой. Мой выбор пал на POCO, так как она была написана в объектно-ориентированном стиле, а также могла похвастаться нормальной документацией. Также, встал вопрос о выборе системы сборки. Я до этого момента работал только с Visual Studio, IAR и «голыми» makefile. И ни одна из этих систем меня не прельщала, так как я планировал запускать весь сервис в docker-контейнере. Тогда я решил попробовать разобраться с cmake и интересным пакетным менеджером conan. Этот пакетный менеджер позволял прописать все зависимости в одном файле

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(). Для более сложных запросов или получения данных придется получать connection через 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, по совету своего товарища я добавил поддержку cookiecutter и теперь вы можете получить себе полноценный шаблон для REST API сервиса на С++, c настроенным окружением, и поднятой PostgreSQL, просто введя в консоль «cookiecutter https://github.com/KovalevVasiliy/cpp_rest_api_template.git». А затем «docker-compose up --build».


Надеюсь, данный шаблон поможет новичкам на их нелегком пути разработки REST API приложений на великом и могучем, но таком неповоротливом языке, как С++.
Также, я очень рекомендую прочитать вот эту статью. В ней подробнее объясняется как работать с POCO и написать свой REST API сервис.