В данной статье рассматривается процесс написания простого кросс-платформенного HTTP прокси.
Для разработки данного примера (исходные тексты) использовался Boost версии 1.35 или выше. Для сборки примера использовался cmake (но в принципе вы можете собрать его и вручную). Для конфигурации и сборки необходимо выполнить следующие команды (для Unix)1:
> cmake . > make
после компиляции у вас получится файл proxy-asio-async
, который можно запустить из
командной строки, и который будет выполнять проксирование данных. Эта программа принимает
один аргумент — количество нитей, которые будут выполнять диспатчеризацию запросов (по
умолчанию это число равно 2-м). Номер порта на котором прокси будет принимать запросы
равен 10001
, это значение заданно в тексте2.
Также как и в предыдущих примерах, наша программа состоит из трех частей:
main
, которая разбирает параметры командной строки, создает отдельные нити для
сервисов asio, объект сервер, и входит в режим ожидания;server
, который принимает запросы, и создает объект, реализующий логику
обработки соединения;connection
, который производит всю обработку клиентских данных, и осуществляет
передачу запросов и ответов между клиентом и веб-сервером.Обработка данных производится в асинхронном режиме, при этом, для распределения нагрузки
между процессорами, может использоваться несколько независимых сервисов выполняющих
диспатчеризацию (asio::io_service
).
Философское замечание: Самая трудная часть разработки асинхронного кода — правильное проектирование процесса обработки данных. Я обычно рисую диаграмму состояний (state diagram), и затем превращаю каждое из состояний в отдельную процедуру. Наличие такой диаграммы также сделает более легким понимание кода другими разработчиками.
Функция main
достаточно проста, так что не будем на ней подробно останавливаться —
достаточно лишь посмотреть на ее исходный код чтобы понять что она делает (все общие
определения находятся в файле common.h.
Реализация сервера (класс server
— proxy-server.hpp & proxy-server.cpp также
незначительно отличается от предыдущих примеров — изменения коснулись лишь способа
задания сервиса, который выполняет диспатчеризацию. В нашем примере новый сервис
последовательно выбирается из списка сервисов, который представляет собой кольцевой
список. Таким образом мы можем достичь некоторого баланса загрузки сервисов.
Вся логика обработки данных сосредоточена в классе, описывающем соединение (класс
connection
— proxy-conn.hpp & proxy-conn.cpp). Хочется сразу отметить, что разбор
заголовков сделан безо всяких оптимизаций, максимально просто3.
Обработка начинается с вызова функции start
классом server
, который принимает соединение и
создает новый объект класса connection
. Эта функция инициирует асинхронное чтение
заголовков запроса от браузера.
Чтение самих заголовков происходит в функции handle_browser_read_headers
, которая
вызывается при получении данных от браузера. Стоит отметить, что если мы получили
заголовки не полностью (отсутствует пустая строка (\r\n\r\n
)), то она инициирует новое
чтение, пытаясь получить заголовки целиком.
После того, как заголовки были получены целиком, то эта функция выделяет версию протокола HTTP, метод и адрес, а также производит разбор заголовков (некоторые данные оттуда нам понадобятся для определения — нужно нам использовать постоянные соединения, или нет).
После разбора заголовков, эта функция вызывает функцию start_connect
, которая разбирает
адрес сервера на имя хоста и порт, и если мы еще не имеем соединения с этим сервером, то
она инициирует процесс получения адреса сервера по его имени. А в том случае, если у нас
уже открыто соединение с сервером, то мы просто начинаем передачу данных с помощью функции
start_write_to_server
.
Функция handle_resolve
вызывается при определении адреса сервера, и инициирует процесс
установки соединения с сервером. Данный процесс обрабатывается функцией handle_connect
,
которая инициирует процесс передачи данных с помощью функции start_write_to_server
,
которая формирует корректный заголовок и начинает отправку данных серверу.
После окончания передачи данных серверу вызывается функция handle_server_write
, которая
выполняет простую операцию по началу чтения заголовков от сервера. Обработка заголовков
выполняется функцией handle_server_read_headers
, которая во многом подобна функции
handle_browser_read_headers
, за тем исключением, что кроме разбора заголовков, она также
пытается понять, нужно ли нам сохранять соединение после передачи данных пользователю, или
нет — это оказывает влияние на весь процесс обработки данных. После окончания обработки,
эта функция инициирует передачу данных (в нашем случае это заголовки) браузеру.
После передачи заголовков, организуется цикл, в котором передается уже тело ответа от
сервера. Для этого используются две функции —
handle_server_read_body
и
handle_browser_write
, которые вызывают друг друга до тех пор, пока мы не считаем все
данные с сервера (то количество данных, которое было указано сервером в заголовках), либо
не получим признак конца файла.
В том случае, если мы получили признак конца файла, мы передаем остаток данных браузеру и
закрываем соединение, в противном случае, если у нас используется постоянное соединение,
то мы инициируем с помощью функции start
новый процесс чтения заголовков запроса от
браузера.
Вот и все. Как уже отмечалось, основной проблемой является построение правильной последовательности вызова функций, реализующих передачу данных друг другу.
1. В том случае, если cmake не может найти библиотек, вы можете указать их расположение с
помощью переменных cmake —
CMAKE_INCLUDE_PATH
и CMAKE_LIBRARY_PATH
, запустив cmake
например вот так:
> cmake . -DCMAKE_INCLUDE_PATH=~/exp/include -DCMAKE_LIBRARY_PATH=~/exp/lib
2. В принципе можно было также вынести его в параметры командной строки, но было лень, поскольку это все-таки не рабочий код, а прототип для проверки идей.
3. В настоящее время ведется разработка проекта cpp-netlib, в составе которого разрабатываются и парсеры для основных протоколов — HTTP, SMTP и т.п.
Last change: 05.03.2013 16:54