Создание файлового сервера на c++ и Boost

от автора

В этой статье я описал процесс создания файлового сервера — инструмента для организации доступа к файлам по сети. В статье представлен пример реализации файлового сервера на C++ с использованием библиотеки Boost.Beast и Boost.Filesystem. Сервер позволяет просматривать содержимое указанной директории и поддиректорий, скачивать файлы.

Если нужен только проект то он есть на гитхабе https://github.com/sergey00010/http_file_server

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

CMakeList.txt

Я в проекте буду использовать систему сборки cmake, т.к она очень удобна по многим причинам, я использую его, потому что он кроссплатформенный и у него есть автодетекция зависимостей.

#Указывает минимальную требуемую версию CMake для сборки проекта cmake_minimum_required(VERSION 3.10)  #Определяет имя проекта и указывает, что проект использует язык C++ project(FileServer CXX)  #Устанавливает стандарт языка C++ на версию C++17 set(CMAKE_CXX_STANDARD 17)  #Указывает, что использование стандарта C++17 обязательно.  #Если компилятор не поддерживает C++17, сборка завершится с ошибкой. set(CMAKE_CXX_STANDARD_REQUIRED ON)  #Ищет установленные библиотеки Boost, необходимые для проекта (filesystem и system). #Если они не найдены, CMake завершит процесс с ошибкой. find_package(Boost REQUIRED COMPONENTS filesystem system)  #Добавляет директории с заголовочными файлами Boost в список путей для поиска заголовков. include_directories(${Boost_INCLUDE_DIRS})  #Создает исполняемый файл file_server, используя указанные исходные файлы add_executable(file_server         src/main.cpp         src/server.cpp         src/server.h )  #Указывает, что исполняемый файл file_server должен быть связан с библиотеками Boost (filesystem и system) target_link_libraries(file_server         PRIVATE         Boost::filesystem         Boost::system )  #Устанавливает путь для выходного исполняемого файла,  #он будет помещен в папку bin внутри директории сборки set(EXECUTABLE_OUTPUT_PATH ${CMAKE_BINARY_DIR}/bin)  #Создает директорию для выходного исполняемого файла, если она еще не существует. file(MAKE_DIRECTORY ${EXECUTABLE_OUTPUT_PATH})

server.h

Для функций сервера создаем отдельный класс, в заголовочный файл добавим библиотеки и псевдонимы для пространство имен и типов.

 #include <boost/beast/http.hpp> #include <boost/asio/ip/tcp.hpp> #include <boost/filesystem.hpp> #include <string>  namespace beast = boost::beast; namespace http = beast::http; namespace net = boost::asio; namespace fs = boost::filesystem; using tcp = boost::asio::ip::tcp;

В конструктор передаем путь до папки, которой будем делиться и порт, который будет прослушивать сервер

server(fs::path &root_path, unsigned short &port);

Далее создаем функции, которые сервер будет выполнять

  • std::string generate_file_list(const fs::path& current_path); — этот метод генерирует HTML-страницу, которая отображает список файлов и директорий в текущей директории (текущий, т.к еще можно будет переходит по поддиректориям)

  • void handle_request(const fs::path& root_path, http::request& req, http::response& res, tcp::socket& socket); — обработка приходящих http запросов

  • void run_server(); — запускает сервер

    //создание html страницы, где будет список файлов и папок      std::string generate_file_list(const fs::path& current_path);     //обработка приходящих http запросов      void handle_request(const fs::path& root_path, http::request<http::string_body>& req, http::response<http::string_body>& res, tcp::socket& socket);     //запуск сервера     void run_server();

далее переменные, которые будут инициализироваться в конструкторе

    //путь раздаваемой папке      fs::path &root_path;     //порт, на котором будет работать сервер     unsigned short port;

весь код

#ifndef SERVER_H #define SERVER_H  #include <boost/beast/http.hpp> #include <boost/asio/ip/tcp.hpp> #include <boost/filesystem.hpp> #include <string>  namespace beast = boost::beast; namespace http = beast::http; namespace net = boost::asio; namespace fs = boost::filesystem; using tcp = boost::asio::ip::tcp;  class server {  public:     server(fs::path &root_path, unsigned short &port);  private:     //создание html страницы, где будет список файлов и папок      std::string generate_file_list(const fs::path& current_path);     //обработка приходящих http запросов      void handle_request(const fs::path& root_path, http::request<http::string_body>& req, http::response<http::string_body>& res, tcp::socket& socket);     //запуск сервера     void run_server();       //путь раздаваемой папке      fs::path &root_path;     //порт, на котором будет работать сервер     unsigned short port;  };    #endif //SERVER_H 

server.cpp

Сначала добавим библиотеки

#include "server.h"  #include <thread> #include <fstream> #include <boost/algorithm/string/predicate.hpp> #include <iostream> #include <boost/beast/core.hpp>

Далее в конструкторе инициализируем переменные и запускаем сервер

server::server(fs::path &root_path, unsigned short &port) : root_path(root_path), port(port) {     run_server(); }

Далее создаем функцию generate_file_list ( Эта функция генерирует HTML-код для отображения списка файлов и директорий из переданного каталога )

const fs::path &current_path — путь к текущему каталогу, для которого будет сгенерирован список файлов и директорий.

std::string server::generate_file_list(const fs::path &current_path) {     std::string html = "<html><body><h1>Files:</h1><ol>";      //Добавить ссылку для предыдущей директории     if (current_path != root_path) {         fs::path parent_path = current_path.parent_path();         //получает относительный путь от родительского каталога до корневого         std::string parent_link = fs::relative(parent_path, root_path).string();         html += "<li><a href=\"" + parent_link + "\">.. (Parent Directory)</a></li>";     }      //Отобразить список файлов     //Cоздается итератор, который проходит по всем файлам и каталогам в текущем каталоге     for (const auto& entry : fs::directory_iterator(current_path)) {         std::string name = entry.path().filename().string();         std::string link = fs::relative(entry.path(), root_path).string();          /*         *Если элемент является директорией,          *то добавляется элемент списка с ссылкой,          *указывающей на эту директорию. В конце имени добавляется слэш (/),          *чтобы указать, что это папка.           *         *Если элемент является обычным файлом,          *то добавляется ссылка на этот файл без слэша.               */         if (fs::is_directory(entry)) {             html += "<li><a href=\"" + link + "\">" + name + "/</a></li>";         } else if () {             html += "<li><a href=\"" + link + "\">" + name + "</a></li>";         }     }          html += "</ol></body></html>";      return html; }

Далее создаем функцию handle_request ( Эта функция обрабатывает HTTP-запросы и отвечает на них, генерируя соответствующие HTTP-ответы )

параметры функции:

  • root_path: Путь к корневой директории. Это путь, с которого начинаются все файлы для сервера.

  • req: HTTP-запрос. Это объект, который содержит все данные запроса, полученные от клиента.

  • res: HTTP-ответ. Это объект, в который записывается ответ сервера, который будет отправлен обратно клиенту.

  • socket: Сокет для подключения с клиентом, через который сервер отправляет ответ.

void server::handle_request(const fs::path& root_path, http::request<http::string_body>& req, http::response<http::string_body>& res, tcp::socket& socket) {     //получаем путь, указанный в запросе     std::string target = std::string(req.target());      //Если целевой путь пустой (например, запрос был на корень сервера)      //или запрос соответствует корню ("/"),      //то генерируется список файлов в корневой директории     if (target.empty() || target == "/") {         res.result(http::status::ok);         res.body() = generate_file_list(root_path);         res.set(http::field::content_type, "text/html");         return;     }      //Удаляем первый символ / из пути.      //Это необходимо, так как путь в запросе начинается с /,     //а нам нужно работать с относительным путем для поиска файла.     target.erase(0, 1);       //Создаем новый путь     fs::path file_path = root_path / target;      //Если file_path каталог, генерируем список файлов и подкаталогов     if (fs::is_directory(file_path)) {         res.result(http::status::ok);         res.body() = generate_file_list(file_path);         res.set(http::field::content_type, "text/html");         return;     }      //Если файл не существует или это не обычный файл то возвращаем ошибку 404     if (!fs::exists(file_path) || !fs::is_regular_file(file_path)) {         res.result(http::status::not_found);         res.body() = "File not found";         return;     }      //Если файл не открывается, то возвращаем ошибку     std::ifstream file(file_path.string(), std::ios::binary);     if (!file) {         res.result(http::status::internal_server_error);         res.body() = "Failed to open file";         return;     }      //Устанавливаем заголовок Content-Disposition с атрибутом attachment,     //что указывает браузеру, что файл должен быть скачан,а не открыт в браузере,     //и задаем имя файла как его имя на сервер     res.result(http::status::ok);     res.set(http::field::content_type, "application/octet-stream");     res.set(http::field::content_disposition, "attachment; filename=\"" + file_path.filename().string() + "\"");       //можно весь файл загрузить в озу и потом передавать его      // но это плохая идея, особенно, если файл большой     // поэтому так не делаем     //std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());     //res.body() = content;      //Создаем буфер размером 8 КБ,     //в который будут читаться данные файла для отправки клиенту.     constexpr size_t buffer_size = 8192;     char buffer[8192];      //Читаем данные из файла по частям     try {         while (file) {             file.read(buffer, buffer_size);             //возвращает количество фактически прочитанных байт             std::streamsize bytes_read = file.gcount();             //Если были прочитаны данные, то присваиваем эти данные телу ответа             if (bytes_read > 0) {                 res.body() = std::string(buffer, buffer + bytes_read);                 //отправляем ответ                 http::write(socket, res);             }         }     } catch (const std::exception& e) {         res.result(http::status::internal_server_error);         res.body() = "Error reading or sending file: " + std::string(e.what());         return;     }       }

Далее создаем функцию run_server (запускает сервер)

 void server::run_server() {     try {         //объект управляет асинхронными операциями ввода-вывода         net::io_context ioc;           //создание объекта для принятия входящих соединений от клиентов.         tcp::acceptor acceptor(ioc, {tcp::v4(), port});         std::cout << "Server started at port " << port << std::endl;          //основной бесконечный цикл,          //который сервер использует для обработки входящих запросов.         while (true) {             // создается новый сокет для обработки соединений             tcp::socket socket(ioc);                        //блокирует выполнение до тех пор,              //пока не будет получено входящее соединение от клиента.              //Как только соединение установлено, оно передается в созданный сокет.             acceptor.accept(socket);              //создаем буфер для хранения данных из входящего запроса.             beast::flat_buffer buffer;             //объект для хранения HTTP-запроса.             http::request<http::string_body> req;             //объект для хранения HTTP-ответа, который будет отправлен клиенту.             http::response<http::string_body> res;              try {                 //этот метод блокирует выполнение до тех пор,                  // пока весь HTTP-запрос не будет полностью прочитан                  //Читаются данные из сокета в буфер,                 //а затем в объект req помещается сам HTTP-запрос.                  http::read(socket, buffer, req);                //ни раз ловил ошибки о потерянном соединении, поэтому создаем исклюсение             } catch (const boost::system::system_error& e) {                 if (e.code() == boost::beast::http::error::end_of_stream) {                     std::cerr << "Client disconnected: " << e.what() << std::endl;                     continue;                 } else {                     std::cerr << "Error: " << e.what() << std::endl;                     continue;                 }             }              //После успешного чтения запроса, вызывается метод,             //который обрабатывает сам запрос и генерирует ответ.             handle_request(root_path, req, res,socket);              //ловил ошибки broken_pipe пару раз, поэтому тоже добавил исключение              try {                 http::write(socket, res);             } catch (const boost::system::system_error& e) {                 if (e.code() == boost::asio::error::broken_pipe) {                     std::cerr << "Client disconnected: " << e.what() << std::endl;                 } else {                     std::cerr << "Error: " << e.what() << std::endl;                 }             }         }     } catch (std::exception const& e) {         std::cerr << "Error: " << e.what() << std::endl;     } } 

весь код

#include "server.h"  #include <thread> #include <fstream> #include <boost/algorithm/string/predicate.hpp> #include <iostream> #include <boost/beast/core.hpp>  server::server(fs::path &root_path, unsigned short &port) : root_path(root_path), port(port) {     run_server(); }   std::string server::generate_file_list(const fs::path& current_path) {     std::string html = "<html><body><h1>Files:</h1><ol>";      //add a link to the previous directory     if (current_path != root_path) {         fs::path parent_path = current_path.parent_path();         std::string parent_link = fs::relative(parent_path, root_path).string();         html += "<li><a href=\"" + parent_link + "\">.. (Parent Directory)</a></li>";     }      //show list of files     for (const auto& entry : fs::directory_iterator(current_path)) {         std::string name = entry.path().filename().string();         std::string link = fs::relative(entry.path(), root_path).string();          if (fs::is_directory(entry)) {             html += "<li><a href=\"" + link + "\">" + name + "/</a></li>";         } else if (fs::is_regular_file(entry)) {             html += "<li><a href=\"" + link + "\">" + name + "</a></li>";         }     }          html += "</ol></body></html>";      return html; }  void server::handle_request(const fs::path& root_path, http::request<http::string_body>& req, http::response<http::string_body>& res, tcp::socket& socket) {     std::string target = std::string(req.target());      //show files in root directory     if (target.empty() || target == "/") {         res.result(http::status::ok);         res.body() = generate_file_list(root_path);         res.set(http::field::content_type, "text/html");         return;     }      target.erase(0, 1);     fs::path file_path = root_path / target;      //generate a new page with files from a subfolder     if (fs::is_directory(file_path)) {         res.result(http::status::ok);         res.body() = generate_file_list(file_path);         res.set(http::field::content_type, "text/html");         return;     }      //if the file is not found, a notification about this is displayed     if (!fs::exists(file_path) || !fs::is_regular_file(file_path)) {         res.result(http::status::not_found);         res.body() = "File not found";         return;     }      //if the file cannot be opened, a notification about this is displayed     std::ifstream file(file_path.string(), std::ios::binary);     if (!file) {         res.result(http::status::internal_server_error);         res.body() = "Failed to open file";         return;     }      res.result(http::status::ok);     res.set(http::field::content_type, "application/octet-stream");     res.set(http::field::content_disposition, "attachment; filename=\"" + file_path.filename().string() + "\"");       /*      * the file is sent in 8kb parts      */     constexpr size_t buffer_size = 8192;     char buffer[8192];      try {         while (file) {             file.read(buffer, buffer_size);             std::streamsize bytes_read = file.gcount();             if (bytes_read > 0) {                 res.body() = std::string(buffer, buffer + bytes_read);                 http::write(socket, res);             }         }     } catch (const std::exception& e) {         res.result(http::status::internal_server_error);         res.body() = "Error reading or sending file: " + std::string(e.what());         return;     }      //that's not right     //std::string content((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());     //res.body() = content; }  void server::run_server() {     try {         net::io_context ioc;          tcp::acceptor acceptor(ioc, {tcp::v4(), port});         std::cout << "Server started at port " << port << std::endl;          while (true) {             tcp::socket socket(ioc);             acceptor.accept(socket);              beast::flat_buffer buffer;             http::request<http::string_body> req;             http::response<http::string_body> res;              try {                 http::read(socket, buffer, req);             } catch (const boost::system::system_error& e) {                 if (e.code() == boost::beast::http::error::end_of_stream) {                     std::cerr << "Client disconnected: " << e.what() << std::endl;                     continue;                 } else {                     std::cerr << "Error: " << e.what() << std::endl;                     continue;                 }             }              handle_request(root_path, req, res,socket);              try {                 http::write(socket, res);             } catch (const boost::system::system_error& e) {                 if (e.code() == boost::asio::error::broken_pipe) {                     std::cerr << "Client disconnected: " << e.what() << std::endl;                 } else {                     std::cerr << "Error: " << e.what() << std::endl;                 }             }         }     } catch (std::exception const& e) {         std::cerr << "Error: " << e.what() << std::endl;     } } 

main.cpp

Осталось запустить сервер в main.cpp, в начале проверяю корректность аргументов, передаваемых программе, программа ожидает два аргумента: путь к директории и порт, на котором сервер должен слушать.

#include "server.h"  #include <boost/filesystem.hpp> #include <iostream>  int main(int argc, char* argv[]) {        //Если аргументов не 3 (включая имя программы),      //выводится сообщение об ошибке и программа завершает выполнение с кодом ошибки 1     if (argc != 3) {         std::cerr << "Usage: " << argv[0] << " <path_to_directory> <port>" << std::endl;         return 1;     }      //создает объект path с использованием первого аргумента командной строки,     //который является путем к директории,     //с которой будет работать сервер     boost::filesystem::path root_path(argv[1]);     //порт, который будет слушать сервер     unsigned short port = static_cast<unsigned short>(std::atoi(argv[2]));      //проверяем, существует ли путь, указанный     if (!boost::filesystem::exists(root_path) || !boost::filesystem::is_directory(root_path)) {         std::cerr << "Invalid directory path" << std::endl;         return 1;     }      //запускаем сервер     server server(root_path, port);     return 0; }

Запуск сервера

после сборки проекта, создастся папка bin с бинарником программы, его запускаем командой

./build/bin/file_server /home/user/Downloads 8080

где ./build/bin/file_server — это путь до бинарника

/home/user/Downloads — папка, которую будет «раздавать» сервер

8080 — порт, который будет слушать сервер

На этом все. Если я помог хотя бы 1 человеку то потратил время на написании статьи не зря. Спасибо за внимание.


ссылка на оригинал статьи https://habr.com/ru/articles/879400/