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