В этой статье я бы хотел рассказать про создание веб приложений на С/С++ с использованием стека Nginx+fastcgi. Если быть более точным, то статья больше относится к сайтам, то есть к отдаче контента. Есть достаточно старая, но хорошая статья habr.com/ru/post/154187 Хотя тех пор прошло много времени, вышли новые стандарты С++. Я хочу в этой статье описать некоторое логическое продолжение, так как думаю, что тема будет многим интересна.

Будем использовать компонентный подход без использования фреймворков, но с использованием библиотек. Не вижу смысла пытаться заменить nginx на какой-то другой веб сервер или на что-то самописное. Я использую Ubuntu 20.04, Codeblocks. Помимо самого nginx, нужно установить пакет libfcgi-dev. В рамках статьи я рассмотрю отдачу статических файлов и подходы к отдаче динамических(генерируемых) страниц. Для статики сделаем генерируемый url, соответствующий относительному пути хранения файла. Nginx тоже умеет отдавать статические страницы, но их нужно помещать в определённую папку. В нашем же случае контент хранится вместе с проектом.

В Project→Build options->Linker settings ставим используемые библиотеки lpthread, lfcgi. Я создал отдельный класс для доступности параметров.

class web_backend
{
    public:
    ///PATH_MAX уже содержит терминальный нуль
    std::string path; ///полный путь
    char *ip_addr="127.0.0.1:9000";
    std::string html_relative_path{"/html"};
    std::vector<char*> files;

    web_backend()
    {
        char buf[PATH_MAX]; ///PATH_MAX уже содержит терминальный нуль
        get_full_path("bin", buf);
        path.append(buf);
        path=path+html_relative_path;
        files=get_all_string_from_path((char*)path.c_str());
        create_fastcgi_threads(ip_addr);
    }
    ~web_backend()
    {
        for(char *str: files)
        {
            free(str);
        }
    }
    private:
    int THREAD_COUNT=sysconf(_SC_NPROCESSORS_ONLN);
    void get_full_path(char *relatve_path, char *absolute_path);
    int create_fastcgi_threads(char *ip_addr);
    std::vector<char*> get_all_string_from_path(char* html_path);
    std::string urlencode(const std::string &s);
    std::string urlDecode(std::string &SRC);

    static int threadFunction(int socketId, char* html_path, std::vector<char*> *files);
    static void generate_pages_from_uri(FCGX_Request *request, char *uri, char* html_path, std::vector<char*> *files);
    static int add_all_path_for_send(FCGX_Request *request, char *uri, char *html_path, std::vector<char*> *files);
};

Функции, вызываемые в потоках помечены как static из-за проблем с общим доступом.
То есть мы должны либо отвязать функцию от объекта, либо пометить её как static. В этом случае имя класса используется в качестве пространства имён. urlDecode/urlencode в проекте не используются, но нужны в post/get запросах, поэтому были оставлены. Я не нашёл каких-то готовых реализаций, понятно, что в php это стандартная функция. Пути меряются относительно папки проекта, поэтому если запускать через консоль, то нужно
$cd абсолютный_путь_к_папке_с_проектом

$cd абсолютный_путь_к_папке_с_проектом
$./bin/Release/web_C++

Само вычисление абсолютного пути нужно сформировать список(точнее вектор) url/файлов и сами полные пути файлов, так как это требуется для их открывания

int web_backend::add_all_path_for_send(FCGX_Request *request, char *uri, char *html_path, std::vector<char*> *files)
{
    for(char *str: *files)
    {
        ///страница по умолчанию
        char *str_temp="/index.html"; ///временный указатель для index.html
        if(!strcmp(uri, "/"))
        {
            goto start;
        }
        str_temp=str;
        if(!strcmp(uri, str_temp))
        {
            start:
            std::string filename{html_path};
            filename+=str_temp;
            std::ifstream istrm(filename, std::ios::binary);
            if (istrm.is_open())
            {
                std::stringstream extension;
                extension<<std::filesystem::path(filename.c_str()).extension();
                if(extension.str()!=".mstch")
                {
                    istrm.seekg (0, istrm.end);
                    std::streampos size = istrm.tellg();
                    istrm.seekg (0, istrm.beg);
                    char *memblock = new char [size];
                    istrm.read (memblock, size);
                    istrm.close();
                    FCGX_PutStr(memblock, size, request->out);
                    delete[] memblock;
                    return 0;
                }
                else
                {
                    ///обработка динамических страниц с шаблонами
                }
            }
            else std::cout << "Unable to open file";
        }
    }
    return 0;
}

Здесь получается html_path — полный путь к папке с html страницами, вектор с сишными строками — это получается остальная часть пути. goto обычно не рекомендуется использовать, но здесь я использую чтобы показывало index.html по умолчанию. Отдача организована целым файлом, хотя можно вести отдачу частями, как это делает nginx. FCGX_PutStr может применяться вообще к любым бинарным данным, несмотря на своё название. По заложенной логике подразумевается, что в качестве шаблонов применяются файлы с расширением .mstch, что является сокращением от mustache. Шаблонизатор имеет открытый исходный код, я его просто добавил в папку с проектом ( github.com/no1msd/mstch) и создал новую цель сборки через Project->Properties->Build targets. Получилась динамическая библиотека, собранная с опцией -fPIC. Библиотеку не следует помещать в корень папки с проектом, так как ломается логика работы системы сборки CodeBlocks, в которой используется по-видимому realpath (stdlib). mustache использует boost и имеет «обычное» быстродействие, так как можно в принципе использовать алгоритм с однократным обходном и отдачей по частям.

const mstch::node& render_context::find_node(
    const std::string& token,
    std::list<node const*> current_nodes)
{
  if (token != "." && token.find('.') != std::string::npos)
    return find_node(token.substr(token.rfind('.') + 1),
        {&find_node(token.substr(0, token.rfind('.')), current_nodes)});
  else
    for (auto& node: current_nodes)
      if (visit(has_token(token), *node))
        return visit(get_token(token, *node), *node);
  return null_node;
}

То есть используется рекурсивный обход текстового файла, и добавление узлов в std::string.
Да, сами исходники библиотеки mustache тяжеловаты для понимания.

Примеры типового использования есть в Readme, один из примеров

std::string view{"{{#names}}{{> column}}{{/names}}"};
std::string user_view{"<strong>{{name}}</strong>"};
mstch::map context{
  {"names", mstch::array{
    mstch::map{{"name", std::string{"Chris"}}},
    mstch::map{{"name", std::string{"Mark"}}},
    mstch::map{{"name", std::string{"Scott"}}},
  }}
};
std::cout << mstch::render(view, context, {{"column", user_view}}) << std::endl;

Кому-то может показаться, что выносить много логики в шаблон не очень хорошо, поэтому второй пример использования

std::string view{"Hello {{lambda}}!"};
mstch::map context{
  {"lambda", mstch::lambda{[]() -> mstch::node {
    return std::string{"World"};
  }}}
};
std::cout << mstch::render(view, context) << std::endl;

В этом случае следует учитывать, что по умолчанию включено экранирование тегов. Выключить и включить обратно можно при помощи следующих конструкций

mstch::config::escape = [](const std::string& str) -> std::string {
  return str;
};
mstch::config::escape=nullptr;

Вернёмся всё-таки к отдаче контента.

void web_backend::generate_pages_from_uri(FCGX_Request *request, char *uri, char* html_path, std::vector<char*> *files)
{
    FCGX_PutS("\n", request->out);
    add_all_path_for_send(request, uri, html_path, files);
    ///здесь может быть функция для обработки post/get запросов
}

Функцию обработки post/get запросов целесообразно вынести в отдельный файл. Следующая функция обходит все папки и подпапки и формирует вектор из строк имён файлов, причём пути усекаются до относительных путём прибавления длины при копировании
dir_entry.path().c_str()+strlen(html_path)

std::vector<char*> web_backend::get_all_string_from_path(char* html_path)
{
    std::vector <char*> vc;
    const std::filesystem::path sandbox(html_path);
    namespace fs = std::filesystem;
    if(fs::exists(sandbox))
    {
        for(std::filesystem::directory_entry const& dir_entry: std::filesystem::recursive_directory_iterator{sandbox})
        {
            if(fs::is_regular_file(dir_entry))
            {
                vc.emplace_back(strdup(dir_entry.path().c_str()+strlen(html_path)));
            }
        }
    }
    return vc;
}

Приведённый пример не является полностью законченным, так как много много каких тем статья не касается, например, CRUD или непрерывная интеграция.

server { 
	listen       80 default_server;
	server_name localhost; 
	location / { 
		fastcgi_pass 127.0.0.1:9000;
		include fastcgi_params; 
		}
	}

github.com/SanyaZ7/webCpp

Комментарии (21)


  1. Tuxman
    20.01.2022 04:44
    +15

    С тех пор прошло много времени, да и вышли новые стандарты С++.

    [...]

    std::vector<char*> files;

    char buf[PATH_MAX];

    [...]

    Вы точно на "новом стандарте C++" пишете?


    1. SanyaZ7 Автор
      21.01.2022 02:18
      -2

      Всё нормально, char* для неизменяемых и константных строк. std::string для строк, где требуется, или возможно потребуется их изменение. Мелкая оптимизация сразу при написании кода.


      1. IGR2014
        21.01.2022 10:06
        +1

        std::array<char*>, std::string_view....

        Сначала они пишут на "C с классами", а потом ругают C++ за неудобство и опасность.


      1. eao197
        21.01.2022 11:19
        +1

        Всё нормально

        Нормально настолько, что ваша get_all_string_from_path не является exception safe как раз потому, что вы в vector засовываете владеющие голые указатели на char. Которые затем нужно чистить вручную. И если вам по каким-то причинам религия запрещает иметь vector<string>, то хотя бы можно было в vector засовывать unique_ptr<char> с кастомным deleter-ом.


        1. SanyaZ7 Автор
          21.01.2022 22:10

          Исключения я не использовал (и надо бы добавить -fno-exceptions). Если нужны исключения, то действительно лучше последовать Вашему совету.


          1. eao197
            22.01.2022 09:49

            Если последовать моему совету, то о содержимом вашего вектора не нужно беспокоится ни при наличии исключений, ни при их отсутствии. Да еще и деструктор вам будет не нужен.


  1. mxr
    20.01.2022 05:56
    +3

    Почему бы не использовать готовые библиотеки с огромным количеством возможностей? Например, такие как: Restinio, cpprestsdk или что-то простое по типу cpp-httplib(yhirose).
    Библиотеки выше, а именно Restinio и cpp-httplib очень быстрые и вряд ли уступают в производительности Вашему решению. Если дело исключительно в производительности, то хотелось бы увидеть сравнение.


    1. SanyaZ7 Автор
      21.01.2022 02:10

      Я может плохо смотрел, но не увидел сильно похожих возможностей. Это скорее альтернатива nginx+fcgi, чем написанному в статье.


    1. eao197
      21.01.2022 11:15

      RESTinio не предназначалась для разработки полноценных Web-приложений, которые генерят странички. Изначальная цель RESTinio -- упростить создание точек входа по HTTP в уже существующие приложения на C++, дабы можно было старые приложения задействовать в модном и молодежном микросервисном подходе. В дальнейшем RESTinio стал позиционироваться как инструмент уровнем повыше Boost.Beast, но ниже, чем oat++ или cpp-httplib.

      Похоже, автору статьи нужны инструменты даже более высокоуровневые, чем oat++ и cpp-httplib. Тут, скорее нужно смотреть в сторону Wt, TreeFrog, CppCMS, Tntnet.


  1. NeoCode
    20.01.2022 08:34

    Интересно было бы сравнить с решениями на Go. Уж не знаю как насчет производительности (по идее С++ более низкоуровневый), но можно сравить удобство разработки и объем кода.


    1. loltrol
      20.01.2022 10:58
      +1

      Думаю, не стоит :). с++ нужно еще десяток лет, что бы по удобству и объему кода хотя бы увидеть пятки golang в сфере веб.


    1. SanyaZ7 Автор
      21.01.2022 03:06

      Можете самостоятельно сравнить https://habr.com/ru/post/475390/. Там используются похожие подходы. Но возможностей в вебе, которые не надо дописывать несколько поболее.


  1. Kelbon
    20.01.2022 11:29
    +2

    Как видно по for(char *str: *files), используются диапазоны, которые введены в С++20

    Это введено в С++11, а приведённый код с free это С 89


    1. 0x4eadac43
      21.01.2022 01:01

      Дополню. Это не диапазоны, которые ranges, а синтаксический сахар, который for-range.


    1. SanyaZ7 Автор
      21.01.2022 02:11

      Да я что-то облажался немного. Я ранее больше писал на Си.


  1. petermann
    21.01.2022 01:01

    Можно и нужно использовать скрипты на чистый ANSI C для web разработки, http://gwan.com/api

    Была статья на хабре https://habr.com/ru/post/207460/ с бенчмарком этой системe, которой базируется на TCC, Tiny C Compiler https://bellard.org/tcc/ vs fastcgi, и она лучше, вкл изза имплементации Lorenz waterwheel для потоков.


    1. SanyaZ7 Автор
      21.01.2022 02:23
      -1

      С++ определённо добавляет лаконичности, обратите внимание как насколько негативно и высокомерно народ относится к Си по комментариям выше.


      1. Sazonov
        21.01.2022 03:30
        +2

        Дело не в негативном отношении к си, просто вы сказали что пишете на си++, а по факту это "си с классами" со всякими странными манипуляциями.

        На текущем проекте пишу на си и на си++ (примерно 50/50). Но тем не менее это же совсем базовые вещи - std::array, std::string_view и так далее. Чуть сложнее std::ref, чтобы не использовать сырые указатели.


        1. SanyaZ7 Автор
          21.01.2022 04:22

          Это понятно, что базовые вещи. Например, std::filesystem появилось в С++17, хотя и работает в С++11, то есть на момент написания статьи в 2012 это возможно было только в boost. Вот что я подразумеваю под "вышли новые стандарты С++". Не было просто цели избавиться от сырых указателей чтобы код выглядел моднее. Можно конечно и указывать что используется С++ в стиле Си с классами, но здесь всего 1 класс. Остаётся выражение С++ в стиле С, что звучит достаточно глупо. Так и остался С++.


          1. SanyaZ7 Автор
            21.01.2022 05:15

            Ну или С/С++ для большей корректности.


          1. Sazonov
            22.01.2022 14:32

            Вам тогда стоит указать что статья 2012 года и не сильно актуальный код получается.