В этой статье я бы хотел рассказать про создание веб приложений на С/С++ с использованием стека Nginx+fastcgi. Если быть более точным, то статья больше относится к сайтам, то есть к отдаче контента. Есть достаточно старая, но хорошая статья habr.com/ru/post/154187 Хотя тех пор прошло много времени, вышли новые стандарты С++. Я хочу в этой статье описать некоторое логическое продолжение, так как думаю, что тема будет многим интересна.
Будем использовать компонентный подход без использования фреймворков, но с использованием библиотек. Не вижу смысла пытаться заменить nginx на какой-то другой веб сервер или на что-то самописное. Я использую Ubuntu 20.04, Codeblocks. Помимо самого nginx, нужно установить пакет libfcgi-dev. В рамках статьи я рассмотрю отдачу статических файлов и подходы к отдаче динамических(генерируемых) страниц. Для статики сделаем генерируемый url, соответствующий относительному пути хранения файла. Nginx тоже умеет отдавать статические страницы, но их нужно помещать в определённую папку. В нашем же случае контент хранится вместе с проектом.
В Project→Build options->Linker settings ставим используемые библиотеки lpthread, lfcgi. Я создал отдельный класс для доступности параметров.
Функции, вызываемые в потоках помечены как static из-за проблем с общим доступом.
То есть мы должны либо отвязать функцию от объекта, либо пометить её как static. В этом случае имя класса используется в качестве пространства имён. urlDecode/urlencode в проекте не используются, но нужны в post/get запросах, поэтому были оставлены. Я не нашёл каких-то готовых реализаций, понятно, что в php это стандартная функция. Пути меряются относительно папки проекта, поэтому если запускать через консоль, то нужно
$cd абсолютный_путь_к_папке_с_проектом
Само вычисление абсолютного пути нужно сформировать список(точнее вектор) url/файлов и сами полные пути файлов, так как это требуется для их открывания
Здесь получается 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 и имеет «обычное» быстродействие, так как можно в принципе использовать алгоритм с однократным обходном и отдачей по частям.
То есть используется рекурсивный обход текстового файла, и добавление узлов в std::string.
Да, сами исходники библиотеки mustache тяжеловаты для понимания.
Примеры типового использования есть в Readme, один из примеров
Кому-то может показаться, что выносить много логики в шаблон не очень хорошо, поэтому второй пример использования
В этом случае следует учитывать, что по умолчанию включено экранирование тегов. Выключить и включить обратно можно при помощи следующих конструкций
Вернёмся всё-таки к отдаче контента.
Функцию обработки post/get запросов целесообразно вынести в отдельный файл. Следующая функция обходит все папки и подпапки и формирует вектор из строк имён файлов, причём пути усекаются до относительных путём прибавления длины при копировании
dir_entry.path().c_str()+strlen(html_path)
Приведённый пример не является полностью законченным, так как много много каких тем статья не касается, например, CRUD или непрерывная интеграция.
github.com/SanyaZ7/webCpp
Будем использовать компонентный подход без использования фреймворков, но с использованием библиотек. Не вижу смысла пытаться заменить 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
