Бібліотеки C++: cpp-httplib, cxxopts, CLI11, Google Test і Catch2
Бібліотеки C++: cpp-httplib, cxxopts, CLI11, Google Test і Catch2
🎯 Мета лекції
- Написати HTTP-сервер і клієнт на
cpp-httplibта підключити відповідь у форматі JSON. - Розбирати аргументи командного рядка бібліотеками
cxxoptsіCLI11та порівняти їх. - Писати тести у Google Test та Catch2 і запускати їх через CTest.
- Правильно підключати кожну бібліотеку через vcpkg і CMake.
🔑 Ключові терміни
- Маршрут (route): шлях запиту (
/api/user), для якого зареєстрований обробник. - Обробник (handler): функція, що виконується при запиті на маршрут.
- Опція командного рядка: параметр виду
--file xабо-v. - Тест-кейс (test case): незалежна перевірка однієї можливості.
Попередня лекція розглядала бібліотеки загального призначення. Тут розглядаються три прикладні задачі: обмін даними по мережі, керування програмою з терміналу та автоматична перевірка коду. Усі приклади зібрано і запущено в одному vcpkg-проєкті.
Підготовка проєкту
{
"name": "lib-demo2",
"version": "1.0.0",
"dependencies": ["cpp-httplib", "nlohmann-json", "cxxopts", "cli11", "catch2", "gtest"],
"builtin-baseline": "1d611efb49276c65d69fb6d010c2aaaf039322b0"
}
cmake_minimum_required(VERSION 3.15)
project(LibDemo2 CXX)
set(CMAKE_CXX_STANDARD 17)
find_package(httplib CONFIG REQUIRED)
find_package(nlohmann_json CONFIG REQUIRED)
find_package(cxxopts CONFIG REQUIRED)
find_package(CLI11 CONFIG REQUIRED)
find_package(Catch2 CONFIG REQUIRED)
add_executable(server server.cpp)
target_link_libraries(server PRIVATE httplib::httplib nlohmann_json::nlohmann_json)
add_executable(client client.cpp)
target_link_libraries(client PRIVATE httplib::httplib)
# macOS: cpp-httplib використовує системний резолвер Apple
if(APPLE)
target_link_libraries(server PRIVATE "-framework CoreFoundation" "-framework CFNetwork")
target_link_libraries(client PRIVATE "-framework CoreFoundation" "-framework CFNetwork")
endif()
add_executable(cli11_demo cli11_demo.cpp)
target_link_libraries(cli11_demo PRIVATE CLI11::CLI11)
add_executable(cxxopts_demo cxxopts_demo.cpp)
target_link_libraries(cxxopts_demo PRIVATE cxxopts::cxxopts)
add_executable(catch_tests catch_tests.cpp)
target_link_libraries(catch_tests PRIVATE Catch2::Catch2WithMain)
enable_testing()
include(CTest)
include(Catch)
catch_discover_tests(catch_tests)
if(APPLE) з'явився не випадково. Версія 0.27.0 cpp-httplib у vcpkg на macOS викликає системний резолвер імен Apple (CFHost), але ціль httplib::httplib не додає потрібних фреймворків. Без них лінкер завершується помилкою «Undefined symbols ... _CFArrayGetCount». Це приклад реальної поведінки пакетів: якщо збірка падає при лінкуванні, дивіться перші рядки помилки: вони називають відсутні символи, а за назвою можна зрозуміти, яку бібліотеку підключити. На Windows і Linux додаткових налаштувань не потрібно.cpp-httplib: HTTP без зайвого
Що це і навіщо
HTTP — протокол, за яким спілкуються браузери й веб-сервіси. cpp-httplib реалізує сервер і клієнт одним заголовковим файлом: без залежностей, без складного налаштування. Вона підходить для невеликих API, службових серверів і тестових стендів. Для висококонкурентних промислових сервісів використовують потужніші рішення (Boost.Beast, Drogon), але для навчання та прототипів cpp-httplib ідеальна.
Сервер
#include <httplib.h>
#include <nlohmann/json.hpp>
#include <iostream>
int main() {
httplib::Server svr;
// GET запит
svr.Get("/", [](const httplib::Request&, httplib::Response& res) {
res.set_content("Привіт від HTTP сервера!", "text/plain; charset=utf-8");
});
// GET з параметром шляху
svr.Get("/hello/:name", [](const httplib::Request& req, httplib::Response& res) {
std::string name = req.path_params.at("name");
res.set_content("Привіт, " + name + "!", "text/plain; charset=utf-8");
});
// POST запит
svr.Post("/api/data", [](const httplib::Request& req, httplib::Response& res) {
std::cout << "Отримано: " << req.body << std::endl;
res.set_content("OK", "text/plain");
});
// JSON API
svr.Get("/api/user", [](const httplib::Request&, httplib::Response& res) {
nlohmann::json user = {{"name", "Іван"}, {"age", 25}};
res.set_content(user.dump(), "application/json");
});
// Зупинка сервера для демонстрації
svr.Get("/stop", [&](const httplib::Request&, httplib::Response& res) {
res.set_content("bye", "text/plain");
svr.stop();
});
std::cout << "Сервер запущено на http://localhost:8765" << std::endl;
svr.listen("localhost", 8765);
std::cout << "Сервер зупинено" << std::endl;
}
Структура проста: об'єкт httplib::Server, до якого додаються обробники методами Get, Post, Put, Delete. Обробник — лямбда-функція з двома параметрами: запит Request (тут шлях, тіло, заголовки, параметри) та відповідь Response (її заповнює функція). Виклик listen блокує потік і обслуговує запити, доки сервер не зупинять.
Фрагмент :name у шляху /hello/:name — параметр маршруту. Значення читається з req.path_params.
Клієнт
#include <httplib.h>
#include <iostream>
int main() {
httplib::Client cli("http://localhost:8765");
auto res = cli.Get("/");
if (res && res->status == 200) {
std::cout << "GET / -> " << res->status << " " << res->body << std::endl;
}
res = cli.Get("/hello/Оксана");
if (res) {
std::cout << "GET /hello/... -> " << res->status << " " << res->body << std::endl;
}
res = cli.Get("/api/user");
if (res) {
std::cout << "GET /api/user -> " << res->status << " " << res->body
<< " (" << res->get_header_value("Content-Type") << ")" << std::endl;
}
auto post = cli.Post("/api/data", R"({"key": "value"})", "application/json");
if (post) {
std::cout << "POST /api/data -> " << post->status << " " << post->body << std::endl;
}
res = cli.Get("/missing");
if (res) {
std::cout << "GET /missing -> " << res->status << std::endl;
}
cli.Get("/stop");
}
Запит повертає Result: об'єкт, який приводиться до bool. Значення false означає, що з'єднатися не вдалося (сервер недоступний, обрив мережі), і тоді res->status читати не можна. Тому перевірка if (res) обов'язкова перед зверненням до статусу. Код 404 для невідомого маршруту — це успішно отримана відповідь, res для нього істинний.
Результат одночасного запуску сервера і клієнта:
Сервер сумісний зі стандартними інструментами. Той самий запит можна надіслати утилітою curl:
{ "name": "cpp-httplib", "features": ["openssl"] }, а в коді перед підключенням заголовка визначається макрос CPPHTTPLIB_OPENSSL_SUPPORT. Подробиці про features були в лекції про просунуті можливості vcpkg.localhost, тобто доступний лише з вашого комп'ютера, і не містить автентифікації. Перш ніж виставляти подібний сервіс у мережу, потрібно додати перевірку вхідних даних, обмеження розміру запитів, HTTPS і авторизацію. cpp-httplib є зручним інструментом для навчання і внутрішніх сервісів, але не готовим захищеним сервером.Аргументи командного рядка
Програми, які запускаються з терміналу, приймають параметри: --file data.txt --verbose. Розбирати argv вручну незручно: потрібно підтримувати довгі й короткі форми, значення за замовчуванням, довідку, повідомлення про помилки. Цю роботу беруть на себе бібліотеки.
cxxopts
Легка header-only бібліотека зі стилем, схожим на GNU-утиліти:
#include <cxxopts.hpp>
#include <iostream>
int main(int argc, char* argv[]) {
cxxopts::Options options("MyApp", "Опис програми");
options.add_options()
("f,file", "Input file", cxxopts::value<std::string>())
("o,output", "Output file", cxxopts::value<std::string>()->default_value("output.txt"))
("v,verbose", "Verbose output", cxxopts::value<bool>()->default_value("false"))
("c,count", "Repeat count", cxxopts::value<int>()->default_value("1"))
("h,help", "Print help");
try {
auto result = options.parse(argc, argv);
if (result.count("help")) {
std::cout << options.help() << std::endl;
return 0;
}
if (result.count("file")) {
std::cout << "Input file: " << result["file"].as<std::string>() << std::endl;
}
std::cout << "Output: " << result["output"].as<std::string>() << std::endl;
std::cout << "Verbose: " << (result["verbose"].as<bool>() ? "yes" : "no") << std::endl;
std::cout << "Count: " << result["count"].as<int>() << std::endl;
} catch (const cxxopts::exceptions::exception& e) {
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
return 0;
}
Запис ("f,file", "опис", тип) визначає одразу коротку (-f) та довгу (--file) форму. Значення за замовчуванням задається через ->default_value. Метод count("ключ") повідомляє, чи вказав користувач опцію.
Довідку --help бібліотека склала самостійно. Некоректне значення (abc замість числа) призводить до виняткової ситуації, яку код перехоплює й повідомляє про помилку.
CLI11
Альтернатива з багатшими можливостями: перевірки значень (валідатори), підкоманди (як у git commit, git push), обов'язкові параметри.
#include <CLI/CLI.hpp>
#include <iostream>
#include <string>
int main(int argc, char** argv) {
CLI::App app{"Демонстрація CLI11"};
std::string file;
int count = 1;
bool verbose = false;
app.add_option("-f,--file", file, "Вхідний файл")->required();
app.add_option("-c,--count", count, "Кількість повторів")->check(CLI::PositiveNumber);
app.add_flag("-v,--verbose", verbose, "Докладний вивід");
CLI11_PARSE(app, argc, argv);
std::cout << "file=" << file << " count=" << count << " verbose=" << verbose << std::endl;
return 0;
}
Відмінність підходу: значення записуються прямо у ваші змінні (file, count), без звернення до таблиці результатів. Метод ->required() робить параметр обов'язковим, а ->check(CLI::PositiveNumber) застосовує перевірку.
Валідатор відхилив нуль і пояснив причину, а пропущений обов'язковий параметр викликав окреме повідомлення. Усе це відбулося без жодного рядка власного коду перевірки.
Порівняння
| Характеристика | cxxopts | CLI11 |
|---|---|---|
| Підхід | Таблиця результатів result["ключ"] | Прив'язка до змінних |
| Підкоманди | Немає | Є |
| Валідатори | Немає, лише розбір типу | Є (PositiveNumber, діапазони, файли) |
| Обов'язкові параметри | Перевіряються вручну | ->required() |
| Розмір і складність | Менше, простіший | Більше можливостей |
| Підключення | cxxopts::cxxopts | CLI11::CLI11 |
Для невеликої утиліти з кількома опціями достатньо cxxopts. Якщо потрібні підкоманди або перевірки значень, зручніший CLI11.
Тестові фреймворки
Основи тестування й CTest розглядались у лекції про тестування. Тут показано, як підключити тестові фреймворки через vcpkg та порівняти два найпоширеніші.
Google Test через vcpkg
У вихідній лекції Google Test завантажувався FetchContent. Менеджер пакетів дозволяє не завантажувати його при кожній конфігурації:
{ "dependencies": ["gtest"] }
find_package(GTest CONFIG REQUIRED)
add_executable(math_tests tests/test_math.cpp)
target_link_libraries(math_tests PRIVATE GTest::gtest_main MathUtils)
include(GoogleTest)
gtest_discover_tests(math_tests)
Сам код тестів (TEST, EXPECT_EQ, параметризовані тести) залишається тим самим.
Catch2 v3
Catch2 — фреймворк із простішим синтаксисом: тести оголошуються макросом TEST_CASE, а допоміжні підрозділи SECTION дозволяють ділити тест на частини з окремим налаштуванням.
#include <catch2/catch_test_macros.hpp>
#include <vector>
int add(int a, int b) {
return a + b;
}
TEST_CASE("Addition works correctly", "[math]") {
REQUIRE(add(2, 2) == 4);
REQUIRE(add(-1, 1) == 0);
REQUIRE(add(0, 0) == 0);
}
TEST_CASE("Vectors work", "[containers]") {
std::vector<int> v;
SECTION("starts empty") {
REQUIRE(v.empty());
REQUIRE(v.size() == 0);
}
SECTION("can add elements") {
v.push_back(5);
REQUIRE(v.size() == 1);
REQUIRE(v[0] == 5);
}
}
#define CATCH_CONFIG_MAIN та заголовок <catch2/catch.hpp>. Так працювала версія 2. У версії 3, яку встановлює vcpkg, цього заголовка немає: підключається <catch2/catch_test_macros.hpp>, а функцію main надає ціль Catch2::Catch2WithMain, а не макрос. Копіювання коду зі старих статей призводить до помилки «file not found».Для CTest Catch2 має власну функцію реєстрації catch_discover_tests (підключається include(Catch)):
Повідомлення «7 assertions in 2 test cases» показує, що секції SECTION не збільшили кількість тест-кейсів. Порядок запуску вибирається випадково (Randomness seeded): це допомагає виявляти тести, які залежать від виконання один одного.
Google Test чи Catch2
| Характеристика | Google Test | Catch2 v3 |
|---|---|---|
| Оголошення тесту | TEST(Набір, Ім'я) | TEST_CASE("опис", "[теги]") |
| Перевірки | EXPECT_* (продовжує), ASSERT_* (зупиняє) | CHECK (продовжує), REQUIRE (зупиняє) |
| Спільне налаштування | Класи-фікстури | SECTION усередині тесту |
| Параметризовані тести | TEST_P, INSTANTIATE_TEST_SUITE_P | GENERATE(...) |
| Імена тестів | Ідентифікатори C++ | Довільні рядки |
| Макроси мокування | У складі (gMock) | Немає, використовують окрему бібліотеку |
| Цілі CMake | GTest::gtest_main | Catch2::Catch2WithMain |
Якщо потрібне мокування (підміна залежностей у тестах), вибирають Google Test з gMock. Для швидких тестів із читабельними описами зручніший Catch2. Обидва фреймворки однаково працюють із CTest, тому перевага визначається смаком команди.
Питання для самоперевірки
Result оцінюється як true, якщо відповідь від сервера отримано (навіть із кодом 404 чи 500), і як false, якщо з'єднання не вдалося. Тому спершу перевіряють if (res), а потім читають res->status. Звернення до статусу при false призведе до невизначеної поведінки.CoreFoundation, CFNetwork) для розв'язання імен із тайм-аутом. Якщо експортована ціль CMake не вказує ці фреймворки, лінкер не знаходить відповідних символів. Рішення: явно додати -framework CoreFoundation -framework CFNetwork для платформи Apple.app add ..., app list ...), готові перевірки значень (діапазони, існування файлу) або зручні обов'язкові параметри. Для простих утиліт з кількома прапорцями cxxopts зручніший завдяки малому розміру.main: замість <catch2/catch.hpp> і #define CATCH_CONFIG_MAIN використовується <catch2/catch_test_macros.hpp> та підключення цілі Catch2::Catch2WithMain.Практичне завдання
Створіть HTTP-сервіс із трьома маршрутами:
GET /notesповертає JSON-масив нотаток.POST /notesприймає JSON{"text": "..."}, додає нотатку й повертає її з номером.DELETE /notes/:idвидаляє нотатку і повертає 404, якщо такої немає.
Додатково: порт береться з параметра --port (CLI11, за замовчуванням 8080), а логіка зберігання винесена в окремий клас NoteStore, покритий тестами Catch2.
- Парсинг тіла:
nlohmann::json::parse(req.body); приparse_errorповертайтеres.status = 400. res.status = 404; res.set_content("...", "text/plain");задає код відповіді.- Тестуйте
NoteStoreнапряму (без мережі): це швидко і не залежить від порту. - Для перевірки використовуйте
curl -X POST -d '{"text":"hi"}' localhost:8080/notes.
Резюме
🌐 cpp-httplib
Get, Post, параметри шляху, JSON. Перед читанням відповіді перевіряйте if (res). На macOS додайте системні фреймворки.⌨️ cxxopts і CLI11
cxxopts легший, CLI11 має підкоманди та валідатори. Обидві складають довідку --help автоматично.🧪 Google Test і Catch2
gtest_discover_tests, catch_discover_tests). Catch2 v3 використовує catch_test_macros.hpp і Catch2WithMain.std::filesystem, Eigen, SQLite, YAML і TOML, а також огляд Boost.Бібліотеки C++: fmt, spdlog і nlohmann/json
Форматування рядків бібліотекою fmt, логування через spdlog (рівні, sinks, ротація, шаблони), робота з JSON у nlohmann/json: створення, читання, файли, власні типи, обробка помилок. Підключення через vcpkg та CMake, перевірені приклади.
Основи: пам'ять, процеси та інструменти
Що таке реверс-інжиніринг, правові межі, адресний простір процесу, доступ до чужої пам'яті та інструменти для Windows, Linux і macOS.