- PVSM.RU - https://www.pvsm.ru -

Не смотря на то, что моя работа в данный момент связана с настольными приложениями, меня в последнее время заинтересовали «серверные технологии». Некоторый серфинг интернета, чтение man’ов и попытки написания чего-то сервероподобного для себя — это все, что было сделано за последнее время, так как нет четкой цели. Придумав себе интересную задачу можно не плохо поднять свой уровень навыков.
В один из моментов, когда мне стало окончательно скучно на работе от рутины, я поставил галку на одном из известных ресурсов поиска работы, что не против посмотреть на рынок, вдруг, что интересное попадется… Как результат некоторое количество предложений с вакансиями, на тему: «Возможно это Вас заинтересует». Среди таких предложений и пришло предложение с тестовым заданием. Тестовое задание – написать WebServer’а на C++ под Linux с реализацией HTTP–протокола; простенький…
Взяв фразу из тестового задания и вбив ее в Google, я нашел еще отзывов о таком не самом коротком тестовом задании на форуме RSDN [1]. Задание было один в один лежащему в моем почтовике. Как задание выполнять его не стал. Принцип прост: тестовое задание если и стоит выполнять, то оно должно быть не более чем на 4 часа рабочего времени рассчитано. Но попробовать в деле все, о чем было прочитано и местами опробовано было интересно. Это и стало стимулом, т.е. постановкой интересной задачи. К какой конторе относится это задание, сказать не могу, так как оно пришло от кадрового агенства, но и не так это важно.
В этой статье будут рассмотрены подходы и соответствующие API, которые мною были найдены по данной тематике. Приведу несколько реализаций WebServer’а с использованием разных подходов и инструментов, проведено сравнительное тестирование полученных «поделок». Статья не рассчитана на «бородатых» серверописателей, но как обзор людям, сталкнувшимся с аналогичными задачами (не только в тестах) вполне может быть полезна. Буду рад конструктивным комментариям всех, в особенности от «бородатых» серверописателей, так как написание статьи — это не только поделиться опытом, но, вполне может быть, и пополнить его для себя…
Результатом рассмотрения средств серверописания стали API *nix систем, API Windows (почему бы не посмотреть, хоть и нет этой платформы в целях данной задачи) и такие библиотеки, как boost.asio и libevent.
Сокеты Беркли [2] хоть и универсальный, портируемый механизм, но не совсем он однозначно портируем. Так в некоторых платформах close для закрытия сокета, а в некоторых closesocket; в некоторых надо инициализировать библиотеку (Windows – WSAStartup/WSACleanup), в некоторых нет; где-то описателем сокета является тип int, а где-то SOCKET и прочие мелкие различия. Получается, если не применять всякие подходы кроссплатформенного программирования такие как pImpl и прочие, то один и тот же код не будет работать, а зачастую, и собираться на разных платформах одинаково. Все эти мелочи скрыты в библиотеках типа boost.asio [3], libevent [4] и аналогичных. Кроме того подобные библиотеки используют и более специфичные методы API соответствующей платформы для реализации наиболее оптимальной работы с сокетами, предоставляя пользователю удобный интерфейс без намеков на платформу.
Если взять очень обобщенно работу сервера, то получается такая последовательность действий:
Все пункты, кроме пятого, относительно схожи и малоинтересны, а вот механизмов реагирования на события, происходящие на сокете много и большая их часть специфична для каждой платформы.
Если посмотреть на Windows, то можно увидеть следующие методы:
Есть отличная книга по разработке сетевого программного обеспечени под Windows – «Программирование в сетях Microsoft Windows».
Теперь, если посмотреть на *nix состемы, то там тоже есть не малый набор селекторов событий:
Кроме функций для ожидания событий на описателях есть еще некоторые небольшие, но очень полезные вещи:
Про использование библиотек лучше сказать кодом для начала, что и будет сделано ниже на примерах boost.asio и libevent. boost.asio весьма сильно упрощает разработку сетевых приложений, libevent – это серверная классика.
Какой бы механизм не был бы выбран для реагирования на сетевые события epoll, poll, select, еще много остается других нюансов.
Один из самых первых вопросов при реализации многопоточного сервера – это выбор количества потоков. Большинство из тех, кому когда-то приходилось по быстрому собрать свой «сервер на сокетах» в учебных или псевдобоевых целях, выбирал стратегию «Одно соединение – один поток». В этом подходе есть как свои плюсы, так и минусы. Самым большим плюсом является простота разработки. Минусов много: большое количество затрачиваемых системных ресурсов, много синхронизационных действий (кода, чего-то с чем-то синхронизирующего). Однако такой подход не плох для HTTP сервера с точки зрения синхронизации, так как особых пересечений между сессиями нет. Но, не смотря на простоту разработки, эту стратегию я не стал рассматривать для своей реализации. Имеются разные рекомендации по оптимальному выбору количества потоков – это по количеству процессоров / ядер процессора в системе, по тому же количеству, но с некоторым коэффициентом. В предлагаемой реализации количество рабочих потоков – параметр опциональный, задаваемый пользователем при запуске сервера. Для себя же было решено, что количество рабочих потоков равно количеству процессоров / ядер умноженному на два.
В текущем контексте под рабочим потоком стоит понимать поток, обрабатывающий запросы пользователя. Кроме этих потоков было задействовано еще два: слушающий поток и основной. Слушающий поток — слушает серверный сокет и принимает входящие соединения, далее они помещаются на обработку в очередь к рабочим потокам. Основной поток – запускает сервер и ждет определенного действия от пользователя для его остановки.
Второй вопрос, который меня интересовал при реализации данного примера – это в каких потоках и как обрабатывать сетевые события при использовании epoll’а. Первое, что мне пришло в голову – это в одном потоке реагировать на все события, отслеживаемые epoll’ом, а их обработку делать в других потоках (рабочих), передавая их туда посредством некоторой очереди. Т.е. один поток отслеживает и входящие события на слушающем сокете, и события о приходе данных на принятые соединения и события о закрытии соединения. Получил событие, положил в очередь, посигналил рабочим потокам, рабочие потоки вызывали accept для принятие нового соединения, добавляли в пул наблюдаемых сокетов в epoll, read, write и close для соединений. Решение ошибочное, так как пока идет обработка одного события, скажем чтение данных с сокета, на этот сокет уже в очереди может лежать событие о закрытия сокета. Конечно, чтение завершится с ошибкой, но вот до действий по очистке всех ресурсов, связанных с соединением, дело дойдет не сразу, а только при вычитывании данного события из очереди. Многие события по закрытию сокета просто терялись в моей реализации. Реализация становилась сложнее, количество мест синхронизации росло и при странных условиях были падения. Падения были по иной причине. С каждым сокетом в структуре epoll-события как пользовательские данные привязывался указатель на объект сессии, который и отвечал за всю работу с клиентом, пока он не закроется. Так как последовательность обработки событий становилась сложнее, отсюда и падения, так как объект, привязанный как пользовательские данные уже был удален (например, при закрытии сессии не по событию извне, а по логике самой сессии внутри нее), а в очереди было еще событие на обрабоку с уже битым указателем. Получив некоторый такой опыт «на граблях» от первой пришедшей идеи, была принята иная стратегия: основной слушающий поток посредством epoll’а реагирует только на события слушающего сокета, принимает входящие соединения и, если их число более чем разрешено для очереди ожидания, то закрывает их, в противном случае помещает принятые соединения в очередь для обработки; рабочие потоки вычитывают эту очередь помещают этот сокет уже в свой epoll набор, за которым и наблюдают. Получается, что рабочие потоки работают со своим описателем epoll’а и все делается в рамках одного потока: помещение в epoll, реакция на события прихода данных, чтение/запись, закрытие (удаление из epoll’а происходит автоматически на уровне системы при закрытии описателя). Как результат такой организации – всего один синхронизационный примитив для защиты очереди входящих соединений. С одной стороны, в эту очередь пишет только слушающий поток, а с другой, из нее выбирают принятые соединения рабочие потоки. Одной проблемой меньше. Осталось отказаться от связывания указателя на объект-сессию с пользовательскими данными структуры epoll’а. Решение: использовать ассоциативный массив; ключ – описатель сокета, данные – объект-сессия. Это позволяет работать с сессиями не только при приходе события, когда мы имеем возможность получить пользовательские данные из события epoll, но и когда по некоторой логике надо, например, закрыть некоторые соединения по таймауту (пул соединений доступен).
Первый вариант, написанный полностью в одном файле и в стиле C# / Java разработчика (без разделения на объявления и определения) у меня оказался более 1800 строк кода. Многовато для тестового задания, несмотря на то, что реализация именно HTTP протокола минимальна, самый минимум на обработку GET/HEAD без чего либо еще и с минимумом обработки параметров HTTP-заголовка. Не в этом суть. Оговорюсь еще раз, что тестовое задание было всего-лишь «пинком» к тому, чтобы что-то попробовать. Основной интерес для меня представляло в данном решении не реализация HTTP-протокола, а реализация многопоточного сервера, управление соединениями и сессиями (под сессией можно понимать некоторую логическую структуру данных с алгоритмом обработки, связанную с соединением).
Разбив этот монструозный файл и местами причесав реализацию, вот что у меня получилось:
class TCPServer
: private Common::NonCopyable
{
public:
TCPServer(InetAddress const &locAddr, int backlog, int maxThreadsCount,
int maxConnectionsCount, UserSessionCreator sessionCreator);
private:
typedef std::tr1::shared_ptr<Common::IDisposable> IDisposablePtr;
typedef std::vector<IDisposablePtr> IDisposablePool;
Private::ClientItemQueuePtr AcceptedItems;
IDisposablePool Threads;
};
Это, пожалуй, самая короткая реализация класса сервера, которую мне приходилось писать. Данный класс всего лишь создает несколько потоков: слушающий и несколько рабочих, и является их держателем.
TCPServer::TCPServer(InetAddress const &locAddr, int backlog, int maxThreadsCount,
int maxConnectionsCount, UserSessionCreator sessionCreator)
: AcceptedItems(new Private::ClientItemQueue(backlog))
{
int EventsCount = maxConnectionsCount / maxThreadsCount;
for (int i = 0 ; i < maxThreadsCount ; ++i)
{
Threads.push_back(IDisposablePtr(new Private::WorkerThread(
EventsCount + (i <= maxThreadsCount - 1 ? 0 : maxConnectionsCount % maxThreadsCount),
AcceptedItems
)));
}
Threads.push_back(IDisposablePtr(new Private::ListenThread(locAddr, backlog, AcceptedItems, sessionCreator)));
}
тоже не велика. Оба класса как
class ListenThread
: private TCPServerSocket
, public Common::IDisposable
{
public:
ListenThread(InetAddress const &locAddr, int backlog,
ClientItemQueuePtr acceptedClients,
UserSessionCreator sessionCreator)
: TCPServerSocket(locAddr, backlog)
, AcceptedClients(acceptedClients)
, SessionCreator(sessionCreator)
, Selector(1, WaitTimeout, std::tr1::bind(&ListenThread::OnSelect,
this, std::tr1::placeholders::_1, std::tr1::placeholders::_2))
{
Selector.AddSocket(GetHandle(), Network::ISelector::stRead);
}
private:
enum { WaitTimeout = 100 };
ClientItemQueuePtr AcceptedClients;
UserSessionCreator SessionCreator;
SelectorThread Selector;
void OnSelect(SocketHandle handle, Network::ISelector::SelectType selectType)
{
//Принятие нового соединения, создание объекта-сессии и помещение его в очередь
}
};
, так и
class WorkerThread
: private Common::NonCopyable
, public Common::IDisposable
{
public:
WorkerThread(int maxEventsCount, ClientItemQueuePtr acceptedClients)
: MaxConnections(maxEventsCount)
, AcceptedClients(acceptedClients)
, Selector(maxEventsCount, WaitTimeout, std::tr1::bind(&WorkerThread::OnSelect,
this, std::tr1::placeholders::_1, std::tr1::placeholders::_2),
SelectorThread::ThreadFunctionPtr(new SelectorThread::ThreadFunction(std::tr1::bind(
&WorkerThread::OnIdle, this))))
{
}
private:
enum { WaitTimeout = 100 };
typedef std::map<SocketHandle, ClientItemPtr> ClientPool;
unsigned MaxConnections;
ClientItemQueuePtr AcceptedClients;
ClientPool Clients;
SelectorThread Selector;
void OnSelect(SocketHandle handle, Network::ISelector::SelectType selectType)
{
//Реакция на события, происходящие на наблюдаемых описателях сокетов (чтение данных, их обработка, закрытие сокетов)
}
void OnIdle()
{
//Выполнение фоновых операций сессий. Вычитывание данных из очереди принятых соединений и помещение объектов-сессий в epoll.
}
};
используют класс потока обработки событий
class SelectorThread
: private EPollSelector
, private System::ThreadLoop
{
public:
using EPollSelector::AddSocket;
typedef System::Thread::ThreadFunction ThreadFunction;
typedef std::tr1::shared_ptr<ThreadFunction> ThreadFunctionPtr;
SelectorThread(int maxEventsCount, unsigned waitTimeout, ISelector::SelectFunction onSelectFunc,
ThreadFunctionPtr idleFunc = ThreadFunctionPtr());
virtual ~SelectorThread();
private:
void SelectItems(ISelector::SelectFunction &func, unsigned waitTimeout, ThreadFunctionPtr idleFunc);
};
. Этот поток использует
class EPollSelector
: private Common::NonCopyable
, public ISelector
{
public:
EPollSelector(int maxSocketCount);
~EPollSelector();
virtual void AddSocket(SocketHandle handle, int selectType);
virtual void Select(SelectFunction *function, unsigned timeout);
private:
typedef std::vector<epoll_event> EventPool;
EventPool Events;
int EPoll;
static int GetSelectFlags(int selectType);
};
для организации реакций на события, происходящие на описателях принятых соединений.
Если посмотреть на исходный класс сервера, то видно, что последним параметром передается функтор для создания классов-пользовательских сессий. Пользовательская сессия — это реализация интерфейса
struct IUserSession
{
virtual ~IUserSession() {}
virtual void Init(IConnectionCtrl *ctrl) = 0;
virtual void Done() = 0;
virtual unsigned GetMaxBufSizeForRead() const = 0;
virtual bool IsExpiredSession(std::time_t lastActionTime) const = 0;
virtual void OnRecvData(void const *buf, unsigned bytes) = 0;
virtual void OnIdle() = 0;
};
В зависимости от реализации данного интерфейса можно реализовывать разные протоколы. Методы Init и Done вызываются при начале сессии и при ее завершении соответственно. GetMaxBufSizeForRead должен возвращать максимальный размер буфера, который будет выделяться при операциях чтения данных. Прочитанные данные приходят в OnRecvData. Для того чтобы сессия могла сказать, что она истекла по времени, надо реализовывать IsExpiredSession соответствующим способом. OnIdle вызывается в промежутках между какими-то действиями, тут реализация сессии может выполнять какие-то фоновые действия и пометить себя как предназначенная для закрытия через интерфейс
struct IConnectionCtrl
{
virtual ~IConnectionCtrl() { }
virtual void MarkMeForClose() = 0;
virtual void UpdateSessionTime() = 0;
virtual bool SendData(void const *buf, unsigned *bytes) = 0;
virtual bool SendFile(int fileHandle, unsigned offset, unsigned *bytes) = 0;
virtual InetAddress const& GetAddress() const = 0;
virtual SocketTuner GetSocketTuner() const = 0;
};
Интерфейс IConnectionCtrl передается для того, чтобы пользовательская сессия могла отправлять данные в сеть (методы SendData и SendFile), пометить себя как предназначенная для закрытия (метод MarkMeForClose), говорить о том что «она жива» (метод UpdateSessionTime; обновляет время, которое приходит в IsExpiredSession), так же сессия может получить адрес входящего соединения (метод GetAddress) и объект SocketTuner для настроек сокета — текущего соединения (метод GetSocketTuner).
Реализация HTTP-протокола находится в классе HttpUserSession. Как и говорил выше, реализация HTTP для меня была не самой интересной и приоритетной, поэтому над ней много не думал; думал ровно столько, сколько хватило для написания того, что получилось :)
Реализация на libevent пока для меня фаворит. Эта библиотека дает возможность организовать асинхронный ввод-вывод и скрыть от разработчика многие тонкости сетевого программирования. Позволяет реализовать работу с сырыми данными, вешая функции обратного вызова на прием, передачу данных и иные события, отправлять асинхронно данные. Кроме низкоуровневой работы с данными есть и более высокоуровневые протоколы. libevent имеет встроенный HTTP-сервер, что дает возможность абстрагироваться от разбора заголовков запросов и формирования тех же заголовков ответов. Есть возможность реализовать RPC средствами библиотеки и прочие возможности.
Если реализовывать HTTP-сервер с использованием встроенного, то последовательность будет примерно такой:
Все получается очень красиво в одном потоке, но, как оказалось, сделать многопоточный сервер тоже не сложно. Если использовать boost::thread или свой кроссплатформенный класс, инкапсулирующий работу потока, или что-то аналогичное, то можно получить полностью кроссплатформенное решение, так как libevent кроссплатформенная библиотека. В своей же реализации я возьму некоторую обертку только над потоками для Linux. Но это не так важно.
Основной поток для каждого рабочего потока должен создать свои описатели, т.е. выполнить шаги 1-5. Рабочие потоки должны только крутить циклы обработки сообщений – шаг 6. Шаг 7 будет выполняться в каждом рабочем потоке. Обобщая, можно сказать: создаем один слушающий сокет и навязываем его обработку нескольким рабочим потокам.
Так в своей реализации с учетом того, что у меня уже готовы некоторые примитивы для потоков, файлов и разбора командной строки у меня получился HTTP-сервер с поддержкой только GET метода всего примерно около 200 строк в стиле C#/Java. Такое сокращение работы по написанию кода с наличием полного контроля происходящего не может не радовать. К тому же субъективно, полученный сервер работает чуть быстрее, но посмотрим на тесты в конце…
#include <event.h>
#include <evhttp.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <vector>
#include <iostream>
#include <tr1/functional>
#include <tr1/memory>
#include "tcp_server_socket.h"
#include "inet_address_v4.h"
#include "thread.h"
#include "command_line.h"
#include "logger.h"
#include "file_holder.h"
namespace Network
{
namespace Private
{
DECLARE_RUNTIME_EXCEPTION(EventBaseHolder)
class EventBaseHolder
: private Common::NonCopyable
{
public:
EventBaseHolder()
: EventBase(event_base_new())
{
if (!EventBase)
throw EventBaseHolderException("Failed to create new event_base");
}
~EventBaseHolder()
{
event_base_free(EventBase);
}
event_base* GetBase() const
{
return EventBase;
}
private:
event_base *EventBase;
};
DECLARE_RUNTIME_EXCEPTION(HttpEventHolder)
class HttpEventHolder
: public EventBaseHolder
{
public:
typedef std::tr1::function<void (char const *, evbuffer *)> RequestHandler;
HttpEventHolder(SocketHandle sock, RequestHandler const &handler)
: Handler(handler)
, Http(evhttp_new(GetBase()))
{
evhttp_set_allowed_methods(Http, EVHTTP_REQ_GET);
evhttp_set_gencb(Http, &HttpEventHolder::RawHttpRequestHandler, this);
if (evhttp_accept_socket(Http, sock) == -1)
throw HttpEventHolderException("Failed to accept socket for http");
}
~HttpEventHolder()
{
evhttp_free(Http);
}
private:
RequestHandler Handler;
evhttp *Http;
static void RawHttpRequestHandler(evhttp_request *request, void *prm)
{
reinterpret_cast<HttpEventHolder *>(prm)->ProcessRequest(request);
}
void ProcessRequest(evhttp_request *request)
{
try
{
Handler(request->uri, request->output_buffer);
evhttp_send_reply(request, HTTP_OK, "OK", request->output_buffer);
}
catch (std::exception const &e)
{
evhttp_send_reply(request, HTTP_INTERNAL,
e.what() ? e.what() : "Internal server error.",
request->output_buffer);
}
}
};
class ServerThread
: private HttpEventHolder
, private System::Thread
{
public:
ServerThread(SocketHandle sock, std::string const &rootDir, std::string const &defaultPage)
: HttpEventHolder(sock, std::tr1::bind(&ServerThread::OnRequest, this,
std::tr1::placeholders::_1,
std::tr1::placeholders::_2))
, Thread(std::tr1::bind(&ServerThread::DispatchProc, this))
, RootDir(rootDir)
, DefaultPage(defaultPage)
{
}
~ServerThread()
{
IsRun = false;
}
private:
enum { WaitTimeout = 10000 };
bool volatile IsRun;
std::string RootDir;
std::string DefaultPage;
void DispatchProc()
{
IsRun = true;
while(IsRun)
{
if (event_base_loop(GetBase(), EVLOOP_NONBLOCK))
{
Common::Log::GetLogInst() << "Failed to run dispatch events";
break;
}
usleep(WaitTimeout);
}
}
void OnRequest(char const *resource, evbuffer *outBuffer)
{
std::string FileName;
GetFullFileName(resource, &FileName);
try
{
System::FileHolder File(FileName);
if (!File.GetSize())
{
evbuffer_add_printf(outBuffer, "Empty file");
return;
}
evbuffer_add_file(outBuffer, File.GetHandle(), 0, File.GetSize());
File.Detach();
}
catch (System::FileHolderException const &)
{
evbuffer_add_printf(outBuffer, "File not found");
}
}
void GetFullFileName(char const *resource, std::string *fileName) const
{
fileName->append(RootDir);
if (!resource || !strcmp(resource, "/"))
{
fileName->append("/");
fileName->append(DefaultPage);
}
else
{
fileName->append(resource);
}
}
};
}
class HTTPServer
: private TCPServerSocket
{
public:
HTTPServer(InetAddress const &locAddr, int backlog,
int maxThreadsCount,
std::string const &rootDir, std::string const &defaultPage)
: TCPServerSocket(locAddr, backlog)
{
for (int i = 0 ; i < maxThreadsCount ; ++i)
{
ServerThreads.push_back(ServerThreadPtr(new Private::ServerThread(GetHandle(),
rootDir, defaultPage)));
}
}
private:
typedef std::tr1::shared_ptr<Private::ServerThread> ServerThreadPtr;
typedef std::vector<ServerThreadPtr> ServerThreadPool;
ServerThreadPool ServerThreads;
};
}
int main(int argc, char const **argv)
{
if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
{
std::cerr << "Failed to call signal(SIGPIPE, SIG_IGN)" << std::endl;
return 0;
}
try
{
char const ServerAddr[] = "Server";
char const ServerPort[] = "Port";
char const MaxBacklog[] = "Backlog";
char const ThreadsCount[] = "Threads";
char const RootDir[] = "Root";
char const DefaultPage[] = "DefaultPage";
// Server:127.0.0.1 Port:5555 Backlog:10 Threads:4 Root:./ DefaultPage:index.html
Common::CommandLine CmdLine(argc, argv);
Network::HTTPServer Srv(
Network::InetAddressV4::CreateFromString(
CmdLine.GetStrParameter(ServerAddr),
CmdLine.GetParameter<unsigned short>(ServerPort)),
CmdLine.GetParameter<unsigned>(MaxBacklog),
CmdLine.GetParameter<unsigned>(ThreadsCount),
CmdLine.GetStrParameter(RootDir),
CmdLine.GetStrParameter(DefaultPage)
);
std::cin.get();
}
catch (std::exception const &e)
{
Common::Log::GetLogInst() << e.what();
}
return 0;
}
boost.asio — это часть boost'а, которая может помочь очень сильно сократить разработку сетевых приложений и к тому же кроссплатформенных. Библиотека скрывает от разработчика много рутины.
Реализацию HTTP-сервера на boost я не стал писать. Взял готовую из примеров к boost.asio. Пример многопоточного HTTP-сервера.HTTP Server 3 [5] Реализация данного примера вполне подойдет для тестирования в совокупности с примерами, приведенными выше.
Есть реализация HTTP-сервера для тестирования, но не плохо было бы немного об общих принципах… К сожалению, в отличии от libevent в boost.asio нет поддержки каких-то более высокоуровневых протоколов на подобиии HTTP и других. Библиотека скроет работу с сетью по TCP в данном случае, а вот реализацию HTTP придется делать разработчику самому: собирать и разбирать заголовки протокола.
Ниже приведу небольшой пример многопоточного эхо сервера с описанием, так как разбирать / собирать заголовки HTTP в свете данной темы мне было менее интересно. Последовательность шагов для создания многопоточного сервера с использованием boost.asio примерно такая:
И как с примером на libevent многопоточный сервер весьма просто создать из однопоточного, используя описанный выше набор шагов. В данном случае разница однопоточного и многопоточного сервера заключается лишь в том, что метод boost::asio::io_service::run нужно вызвать в каждом потоке для в многопоточной реализации.
#include <boost/noncopyable.hpp>
#include <boost/asio.hpp>
#include <boost/shared_ptr.hpp>
#include <boost/thread.hpp>
#include <boost/make_shared.hpp>
#include <boost/bind.hpp>
#include <boost/enable_shared_from_this.hpp>
#include <boost/array.hpp>
namespace Network
{
namespace Private
{
class Connection
: private boost::noncopyable
, public boost::enable_shared_from_this<Connection>
{
public:
Connection(boost::asio::io_service &ioService)
: Strand(ioService)
, Socket(ioService)
{
}
boost::asio::ip::tcp::socket& GetSocket()
{
return Socket;
}
void Start()
{
Socket.async_read_some(boost::asio::buffer(Buffer),
Strand.wrap(
boost::bind(&Connection::HandleRead, shared_from_this(),
boost::asio::placeholders::error,
boost::asio::placeholders::bytes_transferred)
));
}
void HandleRead(boost::system::error_code const &error, std::size_t bytes)
{
if (error)
return;
std::vector<boost::asio::const_buffer> Buffers;
Buffers.push_back(boost::asio::const_buffer(Buffer.data(), bytes));
boost::asio::async_write(Socket, Buffers,
Strand.wrap(
boost::bind(&Connection::HandleWrite, shared_from_this(),
boost::asio::placeholders::error)
));
}
void HandleWrite(boost::system::error_code const &error)
{
if (error)
return;
boost::system::error_code Code;
Socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, Code);
}
private:
boost::array<char, 4096> Buffer;
boost::asio::io_service::strand Strand;
boost::asio::ip::tcp::socket Socket;
};
}
class EchoServer
: private boost::noncopyable
{
public:
EchoServer(std::string const& locAddr, std::string const& port, unsigned threadsCount)
: Acceptor(IoService)
, Threads(threadsCount)
{
boost::asio::ip::tcp::resolver Resolver(IoService);
boost::asio::ip::tcp::resolver::query Query(locAddr, port);
boost::asio::ip::tcp::endpoint Endpoint = *Resolver.resolve(Query);
Acceptor.open(Endpoint.protocol());
Acceptor.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true));
Acceptor.bind(Endpoint);
Acceptor.listen();
StartAccept();
std::generate(Threads.begin(), Threads.end(),
boost::bind(
&boost::make_shared<boost::thread, boost::function<void ()> const &>,
boost::function<void ()>(boost::bind(&boost::asio::io_service::run, &IoService))
));
}
~EchoServer()
{
std::for_each(Threads.begin(), Threads.end(),
boost::bind(&boost::asio::io_service::stop, &IoService));
std::for_each(Threads.begin(), Threads.end(),
boost::bind(&boost::thread::join, _1));
}
private:
boost::asio::io_service IoService;
boost::asio::ip::tcp::acceptor Acceptor;
typedef boost::shared_ptr<Private::Connection> ConnectionPtr;
ConnectionPtr NewConnection;
typedef boost::shared_ptr<boost::thread> ThreadPtr;
typedef std::vector<ThreadPtr> ThreadPool;
ThreadPool Threads;
void StartAccept()
{
NewConnection = boost::make_shared<Private::Connection, boost::asio::io_service &>(IoService);
Acceptor.async_accept(NewConnection->GetSocket(),
boost::bind(&EchoServer::HandleAccept, this,
boost::asio::placeholders::error));
}
void HandleAccept(boost::system::error_code const &error)
{
if (!error)
NewConnection->Start();
StartAccept();
}
};
}
int main()
{
try
{
Network::EchoServer Srv("127.0.0.1", "5555", 4);
std::cin.get();
}
catch (std::exception const &e)
{
std::cerr << e.what() << std::endl;
}
return 0;
}
Пришло время сравнить полученные поделки…
Платформа, на которой все разрабатывалось и тестировалось — обычный ноутбук с 4Гб оперативной памяти и 2х ядерным процессором под управлением Ubuntu 12.04 desktop.
Первым делом ставлю утилиту для тестирования:
sudo apt-get install apache2-utils
и тестирую полученое таким образом:
ab -c 100 -k -r -t 5 "http://127.0.0.1:5555/test.jpg"
Для всех серверов было задано по 4 рабочих потока, 100 параллельных соединений, файл для передачи в 2496629 байт и оценочный интервал времени 5 секунд.
Результаты:
Server Software: MyTestHttpServer
Server Hostname: 127.0.0.1
Server Port: 5555
Document Path: /test.jpg
Document Length: 2496629 bytes
Concurrency Level: 100
Time taken for tests: 5.017 seconds
Complete requests: 2150
Failed requests: 0
Write errors: 0
Keep-Alive requests: 0
Total transferred: 5389312814 bytes
HTML transferred: 5388981758 bytes
Requests per second: 428.54 [#/sec] (mean)
Time per request: 233.348 [ms] (mean)
Time per request: 2.333 [ms] (mean, across all concurrent requests)
Transfer rate: 1049037.42 [Kbytes/sec] received
Connection Times (ms)
min mean[±sd] median max
Connect: 0 0 0.5 0 3
Processing: 74 226 58.2 229 364
Waiting: 2 133 64.8 141 264
Total: 77 226 58.1 229 364
Server Software:
Server Hostname: 127.0.0.1
Server Port: 5555
Document Path: /test.jpg
Document Length: 2496629 bytes
Concurrency Level: 100
Time taken for tests: 5.008 seconds
Complete requests: 1653
Failed requests: 0
Write errors: 0
Keep-Alive requests: 1653
Total transferred: 4263404830 bytes
HTML transferred: 4263207306 bytes
Requests per second: 330.05 [#/sec] (mean)
Time per request: 302.987 [ms] (mean)
Time per request: 3.030 [ms] (mean, across all concurrent requests)
Transfer rate: 831304.15 [Kbytes/sec] received
Connection Times (ms)
min mean[±sd] median max
Connect: 0 53 223.3 0 1000
Processing: 3 228 275.5 62 904
Waiting: 0 11 42.5 5 639
Total: 3 280 417.9 62 1864
Server Software:
Server Hostname: 127.0.0.1
Server Port: 5555
Document Path: /test.jpg
Document Length: 2496629 bytes
Concurrency Level: 100
Time taken for tests: 5.001 seconds
Complete requests: 639
Failed requests: 0
Write errors: 0
Keep-Alive requests: 0
Total transferred: 1655047414 bytes
HTML transferred: 1654999464 bytes
Requests per second: 127.78 [#/sec] (mean)
Time per request: 782.584 [ms] (mean)
Time per request: 7.826 [ms] (mean, across all concurrent requests)
Transfer rate: 323205.36 [Kbytes/sec] received
Connection Times (ms)
min mean[±sd] median max
Connect: 0 0 1.1 0 4
Processing: 286 724 120.0 689 1106
Waiting: 12 364 101.0 394 532
Total: 286 724 120.0 689 1106
Результаты сведены в таблице
| epoll | libevent | boost.asio | |
| Complete requests | 2150 | 1653 | 639 |
| Total transferred (bytes) | 5389312814 | 4263404830 | 1655047414 |
| HTML transferred (bytes) | 5388981758 | 4263207306 | 1654999464 |
| Requests per second [sec] (mean) | 428.54 | 330.05 | 127.78 |
| Time per request [ms] (mean) | 233.348 | 302.987 | 782.584 |
| Transfer rate [Kbytes/sec] received | 1049037.42 | 831304.15 | 323205.36 |
Есть три вида лжи: ложь, наглая ложь и статистика. Хотя, надо признать, результат меня не может не радовать. Думаю не стоит уделять особое внимание полученным результатам, а можно на них оглядываться как на некоторую опорную информацию, которая может будет полезна при принятии решения в выборе инструмента разработки своего серверного программного обеспечения. Для верности результатов желательно устраивать многократный прогон на серверном железе, клиентами, запущенными на других машинах сети и т.д.
100 параллельных запросов — это казалось бы мало, но вполне достаточно для проведения тестирования в таких скромных условиях. Конечно, хотелось бы проверять результаты на тысячах параллельных запросах, но тут есть уже иные факторы. Один из таких факторов — это количество одновременно открытых файловых описателей процесс. Узнать и задать некоторые параметры процесса можно вызовами функций getrlimit и setrlimit. Для того, чтобы узнать сколько на процесс выделяется файловых описателей можно вызвать getrlimit с флагом RLIMIT_NOFILE структуры rlimit. Для своей операционной системы — это 1024 файловых дескриптора на процесс по умолчанию и 4096 максимум, который можно установить на процесс. По этому критерию сильно не разгонишься… Как вариант, для того, чтобы ваш сервер в одном процессе мог работать с большим количеством файловых дескрипторов можно настроить систему соответствующим образом. Есть хорошее описание в статье Увеличение производительности сокета в Linux [6]
Писать свой WebServer на «голых» сокетах однозначно интересно, но не надо забывать, что дьявол кроется в деталях. По мере реализации эти самые детали как снежный ком все больше накатывают и накатывают. Не смотря на мою огромную любовь к велосипедостроительной промышленности в области информационных технологий, я бы все-таки этот подход оставил на случай, когда нужно написать что-то очень специфичное, пожертвовать какими-то обобщениями и чем-то более высокоуровневым в пользу достижения максимального быстродействия сервера. Код, приложенный к статье с реализацией на epoll еще можно совершенствовать и совершенствовать. Доведение до промышленной реализации и поддержка такой разработки будет весьма дорога, соответственно, планируемая отдача от такой системы должна быть тоже не малой. Но «наглая ложь», приведенная в сводной таблице выше не может не радовать, не смотря на то, что сервер был написан на C++, а не на чистом C, не брезговал в его реализации использованием stl, исключениями и писался он без больших обдумываний каждого из шагов.
Как уже и было отмечено выше, фаворитом для меня оказалась библиотека libevent, весьма проста для быстрого старта, дает очень хорошие результаты в производительности, кроссплатформенная, много скрывает рутины. Для большинства проектов я бы ее рассматривал в первую очередь.
По моему субъективному мнению, boost весьма своеобразен. Он дает много плюсов в разработке различного программного обеспечения и местами очень привлекателен. boost.asio дает достаточно высокий уровень абстракции от большинства необходимых вещей при разработке сетевого программного обеспечения. Очень хотелось бы услышать мнения «бывалых» о применении данной библиотеки при разработке именно серверного программного обеспечения и, желательно, с высокой нагрузкой.
Есть еще интересный механизм асинхронного ввода вывода в Linux (aio) [7], но пока не хватило времени сделать на нем реализацию на все тех же «голых» сокетах для сравнения с иными реализациями.
Весь код с минимальными сборочными файлами доступен в SVN [8]. Код, конечно можно еще совершенствовать и совершенствовать. Но! Обострившийся перфекционизм может или сильно затянуть реализацию чего либо, или сделать ее вообще не достижимой. Согласно статье «Разработка через страдание» [9], первое, что нужно сделать — сделать чтобы работало, второе — было красиво, третье — работало быстро. Приведенный код прошел первую стадию и понемногу задел вторую и третью :)
Так «небольшое» тестовое задание стало для меня интересным стимулом к некоторому обзору API операционных систем и библиотек.
Спасибо за внимание!
Автор: NYM
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka/18351
Ссылки в тексте:
[1] RSDN: http://www.rsdn.ru/forum/job/4795893.flat
[2] Сокеты Беркли : http://ru.wikipedia.org/wiki/%D0%A1%D0%BE%D0%BA%D0%B5%D1%82%D1%8B_%D0%91%D0%B5%D1%80%D0%BA%D0%BB%D0%B8
[3] boost.asio: http://www.boost.org/doc/libs/1_51_0/doc/html/boost_asio.html
[4] libevent: http://libevent.org/
[5] HTTP Server 3: http://www.boost.org/doc/libs/1_51_0/doc/html/boost_asio/examples.html#boost_asio.examples.http_server_3
[6] Увеличение производительности сокета в Linux: http://www.ibm.com/developerworks/ru/library/l-hisock/index.html
[7] асинхронного ввода вывода в Linux (aio): http://www.kernel.org/doc/man-pages/online/pages/man7/aio.7.html
[8] SVN: http://web-srv-test.googlecode.com/svn/trunk/
[9] «Разработка через страдание»: http://habrahabr.ru/post/155959/
[10] Написание своего HTTP сервера с использованием libevent: http://incpp.blogspot.com/2009/05/http-libevent.html
[11] Boost network performance with libevent and libev: http://www.ibm.com/developerworks/aix/library/au-libev/
[12] MULTI-THREADED HTTPSERVER USING EVHTTP (LIBEVENT): http://kzk9.net/multi-threaded-httpserver-using-evhttp-libeve
[13] Boost application performance using asynchronous I/O: http://www.ibm.com/developerworks/linux/library/l-async/
[14] Настройка FreeBSD для обслуживания 100-200 тысяч соединений: http://webcrunch.ru/library/equipment/clusters/tuning-freebsd/
Нажмите здесь для печати.