Просматривая временами Хабр, я периодически встречаю посты на тему создания собственного веб-сервера на C++ или на ином языке. Так как больший интерес для меня представляет C++ из языков программирования, то этот блог я больше всего и читаю. Если его полистать, то можно с легкостью найти как написать свой веб-сервер «на сокетах», с применением boost.asio или чего-то еще. Некоторое время назад я так же публиковал свой пост о создании подобного http-сервера как пример решения тестового задания. Но не ограничившись полученным оным и интереса ради я сделал сравнения с libevent и boost.asio разработками. А тестовое задание как-таковое отказался выполнять.
Для себя как-то по работе я рассматривал 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++ и о результатах написать.
Всем спасибо за внимание!
Материалы
Автор: NYM