В этой статье я описал процесс создания файлового сервера — инструмента для организации доступа к файлам по сети. В статье представлен пример реализации файлового сервера на 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 человеку то потратил время на написании статьи не зря. Спасибо за внимание.
Комментарии (4)

Tuxman
04.02.2025 13:47Если вы включаете -std=c++17, то можно использовать
std::filesystemвместоboost::filesystem.
Ещё, у вас сервер синхронный, т.е. если какие-то дисковые операции выполняются медленно, или застревают, то остальные клиенты просто ждут, и диск у вас получается "узким местом". Например, у вас файловая система по NFS.

Sazonov
04.02.2025 13:47boost::program_options для человеческого разбора командной строки.
Весь код из статьи выглядит как туториал из буста. Остальное уже написали :)
В чём же заключается «обучение»?
          
 
simple-mortal
Извини, пожалуйста, но считаю, что педагогическая ценность этой статьи близка к нулю, не говоря о том, что не покидает ощущение, что статья сгенерена какой-то ллмкой.
Если раскрыть тезис о педагогической бесполезности, то я бы выделил 2 момента:
Использование сторонней либы. Почитать доку не сильно сложно. Намного полезнее с точки зрения обучения заюзать хотя бы стд либу, написать больше кода, но объяснить процессы.
Отсутствие глубины теоритической, да и практической, если честно. Просто переписать код с комментами... опытному чуваку и так всё понятно, а новичок вряд ли вынесет что-то осознанное.
В общем, вот. Сорян, если что. И прям вот как будто чатгпт написал...
ROCKFALL
Все верно, без какой-то теории статья просто кусок кода, коих и на гитхабе полно. Ну и c чатжпт в точку. А вот интересные вопросы хотелось бы затронуть.
1) Файловый сервер который одновременно может обсуживать только одного клиента? Подняв планочку до C++20 можно было бы минимальными телодвижениями улучшить это
2) А что если придет relative url? Клиент сможет выбраться за пределы root path
3) Для отправки файла не обязательно костылить цикл с буфером. boost.beast из коробки проворачивает нечто похожее для отправки файлов (см. http::file_body)