Свій http-сервер менш ніж в 40 рядків коду на libevent і C + +11

  Переглядаючи часом Хабр, я періодично зустрічаю пости на тему створення власного веб-сервера на 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 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 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 31277
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 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 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 37247
Waiting: 3 30 8.7 30242
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-серверів. Останній приклад цього поста взятий за основу, в яку нашвидкуруч додано трохи допоміжного функціонала і реалізований сервер, на якому і розташовані вихідні файли всіх наведених прикладів у вигляді zip-архіву . Завантажуючи архів з прикладами з сервера, розробленого на їх же основі можна подивитися на життєздатність сервера. Наприкінці минулого року мною був опублікований пост «Система плагінів як вправу на C + + 11» . В личку були питання наявності якоїсь ще інформації, моє бажання розвивати проект, підтримки і т. д. і в певний момент я вирішив організувати невеликий інформаційний ресурс для описаної системи плагінів. Дизайнер з мене ніякий, так що за дизайн сильно прошу не картати :) Накидав трохи статичного контенту для цього ресурсу і треба було його чимось віддавати. Так, можна було підняти щось з nginx або apache, а може і ще щось. Але мені було цікаво як працюватимуть раніше розроблені мною тестові приклади http-серверів, які я описував в пості про рішення тестового завдання з написанням «простенького» http-сервера. І на одному з таких прикладів, розробленому «на сокетах» (як іноді це люблять називати в тестових завданнях і т. д.) з власним розбором протоколу і т.д. сайт був доступний майже місяць. Пропрацював успішно і без падінь. Хабраеффекта звичайно ж не було. Та й звідки йому взятися. А з написанням цього посту я перевів видачу контенту про систему плагінів на сервер, розроблений на прикладах цього ж поста, там же розмістив і самі приклади описуваних серверів. Чи витримає такий сервер хабраеффекта? Не знаю. Цілком може бути, що на рівні додатку і витримає, а ось чи витримає моя VDS'ка (2 ядра, 1Гб оперативної пам'яті, ОС — Ubuntu 12.04 64bit) вже важко сказати. А чи можливий сам хабраеффект так само не можу на це сподіватися. Поки пропоную трохи протестувати отриманий сервер з урахуванням мережі і розташування на віддаленому віртуальному сервері, а не локально на моїй же машині. Результат тестування:
 ab-c 1000-k-r-t 10 t-boss.ru/libevent_test_http_srv.zip Server Software: t-boss
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 + + і про результати написати.
 
Всім дякую за увагу!
 
 

Матеріали

  
  
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.