From 99e0c2387a009004631849a915a5c7d81288c644 Mon Sep 17 00:00:00 2001 From: Andrey Kutin Date: Fri, 3 Jul 2026 09:40:24 +0300 Subject: [PATCH] Add project source files and build configuration --- CMakeLists.txt | 76 ++++++++++++++++++++++++ README.md | 133 ++++++++++++++++++++++++++++++++++++++++++ config.json | 13 +++++ configure.sh | 20 +++++++ src/common.hpp | 5 ++ src/config.hpp | 57 ++++++++++++++++++ src/database.cpp | 99 +++++++++++++++++++++++++++++++ src/database.hpp | 26 +++++++++ src/main.cpp | 76 ++++++++++++++++++++++++ src/server.cpp | 148 +++++++++++++++++++++++++++++++++++++++++++++++ src/server.hpp | 55 ++++++++++++++++++ 11 files changed, 708 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 config.json create mode 100755 configure.sh create mode 100644 src/common.hpp create mode 100644 src/config.hpp create mode 100644 src/database.cpp create mode 100644 src/database.hpp create mode 100644 src/main.cpp create mode 100644 src/server.cpp create mode 100644 src/server.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..67e63e0 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,76 @@ +cmake_minimum_required(VERSION 3.14) +project(cards) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Find required packages +find_package(Threads REQUIRED) +find_package(PkgConfig REQUIRED) + +# Find PostgreSQL (libpqxx) +pkg_check_modules(PQXX REQUIRED libpqxx>=7.0.0) +pkg_check_modules(PQ REQUIRED libpq) + +# Find Boost +find_package(Boost REQUIRED COMPONENTS system) + +# Find nlohmann_json +find_package(nlohmann_json REQUIRED) + +# Add executable +add_executable(cards + src/main.cpp + src/database.cpp + src/server.cpp +) + +# Include directories +target_include_directories(cards PRIVATE + ${Boost_INCLUDE_DIRS} + ${PQXX_INCLUDE_DIRS} + ${PQ_INCLUDE_DIRS} + src +) + +# Link libraries +target_link_libraries(cards + ${Boost_LIBRARIES} + ${PQXX_LIBRARIES} + ${PQ_LIBRARIES} + Threads::Threads + nlohmann_json::nlohmann_json +) + +# For Windows, add ws2_32 and crypt32 +if(WIN32) + target_link_libraries(cards ws2_32 crypt32) +endif() + +# Compiler flags +target_compile_definitions(cards PRIVATE BOOST_ALL_NO_LIB) + +# Set properties +set_target_properties(cards PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" +) + +# ─── Packaging (CPack) ──────────────────────────────────────────────── +set(CPACK_PACKAGE_NAME "cards") +set(CPACK_PACKAGE_VERSION "1.0.0") +set(CPACK_PACKAGE_VENDOR "cards") +set(CPACK_PACKAGE_MAINTAINER "root") +set(CPACK_PACKAGE_CONTACT "root@localhost") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Cards web server") +set(CPACK_PACKAGE_DESCRIPTION "A simple C++20 web server for cards") +set(CPACK_PACKAGE_SECTION "web") +set(CPACK_DEBIAN_PACKAGE_SECTION "web") +set(CPACK_DEBIAN_PACKAGE_PRIORITY "optional") +set(CPACK_DEBIAN_PACKAGE_DEPENDS + "libpqxx-7.0 (>= 7.0), libpq5 (>= 13), libstdc++6 (>= 10), libgcc-s1" +) +set(CPACK_DEBIAN_FILE_NAME "cards-${CPACK_PACKAGE_VERSION}-Linux.deb") +set(CPACK_GENERATOR "DEB") + +include(CPack) + diff --git a/README.md b/README.md index 0213fd1..74dc74a 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,135 @@ # cards +Веб-сервер на C++20 с использованием Boost.Beast и PostgreSQL. Отдаёт случайные записи из базы данных по REST API. + +## Требования + +- Компилятор с поддержкой C++20 (GCC 10+, Clang 10+) +- CMake >= 3.14 +- PostgreSQL (runtime) + libpq + libpqxx >= 7.0 +- Boost (system) +- nlohmann_json 3.x + +Установка зависимостей (Debian/Ubuntu): + +```bash +sudo apt-get install libboost-all-dev libpq-dev libpqxx-dev nlohmann-json3-dev cmake build-essential +``` + +## Сборка + +```bash +./configure.sh +``` + +Или вручную: + +```bash +mkdir -p build && cd build +cmake .. +make -j"$(nproc)" +cd .. +``` + +Бинарник: `build/bin/cards` + +## Конфигурация + +По умолчанию читает `config.json` из рабочей директории. Можно указать путь к другому файлу: + +```bash +./build/bin/cards /path/to/config.json +``` + +### Поля `config.json` + +**server** +| Поле | Тип | Значение по умолчанию | +|---|---|---| +| address | string | `"0.0.0.0"` | +| port | int | `8080` | +| threads | int | `4` | + +**database** +| Поле | Тип | Значение по умолчанию | +|---|---|---| +| host | string | `"localhost"` | +| port | int | `5432` | +| dbname | string | `"testdb"` | +| user | string | `"postgres"` | +| password | string | (пусто) | + +### Переменные окружения + +Переопределяют соответствующие поля `config.json`: + +- `DB_HOST` +- `DB_PORT` +- `DB_NAME` +- `DB_USER` +- `DB_PASSWORD` + +Пример: + +```bash +DB_PASSWORD=secret ./build/bin/cards +``` + +## API + +| Метод | Путь | Описание | +|---|---|---| +| GET | `/` | Случайная запись из таблицы `random_data` | +| GET | `/health` | Проверка состояния сервера | + +### Пример ответа `GET /` + +```json +{ + "id": 3, + "name": "Gamma", + "value": 300, + "description": "Third test record", + "created_at": "2025-01-01 12:00:00", + "timestamp": 1735732800000000000 +} +``` + +### Пример ответа `GET /health` + +```json +{ + "status": "ok", + "timestamp": 1735732800000000000 +} +``` + +## База данных + +При первом запуске автоматически создаётся таблица `random_data` и заполняется десятью тестовыми записями. + +Схема таблицы: + +```sql +CREATE TABLE random_data ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + value INTEGER NOT NULL, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +``` + +## Сборка deb-пакета + +```bash +cd build +cmake .. +cpack +``` + +Результат: `cards-1.0.0-Linux.deb` + +## Завершение работы + +Для остановки сервера отправьте сигнал `SIGINT` (Ctrl+C) или `SIGTERM`. diff --git a/config.json b/config.json new file mode 100644 index 0000000..2aa284e --- /dev/null +++ b/config.json @@ -0,0 +1,13 @@ +{ + "server": { + "address": "0.0.0.0", + "port": 8080, + "threads": 4 + }, + "database": { + "host": "localhost", + "port": 5432, + "dbname": "testdb", + "user": "postgres" + } +} diff --git a/configure.sh b/configure.sh new file mode 100755 index 0000000..f19eee1 --- /dev/null +++ b/configure.sh @@ -0,0 +1,20 @@ +#!/bin/bash +set -e + +echo "=== Установка зависимостей ===" +sudo apt-get update +sudo apt-get install -y libboost-all-dev libpq-dev libpqxx-dev nlohmann-json3-dev cmake build-essential + +echo "=== Сборка ===" +mkdir -p build +cd build +cmake .. +make -j"$(nproc)" +cd .. + +echo "" +echo "=== Готово ===" +echo "Запуск: ./build/bin/cards" +echo "" +echo "Пароль от БД можно задать через переменную окружения:" +echo " DB_PASSWORD=secret ./build/bin/cards" diff --git a/src/common.hpp b/src/common.hpp new file mode 100644 index 0000000..951b335 --- /dev/null +++ b/src/common.hpp @@ -0,0 +1,5 @@ +#pragma once + +#include + +using json = nlohmann::json; diff --git a/src/config.hpp b/src/config.hpp new file mode 100644 index 0000000..730f9f0 --- /dev/null +++ b/src/config.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include + +#include "common.hpp" + +struct ServerConfig { + std::string address; + int port; + int threads; +}; + +struct DatabaseConfig { + std::string host; + int port; + std::string dbname; + std::string user; + std::string password; +}; + +struct AppConfig { + ServerConfig server; + DatabaseConfig database; + + static AppConfig loadFromFile(const std::string& filename) { + std::ifstream file(filename); + if (!file.is_open()) { + throw std::runtime_error("Cannot open config file: " + filename); + } + + json j; + file >> j; + + AppConfig config; + config.server.address = j.at("server").at("address"); + config.server.port = j.at("server").at("port"); + config.server.threads = j.at("server").at("threads"); + config.database.host = j.at("database").value("host", "localhost"); + config.database.port = j.at("database").value("port", 5432); + config.database.dbname = j.at("database").value("dbname", ""); + config.database.user = j.at("database").value("user", ""); + config.database.password = j.at("database").value("password", ""); + + // Environment variables override config file + if (const char* val = std::getenv("DB_HOST")) config.database.host = val; + if (const char* val = std::getenv("DB_PORT")) config.database.port = std::stoi(val); + if (const char* val = std::getenv("DB_NAME")) config.database.dbname = val; + if (const char* val = std::getenv("DB_USER")) config.database.user = val; + if (const char* val = std::getenv("DB_PASSWORD")) config.database.password = val; + + return config; + } +}; + diff --git a/src/database.cpp b/src/database.cpp new file mode 100644 index 0000000..795b333 --- /dev/null +++ b/src/database.cpp @@ -0,0 +1,99 @@ +#include "database.hpp" +#include + +Database::Database(const DatabaseConfig& cfg) + : conn(std::make_unique( + "host=" + cfg.host + + " port=" + std::to_string(cfg.port) + + " dbname=" + cfg.dbname + + " user=" + cfg.user + + " password=" + cfg.password)), + rng(std::random_device{}()) { + ensureTableExists(); +} + +void Database::ensureTableExists() { + pqxx::work txn(*conn); + + // Создаем таблицу, если её нет + txn.exec( + "CREATE TABLE IF NOT EXISTS random_data (" + " id SERIAL PRIMARY KEY," + " name VARCHAR(100) NOT NULL," + " value INTEGER NOT NULL," + " description TEXT," + " created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP" + ")" + ); + + // Проверяем, есть ли данные + auto result = txn.exec("SELECT COUNT(*) FROM random_data"); + int count = result[0][0].as(); + + // Если данных нет, заполняем тестовыми данными + if (count == 0) { + txn.exec( + "INSERT INTO random_data (name, value, description) VALUES " + "('Alpha', 100, 'First test record'), " + "('Beta', 200, 'Second test record'), " + "('Gamma', 300, 'Third test record'), " + "('Delta', 400, 'Fourth test record'), " + "('Epsilon', 500, 'Fifth test record'), " + "('Zeta', 600, 'Sixth test record'), " + "('Eta', 700, 'Seventh test record'), " + "('Theta', 800, 'Eighth test record'), " + "('Iota', 900, 'Ninth test record'), " + "('Kappa', 1000, 'Tenth test record')" + ); + } + + txn.commit(); +} + +json Database::getRandomRecord() { + std::lock_guard lock(mtx); + + pqxx::work txn(*conn); + + // Получаем общее количество записей + auto countResult = txn.exec("SELECT COUNT(*) FROM random_data"); + int totalCount = countResult[0][0].as(); + + if (totalCount == 0) { + return json{ + {"error", "No records found in database"} + }; + } + + // Генерируем случайный ID + std::uniform_int_distribution idDist(1, totalCount); + int randomId = idDist(rng); + + // Запрашиваем случайную запись + auto result = txn.exec_params( + "SELECT id, name, value, description, created_at " + "FROM random_data " + "WHERE id = $1", + randomId + ); + + if (result.empty()) { + // Если не нашли, берем первую (на всякий случай) + result = txn.exec("SELECT id, name, value, description, created_at FROM random_data LIMIT 1"); + } + + const auto& row = result[0]; + + json response = { + {"id", row[0].as()}, + {"name", row[1].as()}, + {"value", row[2].as()}, + {"description", row[3].as()}, + {"created_at", row[4].as()}, + {"timestamp", std::chrono::system_clock::now().time_since_epoch().count()} + }; + + txn.commit(); + return response; +} + diff --git a/src/database.hpp b/src/database.hpp new file mode 100644 index 0000000..2dc43aa --- /dev/null +++ b/src/database.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "config.hpp" +#include "common.hpp" + +class Database { +public: + explicit Database(const DatabaseConfig& config); + ~Database() = default; + + json getRandomRecord(); + +private: + std::unique_ptr conn; + std::mt19937 rng; + std::mutex mtx; + // Создание тестовой таблицы, если она не существует + void ensureTableExists(); +}; + diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..9da4dfe --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,76 @@ +#include +#include +#include +#include + +#include "config.hpp" +#include "database.hpp" +#include "server.hpp" + +std::atomic stopServer{false}; + +void signalHandler(int signal) { + if (signal == SIGINT || signal == SIGTERM) { + std::cout << "\nReceived shutdown signal. Stopping server..." << std::endl; + stopServer = true; + } +} + +int main(int argc, char* argv[]) { + try { + // Загрузка конфигурации + std::string configFile = "config.json"; + if (argc > 1) { + configFile = argv[1]; + } + + std::cout << "Loading configuration from: " << configFile << std::endl; + AppConfig config = AppConfig::loadFromFile(configFile); + + // Инициализация базы данных + std::cout << "Connecting to database..." << std::endl; + auto db = std::make_shared(config.database); + std::cout << "Database connected successfully!" << std::endl; + + // Инициализация и запуск сервера + Server server( + config.server.address, + config.server.port, + config.server.threads, + db + ); + + // Установка обработчиков сигналов + std::signal(SIGINT, signalHandler); + std::signal(SIGTERM, signalHandler); + + std::cout << "Server is running on " + << config.server.address << ":" + << config.server.port + << " with " << config.server.threads << " threads" << std::endl; + std::cout << "Press Ctrl+C to stop" << std::endl; + + // Запуск сервера в отдельном потоке + std::thread serverThread([&server]() { + server.run(); + }); + + // Ожидание сигнала остановки + while (!stopServer) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + + std::cout << "Stopping server..." << std::endl; + server.stop(); + serverThread.join(); + + std::cout << "Server stopped successfully!" << std::endl; + + } catch (const std::exception& e) { + std::cerr << "Error: " << e.what() << std::endl; + return 1; + } + + return 0; +} + diff --git a/src/server.cpp b/src/server.cpp new file mode 100644 index 0000000..49a7aae --- /dev/null +++ b/src/server.cpp @@ -0,0 +1,148 @@ +#include "server.hpp" +#include +#include + +// Implementation of HttpSession +HttpSession::HttpSession(net::ip::tcp::socket&& socket, std::shared_ptr db) + : stream(std::move(socket)), db(db) {} + +void HttpSession::run() { + readRequest(); +} + +void HttpSession::readRequest() { + req = {}; + http::async_read( + stream, + buffer, + req, + beast::bind_front_handler( + &HttpSession::onRead, + shared_from_this() + ) + ); +} + +void HttpSession::onRead(beast::error_code ec, std::size_t) { + if (ec) { + handleError(ec, "read"); + return; + } + processRequest(std::move(req)); +} + +void HttpSession::processRequest(http::request&& request) { + http::response res{http::status::ok, request.version()}; + res.set(http::field::server, "Boost.Beast Server"); + res.set(http::field::content_type, "application/json"); + + try { + if (request.method() == http::verb::get && request.target() == "/") { + // Получаем случайный объект из БД + json response = db->getRandomRecord(); + res.body() = response.dump(4); + } else if (request.method() == http::verb::get && request.target() == "/health") { + json response = { + {"status", "ok"}, + {"timestamp", std::chrono::system_clock::now().time_since_epoch().count()} + }; + res.body() = response.dump(4); + } else { + res.result(http::status::not_found); + json error = { + {"error", "Not Found"}, + {"path", request.target()} + }; + res.body() = error.dump(4); + } + } catch (const std::exception& e) { + res.result(http::status::internal_server_error); + json error = { + {"error", "Internal Server Error"}, + {"message", e.what()} + }; + res.body() = error.dump(4); + } + + res.prepare_payload(); + writeResponse(std::move(res)); +} + +void HttpSession::writeResponse(http::response&& res) { + auto self = shared_from_this(); + http::async_write( + stream, + res, + [self](beast::error_code ec, std::size_t) { + if (ec) { + self->handleError(ec, "write"); + } + // Close the connection gracefully + self->stream.socket().shutdown(net::ip::tcp::socket::shutdown_send, ec); + } + ); +} + +void HttpSession::handleError(beast::error_code ec, const char* what) { + if (ec == net::error::eof || ec == net::error::connection_reset) { + // Normal disconnection + return; + } + std::cerr << "Error in " << what << ": " << ec.message() << std::endl; +} + +// Implementation of Server +Server::Server(const std::string& address, int port, int threads, std::shared_ptr db) + : ioc(threads), + acceptor(ioc, net::ip::tcp::endpoint(net::ip::make_address(address), port), true), + db(db), + threads(threads), + acceptTimer(ioc) + {} + +void Server::run() { + accept(); + + // Run the I/O service on the requested number of threads + threadPool.reserve(threads); + for (int i = 0; i < threads; ++i) { + threadPool.emplace_back([this]() { + ioc.run(); + }); + } + + // Wait for all threads to finish + for (auto& t : threadPool) { + if (t.joinable()) { + t.join(); + } + } +} + +void Server::stop() { + ioc.stop(); +} + +void Server::accept() { + acceptor.async_accept( + net::make_strand(ioc), + beast::bind_front_handler(&Server::onAccept, this) + ); +} + +void Server::onAccept(beast::error_code ec, net::ip::tcp::socket socket) { + if (!ec) { + std::make_shared(std::move(socket), db)->run(); + accept(); + } else { + std::cerr << "Accept error: " << ec.message() << std::endl; + if(!ioc.stopped()) { + acceptTimer.expires_after(std::chrono::milliseconds(100)); + acceptTimer.async_wait([this](beast::error_code ec) { + if (!ioc.stopped()) { accept(); } + }); + } + } + +} + diff --git a/src/server.hpp b/src/server.hpp new file mode 100644 index 0000000..f7aeb17 --- /dev/null +++ b/src/server.hpp @@ -0,0 +1,55 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.hpp" +#include "database.hpp" + +namespace beast = boost::beast; +namespace http = beast::http; +namespace net = boost::asio; + + +class HttpSession : public std::enable_shared_from_this { +public: + HttpSession(net::ip::tcp::socket&& socket, std::shared_ptr db); + void run(); + +private: + void readRequest(); + void onRead(beast::error_code ec, std::size_t bytes_transferred); + void processRequest(http::request&& req); + void writeResponse(http::response&& res); + void handleError(beast::error_code ec, const char* what); + + beast::tcp_stream stream; + beast::flat_buffer buffer; + std::shared_ptr db; + http::request req; +}; + +class Server { +public: + Server(const std::string& address, int port, int threads, std::shared_ptr db); + void run(); + void stop(); + +private: + void accept(); + void onAccept(beast::error_code ec, net::ip::tcp::socket socket); + + net::io_context ioc; + net::ip::tcp::acceptor acceptor; + std::shared_ptr db; + int threads; + std::vector threadPool; + net::steady_timer acceptTimer; +}; +