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