Pistache для C++: REST-фреймворк нового поколения
Pistache — это современная, легковесная и высокопроизводительная библиотека для создания REST API на языке C++. Разработанная с нуля с использованием возможностей C++11/14/17, она предоставляет разработчикам чистый, интуитивно понятный интерфейс для построения HTTP-серверов и клиентов. В отличие от громоздких решений вроде Boost.Asio, Pistache предлагает лаконичный API, который делает процесс создания веб-сервисов на C++ таким же простым, как на Python или Node.js, но при этом сохраняет всю мощь и производительность нативного кода.
Зачем нужен Pistache? В современном мире микро-сервисной архитектуры и интернета вещей (IoT) часто требуется создавать легковесные, быстрые и надёжные HTTP-серверы. C++ идеально подходит для таких задач благодаря своей производительности, но традиционно написание HTTP-сервера на C++ было сложным и трудоёмким процессом. Pistache решает эту проблему, предоставляя готовые абстракции для маршрутизации, обработки запросов, парсинга параметров, управления сессиями и многого другого. Библиотека активно используется в продакшене для построения REST API, веб-хуков, микросервисов и даже в качестве основы для более сложных сетевых приложений.
Ключевые особенности Pistache включают: асинхронную обработку запросов на основе epoll (Linux) или kqueue (macOS), поддержку HTTP/1.1, встроенный парсинг query-параметров и JSON (через сторонние библиотеки), гибкую систему маршрутизации с поддержкой параметров пути, автоматическое управление памятью через умные указатели, и полную потокобезопасность. Всё это делает Pistache отличным выбором для проектов, где важна как скорость разработки, так и скорость выполнения.
Установка Pistache
Pistache не устанавливается через pip или npm, так как это нативная C++ библиотека. Установка производится через системные пакетные менеджеры или сборкой из исходников.
Linux (Ubuntu/Debian)
# Установка зависимостей
sudo apt-get update
sudo apt-get install -y libpistache-dev cmake g++ libssl-dev
# Проверка установки
dpkg -l | grep pistache
macOS (Homebrew)
brew install pistache
Сборка из исходников (кросс-платформенный способ)
git clone https://github.com/pistacheio/pistache.git
cd pistache
mkdir build && cd build
cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release ..
make -j$(nproc)
sudo make install
Быстрый старт: минимальный сервер
Создадим простейший HTTP-сервер, который отвечает "Hello, World!" на любой GET-запрос.
#include <pistache/endpoint.h>
#include <pistache/http.h>
#include <iostream>
using namespace Pistache;
class HelloHandler : public Http::Handler {
public:
HTTP_PROTOTYPE(HelloHandler)
void onRequest(const Http::Request& request, Http::ResponseWriter writer) override {
writer.send(Http::Code::Ok, "Hello, World!\n");
}
};
int main() {
// Создаём эндпоинт на порту 9080
Address addr(Ipv4::any(), Port(9080));
auto opts = Http::Endpoint::options().threads(1);
Http::Endpoint server(addr);
server.init(opts);
server.setHandler(std::make_shared<HelloHandler>());
server.serve();
return 0;
}
Компиляция: g++ -std=c++17 -lpistache -lpthread -o server server.cpp
Запустите сервер и откройте в браузере http://localhost:9080 — вы увидите приветствие.
Основные классы и методы Pistache
Рассмотрим ключевые компоненты библиотеки с примерами использования.
1. Http::Endpoint — точка входа сервера
Сигнатура: Http::Endpoint(const Address& addr);
Что делает: Создаёт HTTP-сервер, привязанный к указанному адресу и порту. Управляет жизненным циклом сервера.
#include <pistache/endpoint.h>
using namespace Pistache;
int main() {
Address addr(Ipv4::any(), Port(8080));
Http::Endpoint server(addr);
server.init(Http::Endpoint::options().threads(4));
// ... настройка обработчиков ...
server.shutdown();
return 0;
}
2. Http::Handler — базовый обработчик запросов
Сигнатура: class Handler { virtual void onRequest(const Request&, ResponseWriter) = 0; };
Что делает: Абстрактный класс для обработки входящих HTTP-запросов. Все пользовательские обработчики должны наследоваться от него.
class MyHandler : public Http::Handler {
public:
HTTP_PROTOTYPE(MyHandler) // Макрос для клонирования
void onRequest(const Http::Request& request, Http::ResponseWriter writer) override {
writer.send(Http::Code::Ok, "Response from MyHandler");
}
};
3. Http::Request — объект запроса
Сигнатура: class Request { std::string resource() const; std::string query() const; };
Что делает: Представляет входящий HTTP-запрос. Предоставляет доступ к URI, заголовкам, query-параметрам и телу запроса.
void onRequest(const Http::Request& req, Http::ResponseWriter writer) {
std::string path = req.resource(); // "/api/users"
std::string query = req.query(); // "?id=123&name=John"
std::string body = req.body(); // Тело запроса (для POST/PUT)
std::string userAgent = req.headers().get("User-Agent").value();
writer.send(Http::Code::Ok, "Path: " + path);
}
4. Http::ResponseWriter — объект для отправки ответа
Сигнатура: void send(Http::Code code, const std::string& body);
Что делает: Предоставляет методы для отправки HTTP-ответа клиенту. Поддерживает различные форматы: строки, JSON, файлы, потоки.
void onRequest(const Http::Request& req, Http::ResponseWriter writer) {
// Отправка JSON
writer.send(Http::Code::Ok, R"({"status": "ok", "message": "Hello"})", MIME(Application, Json));
// Отправка с заголовками
writer.headers().add("X-Custom-Header", "value");
writer.send(Http::Code::Created, "Resource created");
// Отправка файла
writer.send(Http::Code::Ok, "file.txt");
}
5. Http::Router — маршрутизация запросов
Сигнатура: void get(const std::string& route, RouteHandler handler);
Что делает: Позволяет регистрировать обработчики для конкретных HTTP-методов и путей. Поддерживает параметры в пути.
#include <pistache/router.h>
using namespace Pistache;
class ApiRouter {
public:
static void setup(Rest::Router& router) {
// GET /users
router.get("/users", [](const Rest::Request& req, Http::ResponseWriter writer) {
writer.send(Http::Code::Ok, "List of users");
});
// GET /users/:id — параметр в пути
router.get("/users/:id", [](const Rest::Request& req, Http::ResponseWriter writer) {
auto id = req.param(":id").as<int>();
writer.send(Http::Code::Ok, "User ID: " + std::to_string(id));
});
// POST /users
router.post("/users", [](const Rest::Request& req, Http::ResponseWriter writer) {
writer.send(Http::Code::Created, "User created");
});
}
};
6. Rest::Request — расширенный запрос с параметрами
Сигнатура: template<typename T> T param(const std::string& name) const;
Что делает: Расширяет Http::Request, добавляя удобный доступ к параметрам маршрута, query-параметрам и телу запроса.
void handler(const Rest::Request& req, Http::ResponseWriter writer) {
// Параметр из пути
int userId = req.param(":id").as<int>();
// Query-параметр
auto nameOpt = req.query().get("name");
std::string name = nameOpt.value_or("default");
// Парсинг JSON тела
auto json = nlohmann::json::parse(req.body());
std::string email = json["email"];
writer.send(Http::Code::Ok, "User " + std::to_string(userId) + ": " + name);
}
7. Http::Cookie — работа с куки
Сигнатура: Cookie(const std::string& name, const std::string& value);
Что делает: Позволяет устанавливать и читать HTTP-куки в запросах и ответах.
void onRequest(const Http::Request& req, Http::ResponseWriter writer) {
// Чтение куки из запроса
auto cookie = req.cookie("session_id");
if (cookie) {
std::cout << "Session: " << cookie->value << std::endl;
}
// Установка куки в ответе
Http::Cookie authCookie("token", "abc123");
authCookie.setHttpOnly(true);
authCookie.setMaxAge(3600); // 1 час
writer.cookies().add(authCookie);
writer.send(Http::Code::Ok, "Cookie set");
}
8. Http::Mime — работа с MIME-типами
Сигнатура: MIME(Application, Json) или MIME(Text, Plain)
Что делает: Предоставляет константы и утилиты для работы с MIME-типами содержимого.
void onRequest(const Http::Request& req, Http::ResponseWriter writer) {
// Отправка JSON
writer.send(Http::Code::Ok, "{}", MIME(Application, Json));
// Отправка HTML
writer.send(Http::Code::Ok, "<h1>Title</h1>", MIME(Text, Html));
// Отправка XML
writer.send(Http::Code::Ok, "<root/>", MIME(Application, Xml));
// Отправка произвольного бинарного контента
writer.send(Http::Code::Ok, "binary data", MIME(Application, OctetStream));
}
9. Http::Async — асинхронные операции
Сигнатура: template<typename Func> void async(Func func);
Что делает: Позволяет выполнять длительные операции асинхронно, не блокируя обработку других запросов.
void onRequest(const Http::Request& req, Http::ResponseWriter writer) {
// Асинхронная обработка
Http::Async::async([writer = std::move(writer)]() mutable {
// Длительная операция (например, запрос к БД)
std::this_thread::sleep_for(std::chrono::seconds(2));
writer.send(Http::Code::Ok, "Async response");
});
}
10. Http::Endpoint::options — настройки сервера
Сигнатура: static Options threads(int count);
Что делает: Позволяет конфигурировать сервер: количество потоков, таймауты, максимальный размер запроса и т.д.
auto opts = Http::Endpoint::options()
.threads(8) // Количество потоков
.maxRequestSize(1024 * 1024) // Максимальный размер запроса (1 MB)
.maxResponseSize(10 * 1024 * 1024) // Максимальный размер ответа (10 MB)
.flags(Tcp::Options::ReuseAddr); // Флаги сокета
server.init(opts);
Реальный практический пример: REST API для управления задачами
Создадим полноценный REST API для управления списком задач (To-Do list) с поддержкой CRUD операций.
#include <pistache/endpoint.h>
#include <pistache/router.h>
#include <pistache/http.h>
#include <nlohmann/json.hpp>
#include <vector>
#include <mutex>
#include <algorithm>
using namespace Pistache;
using json = nlohmann::json;
// Модель задачи
struct Task {
int id;
std::string title;
bool completed;
};
// Хранилище задач (в памяти)
class TaskRepository {
private:
std::vector<Task> tasks;
int nextId = 1;
std::mutex mtx;
public:
TaskRepository() {
// Добавим несколько тестовых задач
tasks.push_back({nextId++, "Изучить Pistache", false});
tasks.push_back({nextId++, "Написать REST API", false});
tasks.push_back({nextId++, "Задеплоить на сервер", true});
}
std::vector<Task> getAll() {
std::lock_guard<std::mutex> lock(mtx);
return tasks;
}
std::optional<Task> getById(int id) {
std::lock_guard<std::mutex> lock(mtx);
auto it = std::find_if(tasks.begin(), tasks.end(),
[id](const Task& t) { return t.id == id; });
if (it != tasks.end()) return *it;
return std::nullopt;
}
Task create(const std::string& title) {
std::lock_guard<std::mutex> lock(mtx);
Task newTask{nextId++, title, false};
tasks.push_back(new