Add project source files and build configuration
This commit is contained in:
parent
a7848f56dc
commit
99e0c2387a
|
|
@ -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)
|
||||||
|
|
||||||
133
README.md
133
README.md
|
|
@ -1,2 +1,135 @@
|
||||||
# cards
|
# 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`.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"server": {
|
||||||
|
"address": "0.0.0.0",
|
||||||
|
"port": 8080,
|
||||||
|
"threads": 4
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"host": "localhost",
|
||||||
|
"port": 5432,
|
||||||
|
"dbname": "testdb",
|
||||||
|
"user": "postgres"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
using json = nlohmann::json;
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
#include "database.hpp"
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
Database::Database(const DatabaseConfig& cfg)
|
||||||
|
: conn(std::make_unique<pqxx::connection>(
|
||||||
|
"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<int>();
|
||||||
|
|
||||||
|
// Если данных нет, заполняем тестовыми данными
|
||||||
|
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<std::mutex> lock(mtx);
|
||||||
|
|
||||||
|
pqxx::work txn(*conn);
|
||||||
|
|
||||||
|
// Получаем общее количество записей
|
||||||
|
auto countResult = txn.exec("SELECT COUNT(*) FROM random_data");
|
||||||
|
int totalCount = countResult[0][0].as<int>();
|
||||||
|
|
||||||
|
if (totalCount == 0) {
|
||||||
|
return json{
|
||||||
|
{"error", "No records found in database"}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем случайный ID
|
||||||
|
std::uniform_int_distribution<int> 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<int>()},
|
||||||
|
{"name", row[1].as<std::string>()},
|
||||||
|
{"value", row[2].as<int>()},
|
||||||
|
{"description", row[3].as<std::string>()},
|
||||||
|
{"created_at", row[4].as<std::string>()},
|
||||||
|
{"timestamp", std::chrono::system_clock::now().time_since_epoch().count()}
|
||||||
|
};
|
||||||
|
|
||||||
|
txn.commit();
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <pqxx/pqxx>
|
||||||
|
#include <string>
|
||||||
|
#include <random>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
#include "config.hpp"
|
||||||
|
#include "common.hpp"
|
||||||
|
|
||||||
|
class Database {
|
||||||
|
public:
|
||||||
|
explicit Database(const DatabaseConfig& config);
|
||||||
|
~Database() = default;
|
||||||
|
|
||||||
|
json getRandomRecord();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<pqxx::connection> conn;
|
||||||
|
std::mt19937 rng;
|
||||||
|
std::mutex mtx;
|
||||||
|
// Создание тестовой таблицы, если она не существует
|
||||||
|
void ensureTableExists();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
@ -0,0 +1,76 @@
|
||||||
|
#include <iostream>
|
||||||
|
#include <exception>
|
||||||
|
#include <csignal>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
#include "config.hpp"
|
||||||
|
#include "database.hpp"
|
||||||
|
#include "server.hpp"
|
||||||
|
|
||||||
|
std::atomic<bool> 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<Database>(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
#include "server.hpp"
|
||||||
|
#include <iostream>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
|
// Implementation of HttpSession
|
||||||
|
HttpSession::HttpSession(net::ip::tcp::socket&& socket, std::shared_ptr<Database> 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<http::string_body>&& request) {
|
||||||
|
http::response<http::string_body> 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<http::string_body>&& 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<Database> 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<HttpSession>(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(); }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <boost/beast/core.hpp>
|
||||||
|
#include <boost/beast/http.hpp>
|
||||||
|
#include <boost/beast/version.hpp>
|
||||||
|
#include <boost/asio/ip/tcp.hpp>
|
||||||
|
#include <boost/asio/strand.hpp>
|
||||||
|
#include <memory>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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<HttpSession> {
|
||||||
|
public:
|
||||||
|
HttpSession(net::ip::tcp::socket&& socket, std::shared_ptr<Database> db);
|
||||||
|
void run();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void readRequest();
|
||||||
|
void onRead(beast::error_code ec, std::size_t bytes_transferred);
|
||||||
|
void processRequest(http::request<http::string_body>&& req);
|
||||||
|
void writeResponse(http::response<http::string_body>&& res);
|
||||||
|
void handleError(beast::error_code ec, const char* what);
|
||||||
|
|
||||||
|
beast::tcp_stream stream;
|
||||||
|
beast::flat_buffer buffer;
|
||||||
|
std::shared_ptr<Database> db;
|
||||||
|
http::request<http::string_body> req;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Server {
|
||||||
|
public:
|
||||||
|
Server(const std::string& address, int port, int threads, std::shared_ptr<Database> 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<Database> db;
|
||||||
|
int threads;
|
||||||
|
std::vector<std::thread> threadPool;
|
||||||
|
net::steady_timer acceptTimer;
|
||||||
|
};
|
||||||
|
|
||||||
Loading…
Reference in New Issue