Для себя как-то по работе я рассматривал libevent и libev. У каждой есть свои преимущества. Если же есть желание или потребность в скорой разработке небольшого http-сервера, то для меня большой интерес представляет libevent, а с учетом некоторых новшеств C++11 код становится намного компактнее и позволяет создать базовый http-сервер менее, чем в 40 строк.
Материал поста возможно будет полезен тем, кто еще не знаком с libevent и есть потребность в скором создании своего http-сервера, а так же материал может заинтересует людей, у которых такой потребности пока нет и даже если они уже имели опыт создания подобного, интересно узнать их мнение и опыт. А так как пост не содержит ничего принципиально нового, то может быть использован как материал для начала работы в данном направлении, а следовательно попробую поставить пометку «обучающий материал».
Чем хороша libevent в отличии от, например, libev и boost.asio, так это тем, что она имеет свой встроенный http-сервер, и некоторую абстракцию для работы с буферами. А так же имеет немалый набор вспомогательных функций. Можно HTTP протокол и самому разобрать, написав простенький конечный автомат или еще каким-нибудь методом. При работе с libevent это все уже есть. Эта такая приятная плюшка, а можно и на более низкий уровень спуститься и писать свой же парсер для HTTP, при этом работу с сокетами сделать на libevent. Уровень детализации у библиотеки мне понравился тем, что если есть желание сделать что-то быстро, то можно найти в ней более высокоуровневый интерфейс, который как правило менее гибок. При появлении больших потребностей можно постепенно спускаться уровень за уровнем все ниже и ниже. Библиотека позволяет делать многие вещи: асинхронный ввод-вывод, работу с сетью, работа с таймерами, rpc, т. д; можно с ее помощью создавать как серверное, так и клиентское ПО.
Зачем?
Создание собственного небольшого http-сервера может быть обусловлено для каждого его собственными потребностями, желанием или не желанием использовать полнофункциональные готовые сервера по той или иной причине. Предположим у Вас есть некоторое серверное ПО, которое работает по какому-то своему протоколу и решает некоторые задачи и у Вас появилась потребность выдать некоторое API для данного ПО через HTTP протокол. Возможно всего несколько небольших функций по настройке сервера и получению его текущего состояния по протоколу HTTP. Например, организовав обработку запросов GET с параметрами и отдавать небольшой xml с ответом или еще в каком-то формате. В таком случае можно с малыми трудозатратами создать свой http-сервер, который и будет интерфейсом для основного Вашего серверного ПО. Кроме этого если есть необходимость создать свой небольшой специфичный сервис по раздаче какого-то набора файлов или даже создать свое собственное веб-приложение, то можно так же воспользоваться таким самописным небольшим сервером. В общем можно воспользоваться как для построения самодостаточного серверного ПО, так и для создания вспомогательных сервисов в рамках более крупных систем.
Простой http-сервер менее чем в 40 строк
Чтобы создать простой однопоточный http-сервер с помощью libevent нужно выполнить следующие несколько незамысловатых шагов:
- Инициализировать глобальный объект библиотеки с помощью функции event_init. Эта функция может использоваться только для однопоточной обработки. Для многопоточной работы на каждый поток должен быть создан свой объект (об этом ниже).
- Создание непосредственно http-сервера осуществляется функцией evhttp_start в случае однопоточного сервера с глобальным объектом обработки событий. Объект созданный с помощью evhttp_start в конце следует удалить с помощью evhttp_free.
- Чтобы реагировать на входящие запросы нужно установить функцию обратного вызова с помощью evhttp_set_gencb.
- После чего можно запускать цикл обработки событий функцией event_dispatch. Эта функция так же рассчитана на работу в одном потоке с глобальным объектом.
- При обработке запроса можно получить буфер для ответа функцией evhttp_request_get_output_buffer. В этот буфер добавить какой-то контент. Например, для отправки строки можно воспользоваться функцией evbuffer_add_printf, а для отправки файла функцией evbuffer_add_file. После чего ответ на запрос должен быть отправлен, а сделать это можно с помощью evhttp_send_reply.
#include <memory> #include <cstdint> #include <iostream> #include <evhttp.h> int main() { if (!event_init()) { std::cerr << "Failed to init libevent." << std::endl; return -1; } char const SrvAddress[] = "127.0.0.1"; std::uint16_t SrvPort = 5555; std::unique_ptr<evhttp, decltype(&evhttp_free)> Server(evhttp_start(SrvAddress, SrvPort), &evhttp_free); if (!Server) { std::cerr << "Failed to init http server." << std::endl; return -1; } void (*OnReq)(evhttp_request *req, void *) = [] (evhttp_request *req, void *) { auto *OutBuf = evhttp_request_get_output_buffer(req); if (!OutBuf) return; evbuffer_add_printf(OutBuf, "<html><body><center><h1>Hello Wotld!</h1></center></body></html>"); evhttp_send_reply(req, HTTP_OK, "", OutBuf); }; evhttp_set_gencb(Server.get(), OnReq, nullptr); if (event_dispatch() == -1) { std::cerr << "Failed to run messahe loop." << std::endl; return -1; } return 0; }
Получилось менее 40 строк, которые способны обрабатывать http-запросы, отдавая в ответ строку «Hello World», а если заменить функцию evbuffer_add_printf на evbuffer_add_file, то можно отправлять файлы. Можно такой сервер назвать базовой комплектацией. Любой авто дилер или риэлтор в большинстве своем мечтают, чтобы их авто и квартиры никогда и ни при каких условиях не уходили в базовой комплектации, а только с дополнительными опциями. А вот нужны ли такие опции потребителю и в каком объеме…
Что может дать такая базовая комплектация по быстродействию можно проверить с помощью утилиты ab для *nix систем с небольшой вариацией параметров.
Server Hostname: 127.0.0.1
Server Port: 5555
Document Path: /
Document Length: 64 bytes
Concurrency Level: 1000
Time taken for tests: 2.289 seconds
Complete requests: 50000
Failed requests: 0
Write errors: 0
Keep-Alive requests: 50000
Total transferred: 8500000 bytes
HTML transferred: 3200000 bytes
Requests per second: 21843.76 [#/sec] (mean)
Time per request: 45.780 [ms] (mean)
Time per request: 0.046 [ms] (mean, across all concurrent requests)
Transfer rate: 3626.41 [Kbytes/sec] received
Connection Times (ms)
min mean[±sd] median max
Connect: 0 3 48.6 0 1001
Processing: 17 42 9.0 43 93
Waiting: 17 42 9.0 43 93
Total: 19 45 49.7 43 1053
Server Hostname: 127.0.0.1
Server Port: 5555
Document Path: /
Document Length: 64 bytes
Concurrency Level: 1000
Time taken for tests: 5.004 seconds
Complete requests: 50000
Failed requests: 0
Write errors: 0
Total transferred: 6300000 bytes
HTML transferred: 3200000 bytes
Requests per second: 9992.34 [#/sec] (mean)
Time per request: 100.077 [ms] (mean)
Time per request: 0.100 [ms] (mean, across all concurrent requests)
Transfer rate: 1229.53 [Kbytes/sec] received
Connection Times (ms)
min mean[±sd] median max
Connect: 0 61 214.1 20 3028
Processing: 7 34 17.6 31 277
Waiting: 6 28 16.9 25 267
Total: 17 95 219.5 50 3055
Тест проводился на уже не совсем новом ноутбуке (2 ядра, 4Гб оперативной памяти) под управлением 32-х битной операционной системы Ubuntu 12.10.
Многопоточный http-сервер
Нужна ли многопоточность? Вопрос риторический… Можно все IO и в одном потоке организовать, а запросы складывать в очередь и разгребать ее в несколько потоков. В таком случае вышеприведенный сервер можно просто дополнить очередью и пулом потоков для обработки и больше ничего городить не стоит. Если же есть желание или потребность построить многопоточный сервер, то он будет немного длиннее предыдущего, однако ненамного. C++11 с его умными указателями позволяют хорошо реализовывать RAII, как это было приведено с std::unique_ptr в примере выше, а также наличие лямбда-функций немного сокращает код.
Пример многопоточного сервера по своей идеологии аналогичен однопоточному, а некоторые особенности, связанные с многопоточностью его увеличивают примерно в 2 раза по объему кода. Восемьдесят с небольшим строк кода для многопоточного http-сервера на C++ — это не так и много.
Одно из решений, которое можно сделать:
- Создать несколько потоков, например, равное удвоенному количеству ядер процессора. C++11 имеет поддержку работы с потоками и теперь больше не надо писать свои обертки.
- Для каждого потока создать свой объект работы с событиями с помощью функции event_base_new. Созданный объект в конце должен быть удален функцией event_base_free, а std::unique_ptr и RAII это позволяют сделать более компактно.
- Для каждого потока с учетом вышесозданного объекта создать свой объект http-сервера с помощью функции evhttp_new. Этот объект так же в конце должен быть удален, а сделать это можно с помощью evhttp_free.
- Так же как и в предыдущем примере установить обработчик запросов с помощью evhttp_set_gencb.
- Этот шаг может оказаться самым странным. Нужно создать и привязать сокет к сетевому интерфейсу для нескольких обработчиков, каждый из которых расположен в своем потоке. Тут можно воспользоваться API для работы с сокетами (создать сокет, настроить его, привязать к определенному интерфейсу), а после передать сокет для работы серверу функцией evhttp_accept_socket. Это долго. Libevent предоставляет несколько функций для решения этой задачи. Как уже выше сказано было, libevent дает возможность при необходимости опускаться на уровень ниже и ниже в зависимости от потребности и выбрать для себя оптимальный. В данном случае для первого потока вся работа по созданию сокета, его настройке и привязке выполняется функцией evhttp_bind_socket_with_handle и из настроенного объекта извлекается сокет для других потоков с помощью evhttp_bound_socket_get_fd. Все остальные потоки уже используют полученный сокет, установив его для обработки функцией evhttp_accept_socket. Немного странно, но куда проще, чем при использовании API для работы с сокетами, и еще проще если учитывать кроссплатформенность. Казалось бы API для беркли сокетов оно одно и то же, но если Вы писали кроссплатформенное ПО с его использованием, например для Windows и Linux, то код написанный под одну операционную систему однозначно не эквивалентен коду под другую.
- Запустить цикл обработки событий. В отличии от однопоточного сервера это надо сделать иным способом, так как объекты у всех разные. Для этого есть специальная функция в libevent (event_base_dispatch). Для себя я в ней вижу один минус — ее трудно править корректным способом (например, надо иметь ситуацию, в которой можно вызвать event_base_loopexit). Для этого надо немного извернуться. А так можно воспользоваться функцией event_base_loop. Эта функция не блокирующая даже если нет событий к обработке, она возвращает управление, что дает упрощенную возможность завершения цикла обработки событий и возможность что-то делать между вызовами. Есть и минус — чтобы напрасно не загружать процессор на холостом ходу надо поставить хоть небольшую задержку (в C++11 — ‘это легко сделать примерно так: std::this_thread::sleep_for(std::chrono::milliseconds(10)) ).
- Обработка запросов аналогична первому примеру.
- В ходе создания и настройки очередного потока в его функции может что-то быть не ладно: например, какая-то функция libevent сообщила об ошибке. В данном случае можно кинуть исключение и перехватить его, а после отправить за пределы потока с помощью все тех же средств C++11 (std::exception_ptr, std::current_exception и std::rethrow_exception)
#include <stdexcept> #include <iostream> #include <memory> #include <chrono> #include <thread> #include <cstdint> #include <vector> #include <evhttp.h> int main() { char const SrvAddress[] = "127.0.0.1"; std::uint16_t const SrvPort = 5555; int const SrvThreadCount = 4; try { void (*OnRequest)(evhttp_request *, void *) = [] (evhttp_request *req, void *) { auto *OutBuf = evhttp_request_get_output_buffer(req); if (!OutBuf) return; evbuffer_add_printf(OutBuf, "<html><body><center><h1>Hello Wotld!</h1></center></body></html>"); evhttp_send_reply(req, HTTP_OK, "", OutBuf); }; std::exception_ptr InitExcept; bool volatile IsRun = true; evutil_socket_t Socket = -1; auto ThreadFunc = [&] () { try { std::unique_ptr<event_base, decltype(&event_base_free)> EventBase(event_base_new(), &event_base_free); if (!EventBase) throw std::runtime_error("Failed to create new base_event."); std::unique_ptr<evhttp, decltype(&evhttp_free)> EvHttp(evhttp_new(EventBase.get()), &evhttp_free); if (!EvHttp) throw std::runtime_error("Failed to create new evhttp."); evhttp_set_gencb(EvHttp.get(), OnRequest, nullptr); if (Socket == -1) { auto *BoundSock = evhttp_bind_socket_with_handle(EvHttp.get(), SrvAddress, SrvPort); if (!BoundSock) throw std::runtime_error("Failed to bind server socket."); if ((Socket = evhttp_bound_socket_get_fd(BoundSock)) == -1) throw std::runtime_error("Failed to get server socket for next instance."); } else { if (evhttp_accept_socket(EvHttp.get(), Socket) == -1) throw std::runtime_error("Failed to bind server socket for new instance."); } for ( ; IsRun ; ) { event_base_loop(EventBase.get(), EVLOOP_NONBLOCK); std::this_thread::sleep_for(std::chrono::milliseconds(10)); } } catch (...) { InitExcept = std::current_exception(); } }; auto ThreadDeleter = [&] (std::thread *t) { IsRun = false; t->join(); delete t; }; typedef std::unique_ptr<std::thread, decltype(ThreadDeleter)> ThreadPtr; typedef std::vector<ThreadPtr> ThreadPool; ThreadPool Threads; for (int i = 0 ; i < SrvThreadCount ; ++i) { ThreadPtr Thread(new std::thread(ThreadFunc), ThreadDeleter); std::this_thread::sleep_for(std::chrono::milliseconds(500)); if (InitExcept != std::exception_ptr()) { IsRun = false; std::rethrow_exception(InitExcept); } Threads.push_back(std::move(Thread)); } std::cout << "Press Enter fot quit." << std::endl; std::cin.get(); IsRun = false; } catch (std::exception const &e) { std::cerr << "Error: " << e.what() << std::endl; } return 0; }
В коде можно заметить, что каждый поток создается после некоторого внесенного ожидания. Это небольшой хак, который уже будет исправлен в конечной версии сервера. Пока можно сказать только, что если этого не сделать, то потоки надо будет как-то синхронизировать, чтобы они отработали «странный шаг» по созданию и привязке сокета. Для упрощения пока пусть останется такой хак. Так же в приведенном коде лямбда-функция может показаться спорным решением. Лямбды могут быть хорошим решением при использовании, например, в качестве некоторого предиката при работе со стандартными алгоритмами. В то же время можно задуматься об их использовании и при написании более больших фрагментов кода. В примере выше можно было все вынести в обычную функцию, передать все нужные параметры и получить код в стиле C++03. В то же время использование лямбды дало сокращение в объеме кода. На мой взгляд, когда код невелик, то лямбды могут вполне хорошо в него вписывать даже с не самым коротким ее содержанием и не влиять пагубно на качество кода, конечно не стоит вдаваться в крайности и вспоминать студенческие будни с написанием лабораторной работы в 700 строк в единственной функции main.
Тестирование многопоточного сервера проведено с теми же параметрами, что и предыдущего примера.
Server Hostname: 127.0.0.1
Server Port: 5555
Document Path: /
Document Length: 64 bytes
Concurrency Level: 1000
Time taken for tests: 1.576 seconds
Complete requests: 50000
Failed requests: 0
Write errors: 0
Keep-Alive requests: 50000
Total transferred: 8500000 bytes
HTML transferred: 3200000 bytes
Requests per second: 31717.96 [#/sec] (mean)
Time per request: 31.528 [ms] (mean)
Time per request: 0.032 [ms] (mean, across all concurrent requests)
Transfer rate: 5265.68 [Kbytes/sec] received
Server Hostname: 127.0.0.1
Server Port: 5555
Document Path: /
Document Length: 64 bytes
Concurrency Level: 1000
Time taken for tests: 3.685 seconds
Complete requests: 50000
Failed requests: 0
Write errors: 0
Total transferred: 6300000 bytes
HTML transferred: 3200000 bytes
Requests per second: 13568.41 [#/sec] (mean)
Time per request: 73.701 [ms] (mean)
Time per request: 0.074 [ms] (mean, across all concurrent requests)
Transfer rate: 1669.55 [Kbytes/sec] received
Connection Times (ms)
min mean[±sd] median max
Connect: 0 36 117.2 23 1033
Processing: 3 37 10.0 37 247
Waiting: 3 30 8.7 30 242
Total: 9 73 118.8 61 1089
Конечный вариант сервера
Базовая комплектация приведена, комплектация с небольшим набором опций так же есть. Теперь очередь подошла и для создания чего-то более полезного и функционального, а так же с небольшим тюнингом.
Минимальный http-сервер:
#include "http_server.h" #include "http_headers.h" #include "http_content_type.h" #include <iostream> int main() { try { using namespace Network; HttpServer Srv("127.0.0.1", 5555, 4, [&] (IHttpRequestPtr req) { req->SetResponseAttr(Http::Response::Header::Server::Value, "MyTestServer"); req->SetResponseAttr(Http::Response::Header::ContentType::Value, Http::Content::Type::html::Value); req->SetResponseString("<html><body><center><h1>Hello Wotld!</h1></center></body></html>"); }); std::cout << "Press Enter for quit." << std::endl; std::cin.get(); } catch (std::exception const &e) { std::cout << e.what() << std::endl; } return 0; }
Весьма минимальный объем кода для http-сервера на C++. За все есть плата. И в данном случае такая простота клиентского кода по созданию сервера, оплачена более длинной реализацией, скрытой в предлагаемой обертке над libevent. На самом же деле ненамного увеличилась реализация. Чуть ниже ее фрагменты будут описаны.
Создание сервера:
- Необходимо создать объект типа HttpServer. В качестве параметров как минимум передать адрес и порт, на котором будет работать сервер, количество потоков и функцию для обработки запросов (в данном случае так как обработка запросов минимальна, то можно и небольшой лямбдой обойтись без создания отдельной функции или даже целого класса-обработчика). После создания объекта сервер будет работать до тех пор, пока будет существовать его объект.
- Обработчик принимает умный указатель на интерфейс IHttpRequest, реализация которого скрывает всю работу с буфером libevent и отправку ответа, а его методы дают возможность получать данные из входящего запроса и формировать ответ.
namespace Network { DECLARE_RUNTIME_EXCEPTION(HttpRequest) struct IHttpRequest { enum class Type { HEAD, GET, PUT, POST }; typedef std::unordered_map<std::string, std::string> RequestParams; virtual ~IHttpRequest() {} virtual Type GetRequestType() const = 0; virtual std::string const GetHeaderAttr(char const *attrName) const = 0; virtual std::size_t GetContentSize() const = 0; virtual void GetContent(void *buf, std::size_t len, bool remove) const = 0; virtual std::string const GetPath() const = 0; virtual RequestParams const GetParams() const = 0; virtual void SetResponseAttr(std::string const &name, std::string const &val) = 0; virtual void SetResponseCode(int code) = 0; virtual void SetResponseString(std::string const &str) = 0; virtual void SetResponseBuf(void const *data, std::size_t bytes) = 0; virtual void SetResponseFile(std::string const &fileName) = 0; }; typedef std::shared_ptr<IHttpRequest> IHttpRequestPtr; }
Данный интерфейс позволяет получать из входящего запроса его тип, некоторые атрибуты (заголовки), размер тела запроса и само тело запроса при его наличии, а так же формировать ответ с возможностью задать атрибуты (заголовки), код завершения обработки запроса и тело ответа (в данной реализации имеются методы для передачи строки, некоторого буфера или файла в ответ). Каждый метод в его реализации может генерировать исключение типа HttpRequestException.
Если еще раз взглянуть на код сервера, то в коде обработки запросов можно заметить такие строки:
req->SetResponseAttr(Http::Response::Header::Server::Value, "MyTestServer"); req->SetResponseAttr(Http::Response::Header::ContentType::Value, Http::Content::Type::html::Value);
Это формирование заголовка ответа, а данном примере задаются такие поля заголовка, как «Content-Type» и «Server». Не смотря на то, что libevent имеет достаточно широкий функционал, выходящий далеко за потребности HTTP, списка констант полей заголовков в ней нет; есть только неполный список кодов возврата (наиболее часто используемых). Чтобы не возиться со строками, определяющими поля заголовков (например, во избежании опечаток в пользовательском коде), все константы определены уже в предлагаемой обертке над libevent.
namespace Network { namespace Http { namespace Request { namespace Header { DECLARE_STRING_CONSTANT(Accept, Accept) DECLARE_STRING_CONSTANT(AcceptCharset, Accept-Charset) // ... } } namespace Response { namespace Header { DECLARE_STRING_CONSTANT(AccessControlAllowOrigin, Access-Control-Allow-Origin) DECLARE_STRING_CONSTANT(AcceptRanges, Accept-Ranges) // ... } } } }
Строковые константы можно определить как простыми макросами в старом стиле чистого C в заголовочных файлах, так и разнести их объявления и определения между .h и .cpp файлами при этом сделав их типизированными уже в стиле C++. Однако можно обойтись и без разнесения по файлам, а сделать все типизированные определения в стиле C++ только в заголовочном файле. Для этого можно использовать некоторый подход с шаблонами и написать такой макрос (макросы, конечно, признанное C++ зло, а так же в небольших дозировках — бальзам; гетерогенные решения обладают большей жизнеспособностью).
#define DECLARE_STRING_CONSTANT(name_, value_) \ namespace Private \ { \ template <typename T> \ struct name_ \ { \ static char const Name[]; \ static char const Value[]; \ }; \ template <typename T> \ char const name_ <T>::Name[] = #name_; \ template <typename T> \ char const name_ <T>::Value[] = #value_; \ } \ typedef Private:: name_ <void> name_;
Почти аналогичным образом определены и константы для задания типа контента; имеют небольшую модификацию. Было желание реализовать поиск типа контента по расширению файла для удобства при отправке файлов в ответ на запрос.
При желании что-то получить из входящего запроса, например, с какого хоста и с какой страницы был осуществлен переход на запрашиваемый ресурс и, например, есть ли у пользователя «печеньки», можно это все получить из заголовка входящего запроса таким образом:
std::string Host = req->GetHeaderAttr(Http::Request::Header::Host::Value); std::string Referer = req->GetHeaderAttr(Http::Request::Header::Referer::Value); std::string Cookie = req->GetHeaderAttr(Http::Request::Header::Cookie::Value);
Аналогичным образом в ответе можно, например, установить пользователю некоторые Cookie, по которым в дальнейшем работать с его сессией и отслеживать при желании его блуждания по Вашему ресурсу (пример работы с заголовками ответа приведен в кода сервера).
Если же есть желание организовать некоторое свое API через HTTP, то это так же легко сделать. Предположим надо создать методы: открытие сессии, получение статистической информации о сервере и закрытие сессии. Пусть для этого строки запроса к Вашему серверу будут выглядеть примерно так:
http://myserver.com/service/login/OpenSession?user=nym&pwd=kakoyto http://myserver.com/service/login/CliseSession?sessionId=nym1234567890 http://myserver.com/service/stat/GetInfo?sessionId=nym1234567890
Ответом на эти строки запросов сервер пользователя может сгенерировать какой-то ответ, например, в формате xml. Это дело разработчика сервера. А вот как работать с такими запросами, получать из них параметры приведено ниже:
auto Path = req->GetPath(); auto Params = req->GetParams();
Один из путей для примеров выше будет таким /service/login/OpenSession, а параметры это карта из переданных пар ключ / значение. Тип карты параметров:
typedef std::unordered_map<std::string, std::string> RequestParams;
После разбора всего того, что можно реализовать с помощью предлагаемой конечной версии обертки над libevent можно заглянуть и под капот этой самой обертки.
namespace Network { DECLARE_RUNTIME_EXCEPTION(HttpServer) class HttpServer final : private Common::NonCopyable { public: typedef std::vector<IHttpRequest::Type> MethodPool; typedef std::function<void (IHttpRequestPtr)> OnRequestFunc; enum { MaxHeaderSize = static_cast<std::size_t>(-1), MaxBodySize = MaxHeaderSize }; HttpServer(std::string const &address, std::uint16_t port, std::uint16_t threadCount, OnRequestFunc const &onRequest, MethodPool const &allowedMethods = {IHttpRequest::Type::GET }, std::size_t maxHeadersSize = MaxHeaderSize, std::size_t maxBodySize = MaxBodySize); private: volatile bool IsRun = true; void (*ThreadDeleter)(std::thread *t) = [] (std::thread *t) { t->join(); delete t; };; typedef std::unique_ptr<std::thread, decltype(ThreadDeleter)> ThreadPtr; typedef std::vector<ThreadPtr> ThreadPool; ThreadPool Threads; Common::BoolFlagInvertor RunFlag; }; } </source</spoiler> <spoiler title="Реализация класса HttpServer"><source lang="cpp"> namespace Network { HttpServer::HttpServer(std::string const &address, std::uint16_t port, std::uint16_t threadCount, OnRequestFunc const &onRequest, MethodPool const &allowedMethods, std::size_t maxHeadersSize, std::size_t maxBodySize) : RunFlag(&IsRun) { int AllowedMethods = -1; for (auto const i : allowedMethods) AllowedMethods |= HttpRequestTypeToAllowedMethod(i); bool volatile DoneInitThread = false; std::exception_ptr Except; evutil_socket_t Socket = -1; auto ThreadFunc = [&] () { try { bool volatile ProcessRequest = false; RequestParams ReqPrm; ReqPrm.Func = onRequest; ReqPrm.Process = &ProcessRequest; typedef std::unique_ptr<event_base, decltype(&event_base_free)> EventBasePtr; EventBasePtr EventBase(event_base_new(), &event_base_free); if (!EventBase) throw HttpServerException("Failed to create new base_event."); typedef std::unique_ptr<evhttp, decltype(&evhttp_free)> EvHttpPtr; EvHttpPtr EvHttp(evhttp_new(EventBase.get()), &evhttp_free); if (!EvHttp) throw HttpServerException("Failed to create new evhttp."); evhttp_set_allowed_methods(EvHttp.get(), AllowedMethods); if (maxHeadersSize != MaxHeaderSize) evhttp_set_max_headers_size(EvHttp.get(), maxHeadersSize); if (maxBodySize != MaxBodySize) evhttp_set_max_body_size(EvHttp.get(), maxBodySize); evhttp_set_gencb(EvHttp.get(), &OnRawRequest, &ReqPrm); if (Socket == -1) { auto *BoundSock = evhttp_bind_socket_with_handle(EvHttp.get(), address.c_str(), port); if (!BoundSock) throw HttpServerException("Failed to bind server socket."); if ((Socket = evhttp_bound_socket_get_fd(BoundSock)) == -1) throw HttpServerException("Failed to get server socket for next instance."); } else { if (evhttp_accept_socket(EvHttp.get(), Socket) == -1) throw HttpServerException("Failed to bind server socket for new instance."); } DoneInitThread = true; for ( ; IsRun ; ) { ProcessRequest = false; event_base_loop(EventBase.get(), EVLOOP_NONBLOCK); if (!ProcessRequest) std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } catch (...) { Except = std::current_exception(); } }; ThreadPool NewThreads; for (int i = 0 ; i < threadCount ; ++i) { DoneInitThread = false; ThreadPtr Thread(new std::thread(ThreadFunc), ThreadDeleter); NewThreads.push_back(std::move(Thread)); for ( ; ; ) { if (Except != std::exception_ptr()) { IsRun = false; std::rethrow_exception(Except); } if (DoneInitThread) break; std::this_thread::sleep_for(std::chrono::milliseconds(100)); } } Threads = std::move(NewThreads); } }
Функцию обработки запросов можно посмотреть в полной версии, скачав исходные файлы примеров, она стала немного больше, чем в ранее приведенных примерах, и перестала претендовать на лямбду без потери читаемости кода. Так же не стал приводить реализацию интерфейса IHttpRequest, так как она мало интересна своей рутинной работой с буфером libevent. А в остальном если посмотреть на код итоговой версии, он не сильно-то изменился. Небольшая модификация и добавилось немного «тюнинга».
Сервер пользователя не обязан обрабатывать все типы http-запросов. Можно задать список типов запросов, которые сервер должен обрабабывать и для этого libevent имеет функцию evhttp_set_allowed_methods (а по умолчанию обертка задает только тип запросов GET). При задании списка обрабатываемых запросов на все остальные libevent сама будет сообщать о невозможности выполнения такого запроса, тем самым избавив пользователя от дополнительных проверок.
Пытливость ума она бывает разной: нацеленной на созидание и на разрушение. От разрушительной пытливости ума с желанием «завалить» сервер послав ему какой-то непомерно для него большой заголовок http-пакета или сформировав большое тело запроса можно так же проактивно защититься функциями evhttp_set_max_headers_size и evhttp_set_max_body_size. Конечно же отправка больших запросов может быть вызвана не только недобрыми помыслами, а так же и иными причинами. Приведенные методы позволят немного сократить нежелательные аварийные завершения Вашего сервера. Возможно еще что-то предусмотреть, а в остальном уже можно реагировать реактивно, что как правило и происходит…
В конце приведу финальную версию, которая отрабатывает запросы GET (отдает файлы из указанной директории) и выводит на экран с какого хоста был сделан запрос и с какой страницы был осуществлен переход на ресурс, обрабатываемый сервером.
#include "http_server.h" #include "http_headers.h" #include "http_content_type.h" #include <iostream> #include <sstream> #include <mutex> int main() { char const SrvAddress[] = "127.0.0.1"; std::uint16_t SrvPort = 5555; std::uint16_t SrvThreadCount = 4; std::string const RootDir = "../test_content"; std::string const DefaultPage = "index.html"; std::mutex Mtx; try { using namespace Network; HttpServer Srv(SrvAddress, SrvPort, SrvThreadCount, [&] (IHttpRequestPtr req) { std::string Path = req->GetPath(); Path = RootDir + Path + (Path == "/" ? DefaultPage : std::string()); { std::stringstream Io; Io << "Path: " << Path << std::endl << Http::Request::Header::Host::Name << ": " << req->GetHeaderAttr(Http::Request::Header::Host::Value) << std::endl << Http::Request::Header::Referer::Name << ": " << req->GetHeaderAttr(Http::Request::Header::Referer::Value) << std::endl; std::lock_guard<std::mutex> Lock(Mtx); std::cout << Io.str() << std::endl; } req->SetResponseAttr(Http::Response::Header::Server::Value, "MyTestServer"); req->SetResponseAttr(Http::Response::Header::ContentType::Value, Http::Content::TypeFromFileName(Path)); req->SetResponseFile(Path); }); std::cin.get(); } catch (std::exception const &e) { std::cout << e.what() << std::endl; } return 0; }
Заключение
Кроме рассмотренного функционала libevent еще много содержит полезных возможностей. В общем: еще есть чего попробовать написать с помощью этой библиотеки и о чем написать. Этот пост показал только ее малую часть, предназначенную для разработки http-серверов. Последний пример этого поста взят за основу, в которую на скорую руку добавлено немного вспомогательного функционала и реализован сервер, на котором и расположены исходные файлы всех приведенных примеров в виде zip-архива. Скачивая архив с примерами с сервера, разработанного на их же основе можно посмотреть на жизнеспособность сервера. В конце прошлого года мной был опубликован пост «Система плагинов как упражнение на C++ 11». В личку были вопросы наличии какой-то еще информации, моем желании развивать проект, поддержке и т. д. и в определенный момент я решил организовать небольшой информационный ресурс для описанной системы плагинов. Дизайнер из меня никакой, так что за дизайн сильно прошу не журить 🙂 Накидал немного статического контента для этого ресурса и надо было его чем-то отдавать. Да, можно было поднять что-то из nginx или apache, а может и еще что-то. Но мне было интересно как будут работать ранее разработанные мною тестовые примеры http-серверов, которые я описывал в посте о решении тестового задания с написанием «простенького» http-сервера. И на одном из таких примеров, разработанном «на сокетах» (как иногда это любят называть в тестовых заданиях и т. д.) с собственным разбором протокола и т. д. сайт был доступен почти месяц. Проработал успешно и без падений. Хабраэффекта конечно же не было. Да и откуда ему взяться. А с написанием этого поста я перевел выдачу контента о системе плагинов на сервер, разработанный на примерах этого же поста, там же разместил и сами примеры описываемых серверов. Выдержит ли такой сервер хабраэффекта? Не знаю. Вполне может быть, что на уровне приложения и выдержит, а вот выдержит ли моя VDS’ка (2 ядра, 1Гб оперативной памяти, ОС — Ubuntu 12.04 64bit) уже затрудняюсь сказать. А возможен ли сам хабраэффект так же не могу на это надеяться. Пока предлагаю немного протестировать полученный сервер с учетом сети и расположения на удаленном виртуальном сервере, а не локально на моей же машине. Результат тестирования:
Server Hostname: t-boss.ru
Server Port: 80
Document Path: /libevent_test_http_srv.zip
Document Length: 23756 bytes
Concurrency Level: 1000
Time taken for tests: 10.012 seconds
Complete requests: 2293
Failed requests: 0
Write errors: 0
Keep-Alive requests: 2293
Total transferred: 60628847 bytes
HTML transferred: 60328370 bytes
Requests per second: 229.02 [#/sec] (mean)
Time per request: 4366.365 [ms] (mean)
Time per request: 4.366 [ms] (mean, across all concurrent requests)
Transfer rate: 5913.65 [Kbytes/sec] received
Две с небольшим тысячи обработанных запросов на получение архива с исходными файлами примеров поста за десять секунд. Кроме самого http-сервера есть еще несколько иных задач: оптимальная организация логилования, кэширование и т. д. Пока этого нет и это дает возможность еще немного поэксперементировать с memcached, berkeley db и иными технологиями по созданию собственного веб-приложения на C++ и о результатах написать.
Всем спасибо за внимание!
Материалы
ссылка на оригинал статьи http://habrahabr.ru/post/217437/
Добавить комментарий