Реализация однопоточного асинхронного HTTP-прокси сервера на C++20 с использованием библиотеки Boost.Asio.
Проект представляет собой приложение, которое принимает HTTP-соединения, извлекает из запроса целевой хост, устанавливает с ним соединение и ретранслирует данные между клиентом и целевым сервером. Вся обработка сетевых операций выполняется асинхронно в одном потоке.
- Архитектура и реализация
- UML диаграммы
- Используемые технологии
- Начало работы
- Сборка проекта
- Использование
- Тестирование
-
Однопоточная модель: Сервер функционирует в одном потоке управления. Для обработки множественных одновременных соединений используется кооперативная многозадачность, реализованная через сопрограммы (корутины) C++20. Это исключает состояние гонки (race conditions) и необходимость использования примитивов синхронизации. Интеграционный тест
test_single_threaded_execution
верифицирует, что идентификатор потока обработки сессии совпадает с PID процесса. -
Асинхронные операции ввода-вывода: Все сетевые операции (принятие соединений, чтение, запись, разрешение имен) реализованы с помощью
boost::asio::co_spawn
иboost::asio::use_awaitable
. Это позволяет приостанавливать выполнение сопрограммыsession
на время ожидания I/O операции, не блокируя основной поток. -
Обработка сессии (
session
): Каждое новое клиентское соединение обрабатывается в отдельном экземпляре сопрограммыsession
. Жизненный цикл сессии включает следующие этапы:- Асинхронное чтение HTTP-заголовков от клиента с помощью
async_read_until
до разделителя\r\n\r\n
. Установлен лимит на максимальный размер заголовков (MAX_HEADERS_SIZE
). - Парсинг заголовков для извлечения значения поля
Host
функциейfindHostPort
. - Асинхронное разрешение доменного имени и установка TCP-соединения с целевым сервером.
- Пересылка исходного клиентского запроса целевому серверу.
- Асинхронное чтение заголовков ответа от сервера до
\r\n\r\n
. - Парсинг заголовков ответа для извлечения
Content-Length
(если присутствует) функциейfindContentLength
. - Немедленная пересылка заголовков ответа клиенту.
- Потоковая передача тела ответа: данные читаются от сервера в буфер фиксированного размера и немедленно записываются клиенту до тех пор, пока сервер не закроет соединение (
eof
) или не будет прочитаноContent-Length
байт.
- Асинхронное чтение HTTP-заголовков от клиента с помощью
-
Парсинг HTTP-заголовков: Реализован набор функций (
iterHeaders
,findHostPort
,findContentLength
), для итерации по заголовкам без выделения дополнительной памяти и копирования данных. -
Обработка ошибок: Исключения, возникающие в процессе сессии (например, невозможность подключиться к хосту), перехватываются в
try/catch
блоке внутри сопрограммыsession
. Клиенту в этом случае отправляется ответHTTP/1.1 502 Bad Gateway
. Соединение с клиентом также принудительно разрывается при нарушении лимитов:- Размер заголовков запроса превышает
MAX_HEADERS_SIZE
. - Заголовок
Content-Length
в ответе сервера превышаетMAX_BODY_SIZE
. - Отсутствует заголовок
Host
в запросе клиента.
- Размер заголовков запроса превышает
-
Управление ресурсами: Жизненный цикл сокетов и буферов управляется в рамках сопрограммы
session
. Завершение сопрограммы (штатное или из-за исключения) приводит к автоматическому закрытию сокетов и освобождению связанных ресурсов благодаря принципу RAII.
flowchart LR
C[Client] -->|HTTP Request| PRX[Async HTTP Proxy]
PRX -->|HTTP Request| T[(Target Server)]
T -->|HTTP Response| PRX
PRX -->|HTTP Response| C
- C++20/23: Сопрограммы :
awaitable
,use_awatable
,co_spawn
,tcp::socket
,async_read
,async_read_until
,async_write
,transfer_at_least
- Boost.Asio: Низкоуровневая работа с сетью, реализация асинхронных операций.
- CMake: Система сборки проекта.
- GoogleTest: Фреймворк для модульного тестирования.
- Pytest: Фреймворк для интеграционного тестирования.
Рекомендуемый способ работы с проектом — использование Dev Containers в Visual Studio Code.
- Нажмите
F1
и выберите командуDev Containers: Reopen in Container
. Это откроет проект в Docker-контейнере с предустановленными зависимостями.
Команды следует выполнять в терминале внутри dev-контейнера.
- Создание директории для сборки:
mkdir -p build && cd build
- Генерация файлов сборки:
cmake ..
- Компиляция проекта:
make
Исполняемые файлы будут размещены в директории build
.
Для запуска прокси-сервера необходимо указать порт для прослушивания.
./build/AsyncHttpProxy 5555
Выполнение запроса к http://example.com
через запущенный прокси-сервер:
wget -e use_proxy=yes -e http_proxy=127.0.0.1:5555 http://example.com
Эта последовательность команд демонстрирует полный цикл работы: запуск прокси, запуск тестового веб-сервера и выполнение запроса к нему через прокси.
Шаг 1: Переход в директорию сборки (если вы еще не там)
cd build
Шаг 2: Запуск прокси-сервера Запустите прокси-сервер в фоновом режиме на порту 5555.
./AsyncHttpProxy 5555 &
Шаг 3: Запуск тестового веб-сервера Запустите простой одноразовый веб-сервер на порту 8000 с помощью Python и netcat.
python3 -c 'print("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 4096\r\n\r\n" + "A"*4096, end="")' | nc -l 127.0.0.1 -p 8000 &
Шаг 4: Выполнение запроса через прокси
С помощью wget
сделайте запрос к вашему локальному тестовому серверу (127.0.0.1:8000
) через прокси.
wget -e use_proxy=yes -e http_proxy=127.0.0.1:5555 127.0.0.1:8000
Находясь в директории build
, выполните команды:
ctest --verbose
Проверяют функции парсинга заголовков (headers.cpp
) на корректность обработки различных форматов входных данных.
ctest -R AsyncHttpProxy_Tests --verbose
Запускают экземпляр сервера и выполняют end-to-end тесты, эмулируя HTTP-клиента. Сценарии включают: базовые GET/POST запросы, проверку конкурентного доступа, обработку больших заголовков и тел ответов, а также верификацию однопоточной модели исполнения.
ctest -R Integration.PytestProxyTest --verbose