Свой http-сервер менее чем в 40 строк кода на libevent и C++11

  • Tutorial
Просматривая временами Хабр, я периодически встречаю посты на тему создания собственного веб-сервера на 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.

Код однопоточного сервера менее чем в 40 строк:
#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 систем с небольшой вариацией параметров.
ab -c 1000 -k -r -t 10 http://127.0.0.1:5555/
Server Software:
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

ab -c 1000 -r -t 10 http://127.0.0.1:5555/
Server Software:
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.

Тестирование многопоточного сервера проведено с теми же параметрами, что и предыдущего примера.
ab -c 1000 -k -r -t 10 http://127.0.0.1:5555/
Server Software:
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

ab -c 1000 -r -t 10 http://127.0.0.1:5555/
Server Software:
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 и отправку ответа, а его методы дают возможность получать данные из входящего запроса и формировать ответ.

Интерфейс IHttpRequest
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++ зло, а так же в небольших дозировках — бальзам; гетерогенные решения обладают большей жизнеспособностью).
DECLARE_STRING_CONSTANT
#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 можно заглянуть и под капот этой самой обертки.
Класс HttpServer
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 (отдает файлы из указанной директории) и выводит на экран с какого хоста был сделан запрос и с какой страницы был осуществлен переход на ресурс, обрабатываемый сервером.
Финальная версия простого http-сервера
#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-серверов. Все исходные файлы всех приведенных примеров доступны на github. На базе приведенных примеров построен http server из еще одного моего домашнего проекта.
Результат тестирования:
ab -c 1000 -k -r -t 10 http://localhost:8888/libevent_test_http_srv.zip
Server Software: test
Server Hostname: test
Server Port: 8888

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

Две с небольшим тысячи обработанных запросов на получение архива с исходными файлами примеров поста за десять секунд на уже весьма скромной конфигурации железа…

Всем спасибо за внимание!

Материалы



Поделиться публикацией
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 29

    +10
    C++, как бы, и не причем. Вы же сишный libevent используете.
      +10
      Особенно сишной выглядит вот эта часть:
      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>");
            });
      
        +1
        Это пример из кода двух файлов, которые явно не отражают заголовок про 40 строк =) Зачем же так ревностно минусовать?
      +1
      Пробовали muduo?
        0
        Пока нет. Спасибо! Посмотрю обязательно.
        • НЛО прилетело и опубликовало эту надпись здесь
            +2
            извращение)
          +2
          однопоточный сервер в любой папке: python -m SimpleHTTPServer
            +3
            Во всех топиках с простыми C++ серверами наблюдаю данный комментарий :) Ничего не имею против питона, наоборот — люблю его за простоту разработки! Однако очень хочется увидеть сравнительный анализ производительности (запросы в секунду, среднее время ответа под нагрузкой, количество потребляемых ресурсов (CPU, ОЗУ, IO, и т.д.)) питонячего SimpleHTTPServer и вариаций на C++ из 40 строк.
              +8
              А может просто хватит этой глупой фаллометрии? Раньше мастера удивляли играми на JavaScript в «40 строк», теперь вот уже вторую статью про «40-строчный» вебсервер вижу за последнюю неделю. Одна история замечательнее другой просто.

              И ладно-бы там действительно было 40 строк — эдакий эстетический интерес. Дык оба автора ради этих «40 строк» — цепляют библиотеки размером существенно больше. Раз их не видно — значит их нет? Я понимаю, что заголовок этой статьи, возможно, имел другой контекст — но, блин, при чём тут количество строк?

              Статья действительно полезная, но за «40 строк» захотелось кого-нибудь ударить.
                +1
                Зашел сюда написать подобный комментарий.
            +1
            Эцсамое. Об этом в доке не пишут, но даже если переключить libevent в многопоточный режим, evhttp не является потокобезопасным. То есть, с запросами можно работать только из того потока, в котором создан слушатель.
              +1
              Используй вы форматирование в стиле K&R, то уместились бы в тренд «30-ти строк кода».
                0
                >Чем хороша libevent в отличии от, например, libev и boost.asio, так это тем, что она имеет свой встроенный http-сервер
                У libev есть слой эмуляции libevent, предоставляющий, в том числе, тот же http-сервер.
                  0
                  Даже интересно, что же ещё придумают уместить в n строк…
                    +3
                    Например, мы (я и Вы) умудрились уместить абсолютно бесполезный комментарий в 1 строку. :)
                      +2
                      Это не считается, это уже давно не новое :)
                    0
                    Cпасибо за статью.
                    А где можно нормальный туториал по libevent почитать? На их сайте как-то маловато.
                      0
                      Честно, не знаю где можно найти еще. На сайте есть некоторая информация, а так же в сети можно найти множество примеров. К тому же мне было достаточно просто понять что и к чему благодаря комментариям к функциям libevent. Можно многое узнать просто просмотрев несколько комментариев с описание назначения и параметров той или иной функции. У libevent это весьма хорошо сделано. Большую часть информации о ней я почерпнул из нее же (описаний функций и параметров во включаемых файлах). Найдете что-то стоящее вне официального сайта, поделитесь :)
                        +1
                        Туториал по libevent от автора бибиотеки: http://www.wangafu.net/~nickm/libevent-book/
                        +2
                        Сам писал нечто подобное, когда нужен был встроенный сервер.

                        Сейчас смотрю на libuv, стабильный, быстро развивающийся и предоставляющий более богатый функционал по сравнению с libevent. Если добавить к нему http-parser, легко написать свой веб-сервер gist.github.com/utaal/1195428
                          0
                          Можно не самостоятельно добавлять, а взять libwebsite.
                            0
                            libuv — это не плохая надстройка над libev
                              0
                              Была когда-то. Вроде бы постепенно переписали с нуля.
                            0
                            ну evhttp всё же отдельный кусок libevent, в котором по сути всё уже готово, так что заголовок статьи должен быть: «Простой HTTP сервер на базе библиотеки с готовым HTTP сервером, в 40 строк».

                            Ну и зачем брать заведомо ущербное решение? Если пойдет что попроще, то хватит и обычного select, если надо помощнее, то уж не стоит брать однопоточное решение. Чем не устраивает boost::asio? Хотя видимо потому, что там http сервер не реализован из коробки?
                              0
                              спасибо за статью,
                              а не пробовали использовать libevent в многопоточном режиме?
                              было бы очень интересно, нормальных примеров не нашел
                                0
                                Уточните, пожалуйста, что Вы имели ввиду под примерами в многопоточном режиме. Как я могу судить из Вашего комментария второй пример поста (многопоточный сервер) и финальная версия этому критерию не отвечают по Вашим словам.
                                0
                                Автор, каким компилятором собирать Ваш проект?
                                У меня gcc 4.6.3 не понимает g++11, поменял на g++0x вот что пишет:

                                [test@ip-10-179-38-242 sample3]$ make
                                g++ -Wall -std=c++0x  -O3 -g0 -c http_server.cpp -o http_server.o
                                In file included from http_server.h:6:0,
                                                 from http_server.cpp:1:
                                tools.h:10:5: error: expected initializer before ‘:’ token
                                In file included from /usr/lib/gcc/x86_64-amazon-linux/4.6.3/../../../../include/c++/4.6.3/memory:73:0,
                                                 from http_request.h:8,
                                                 from http_server.h:7,
                                                 from http_server.cpp:1:
                                /usr/lib/gcc/x86_64-amazon-linux/4.6.3/../../../../include/c++/4.6.3/typeinfo:42:37: error: expected ‘}’ before end of line
                                /usr/lib/gcc/x86_64-amazon-linux/4.6.3/../../../../include/c++/4.6.3/typeinfo:42:37: error: expected declaration before end of line
                                make: *** [http_server.o] Error 1
                                

                                  +1
                                  Я собирал на g++ 4.8.1. С большой вероятностью все соберется на gcc4.7.2 или даже на gcc4.7, ниже уже маловероятно. Версии 4.6.x еще слабовато поддерживают многие плюшки C++11.

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое