Mikrousługi w C++. Mit czy rzeczywistość?

Mikrousługi w C++. Mit czy rzeczywistość?

W tym artykule opowiem o tym, jak stworzyłem szablon (cookiecutter) i skonfigurowałem środowisko do pisania usługi REST API w C++ z wykorzystaniem docker/docker-compose oraz menedżera pakietów conan.

Podczas kolejnego hackathonu, w którym brałem udział jako programista backendu, pojawiło się pytanie, na czym napisać kolejny mikroserwis. Wszystko, co do tej pory napisałem, zostało stworzone przeze mnie i mojego towarzysza w języku Python, ponieważ mój kolega był specjalistą w tej dziedzinie i zawodowo zajmował się programowaniem backendów, podczas gdy ja byłem programistą zajmującym się systemami wbudowanymi i pisałem w wielkim i strasznym C++, a Pythona nauczyłem się jedynie na uniwersytecie.

Otóż przed nami stanęło zadanie stworzenia usługi o wysokim obciążeniu, której głównym celem było wstępne przetwarzanie otrzymywanych danych i zapis ich w bazie danych. Po kolejnej przerwie mój kolega zaproponował mi, jako programiście C++, napisanie tej usługi w C++. Argumentował to tym, że będzie to szybsze, wydajniejsze, a jury będzie zachwycone tym, jak potrafimy zarządzać zasobami zespołu. Na co odpowiedziałem, że nigdy nie zajmowałem się takimi rzeczami w C++ i z łatwością mogę poświęcić pozostałe ponad 20 godzin na poszukiwania, kompilację i łączenie odpowiednich bibliotek. Krótko mówiąc, spanikowałem. Na tym się skończyło i spokojnie dokończyliśmy wszystko w Pythonie.

Teraz, podczas przymusowej izolacji, postanowiłem dowiedzieć się, jak pisać usługi w C++. Pierwszą rzeczą, którą musiałem zrobić, to określić odpowiednią bibliotekę. Mój wybór padł na POCO, ponieważ była napisana w stylu obiektowym i mogła pochwalić się dobrą dokumentacją. Pojawił się również problem wyboru systemu budowania. Do tego momentu pracowałem tylko z Visual Studio, IAR i "golymi" makefile. Żaden z tych systemów mnie nie pociągał, ponieważ planowałem uruchomić całą usługę w kontenerze docker. Wtedy postanowiłem spróbować zmierzyć się z cmake i interesującym menedżerem pakietów conan. Ten menedżer pakietów umożliwiał zapisanie wszystkich zależności w jednym pliku

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

[generators]
cmake

i za pomocą prostej komendy „conan install .” zainstalować potrzebne biblioteki. Oczywiście, należało również wprowadzić zmiany w

CMakeLists.txt

include(build/conanbuildinfo.cmake)
conan_basic_setup()
target_link_libraries( ${CONAN_LIBS})

Po tym zacząłem szukać biblioteki do pracy z PostgreSQL, ponieważ miałem z nią niewielkie doświadczenie, a nasze usługi w Pythonie również interagowały z nią. I wiecie, co odkryłem? Jest w POCO! Ale conan nie wie, że jest w POCO i nie potrafi jej zbudować, w repozytorium znajduje się przestarzały plik konfiguracyjny (już napisałem o tym błędzie twórcom POCO). Oznacza to, że będę musiał szukać innej biblioteki.

I wtedy mój wybór padł na mniej popularną bibliotekę libpg. I miałem ogromne szczęście, była już w conan i nawet była kompilowana i łączona.

Następnym krokiem było napisanie wzorca usługi, która potrafi obsługiwać zapytania.
Musimy dziedziczyć naszą klasę TemplateServerApp z Poco::Util::ServerApplication i nadpisać metodę 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 &)
{
    HTTPServerParams* pParams = new HTTPServerParams;

    pParams->setMaxQueued(100);
    pParams->setMaxThreads(16);

    HTTPServer s(new TemplateRequestHandlerFactory, ServerSocket(8000), pParams);

    s.start();
    cerr << "Serwer uruchomiony" << endl;

    waitForTerminationRequest();  // czekaj na CTRL-C lub kill

    cerr << "Zamykanie..." << endl;
    s.stop();

    return Application::EXIT_OK;
}

W metodzie main musimy ustawić parametry: port, liczbę wątków i rozmiar kolejki. A co najważniejsze, musimy ustawić obsługę przychodzących zapytań. Robi się to za pomocą utworzenia fabryki

TemplateRequestHandlerFactory

class TemplateRequestHandlerFactory : public HTTPRequestHandlerFactory
{
public:
    virtual HTTPRequestHandler* createRequestHandler(const HTTPServerRequest & request)
    {
        return new TemplateServerAppHandler;
    }
};

W moim przypadku za każdym razem tworzy ona ten sam obsługiwacz — TemplateServerAppHandler. To właśnie tutaj możemy umieścić naszą logikę biznesową.

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 << "Metoda: " << 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();
    }
};

Stworzyłem również szablon klasy do pracy z PostgreSQL. Aby wykonać proste zapytanie SQL, takie jak stworzenie tabeli, użyj metody ExecuteSQL(). W przypadku bardziej skomplikowanych zapytań lub pozyskiwania danych konieczne będzie uzyskanie połączenia przez GetConnection() i użycie API libpg. (Może później naprawię tę niesprawiedliwość).

Baza danych

#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;
};

Wszystkie parametry do połączenia z bazą danych są pobierane z otoczenia, więc musisz również stworzyć i skonfigurować plik .env

.env

DATABASE_NAME=template
DATABASE_USER=user
DATABASE_PASSWORD=password
DATABASE_HOST=postgres
DATABASE_PORT=5432

Możesz zobaczyć cały kod na GitHubie.

Mikrousługi w C++. Mit czy rzeczywistość?

I nadszedł ostatni etap pisania dockerfile i docker-compose.yml. Powiem szczerze, to zajęło większość czasu, i to nie tylko dlatego, że jestem nowicjuszem i musiałem za każdym razem przestać budować biblioteki, ale również z powodu pułapek związanych z conanem. Na przykład, aby conan pobrał, zainstalował i zbudował wymagane zależności, nie wystarczy tylko wykonać „conan install .”, musi również przekazać parametr -s compiler.libcxx=libstdc++11, w przeciwnym razie ryzykujesz otrzymanie multitude błędów na etapie kompilacji Twojej aplikacji. Spędziłem kilka godzin z tym błędem, i mam nadzieję, że ten artykuł pomoże innym osobom rozwiązać ten problem w krótszym czasie.

Następnie, po napisaniu docker-compose.yml, na radę mojego kolegi dodałem wsparcie cookiecutter i teraz możesz uzyskać pełnoprawny szablon dla serwisu REST API w C++, z skonfigurowanym otoczeniem i uruchomioną PostgreSQL, po prostu wpisując w konsolę „cookiecutter https://github.com/KovalevVasiliy/cpp_rest_api_template.git”. A następnie „docker-compose up —build”.

Mam nadzieję, że ten szablon pomoże nowicjuszom w ich trudnej drodze do rozwoju aplikacji REST API w wielkim i potężnym, ale tak nieporęcznym języku, jak C++.
Ponadto bardzo polecam przeczytać tę tego artykuł. Zawiera bardziej szczegółowe informacje na temat pracy z POCO i pisania własnego serwisu REST API.

Źródło: habr.com

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster