В этой статье я описал процесс создания файлового сервера — инструмента для организации доступа к файлам по сети. В статье представлен пример реализации файлового сервера на 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 ¤t_path — путь к текущему каталогу, для которого будет сгенерирован список файлов и директорий.
std::string server::generate_file_list(const fs::path ¤t_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/
Добавить комментарий