пятница, 5 ноября 2010 г.

Использование OpenSSL

Часть 1. Сборка и подключение


Чтобы добавить поддержку шифрования в сетевое приложение можно воспользоваться открытой библиотекой OpenSSL.

1. Сборка библиотеки


Поскольку бинарные сборки библиотеки доступны не для всех архитектур — будем собирать из исходников. Тем более, что это позволит использовать подходящие флаги оптимизации.

1.1 Unix


Сборка под Unix достаточно тривиальна, особенно, если в нашем распоряжении есть менеджер пакетов. Но, на всякий случай, приведу последовательность сборки из исходников.
Качаем последнюю (или необходимую) версию по адресу http://www.openssl.org/source/ и распаковываем ее. На данный момент последняя версия — openssl-1.0.0a:

> tar xvpf openssl-1.0.0a.tar.gz

В каталоге ./openssl-1.0.0a/ запускаем скрипт конфигурации:

> ./config 


Собираем:
   
> make
   
И устанавливаем:
   
> sudo make install

     Проверить работоспособность и версию установленной библиотеки можно выполнив команду:
   

> openssl version
   

1.2 Windows

   
    Под Windows нам понадобится чуть больше движений. Для конфигурирования необходим Perl, который можно взять здесь (http://strawberryperl.com/) и для сборки — Visual Studio с компилятором C++.
    После установки перечисленных выше пакетов, можно приступать к сборке.
    В командной строке переходим в каталог openssl-1.0.0a и выполняем и запускаем Perl с указанием компилятора и каталога, в который будет установлена собранная библиотека:

    > perl Configure VC-WIN32 --prefix=c:\Temp\openssl
    > ms\do_ms
    Если последней строчкой будет: Configured for VC-WIN32 — значит все хорошо, если нет — ищем ошибку, исправляем и повторяем процедуру.
    Теперь компиляция. Запускаем Visual Studio 2008 Command Prompt в каталоге с openssl и запускаем сборку.
Чтобы получить DLL:

> nmake -f ms\ntdll.mak
> nmake -f ms\ntdll.mak install
   
Чтобы собрать статическую библиотеку:

> nmake -f ms\nt.mak
> nmake -f ms\nt.mak install

    2. Подключение

   
    После того, как библиотека была успешно собрана и установлена, ее можно начинать использовать. Нам понадобятся заголовочные файлы и сама библиотека.
В Unix это тривиальное -lssl во флагах линковщика, а в Windows необходимо указать путь к заголовочным файлам, собранной библиотеке и указать имена библиотек:

Additional Include Directories: c:/Temp/openssl/include
Additional Library Directories: c:/Temp/openssl/lib
Additional Dependencies: libeay32.lib ssleay32.lib


Теперь можно использовать функции OpenSSL в проекте. Как добавить шифрование в сервер и клиент я расскажу в следующей части.

пятница, 29 января 2010 г.

Исключения в C

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

Функции printf(), vprintf(), fprintf(), vfprintf(), sprintf(), vsprintf(), asprintf(), vasprintf() — возвращают количество записанных байт,
а функции snprintf() и vsnprintf()количество байт, которое могло бы быть записаным, если бы размер буфера был неограниченный! Эти функции гарантировано возвращают NULL-терминированную строку, если не произошло ошибки.

вторник, 1 декабря 2009 г.

Буферизированный ввод/вывод средствами libevent

Разрабатывая сетевые приложения, часто сталкиваешься с необходимостью работать с пакетами данных на уровне,
который находится чуть выше чем потоковый. Удобно, когда при отправке пакета информации на стороне клиента
сервер получает этот пакет за один раз и асинхронно относительно других клиентов.
Техническая реализация сетевого стека и API для работы с ним такова, что в асинхронном режиме работы с сокетами
вызов send(2) и read(2) не обязательно отправляют или принимают полный пакет, лишь только тот объем, который
в текущий момент доступен. В синхронном (блокирующем) режиме возникает другое ограничение — выполнение программы
приостанавливается, пока не будет отправлено или вычитано указанное количество байт, что мешает одновременно работать
сразу со многими подключениями в одном потоке. Решение этой проблемы можно найти, например, в библиотеке libevent.
Библиотека libevent, помимо классических обратных вызовов (callbacks) по событиям, предоставляет абстракцию,
которая буферизирует все вызовы чтения/записи (buffered event) [1]. Такой механизм обеспечивает автоматическое
заполнение и освобождение буферов при операциях чтения и записи.
Для того, чтобы начать работать с буферизированным вводом/выводом необходимо:
— инициализировать структуру bufferevent вызовом bufferevent_new(), указав функции для обратного вызова на события 
чтения (необязательно), записи (необязательно) и ошибки (обязательно) ;
— Привязать это событие к локальной событийной базе (event_base) вызовом bufferevent_base_set();
— Включать или выключать обработчики чтения/записи вызовами bufferevent_enable()/bufferevent_disable();
— Читать и писать вызовами bufferevent_read() и bufferevent_write() соответственно.
Стоит отметить, что обработчик чтения будет вызван после того, как в буфере накопится весь пакет или закроется подключение,
а обработчик записи — когда отправляемый буфер отправлен либо полностью, либо до указанной отметки (watermark).

Пример использования


Для примера напишем эхо-сервер и простой клиент, который будет обслуживать одновременно несколько клиентов.
Начнем с сервера. Для этого нам понадобится слушающий сокет, который может асинхронно принимать подключения,
обработчик подключения и логика работы с клиентом.
Для создания серверного сокета достаточно воспользоваться этой статьей.
Для удобства, вся информация о подключениях хранится в хэш-таблице, которая завернута в класс Connections.
Функция main будет практически идентичной, за исключением инициализации событийной базы:

// Initialize
event_base * base = event_base_new();


Создается новая событийная база для того, чтобы не перекрывать глобальный контекст.

// Init events
struct event ev;
// Set connection callback (on_connect()) to read event on server socket
event_set(&ev, server_sock, EV_READ | EV_PERSIST, on_connect, base);
// Attach event to new event base
event_base_set(base, &ev);
// Add server event without timeout
event_add(&ev, NULL);


Создается событие, которое реагирует на чтение из серверного сокета — это событие подключение нового клиента.
Функция event_base_set — привязывает это событие к событийной базе.
Обработка событий выполняется так:

// Dispatch events
event_base_loop(base, 0);


Обработка подключения заключается в том, чтоб создать новое буферизированное событие, связанное с сокетом клиента.

// Setup connection
ConnectionData & cdata = Connections::Instance().Add(sock);

cdata.evb = bufferevent_new(sock, on_read, on_write, on_error, arg);
bufferevent_base_set(reinterpret_cast(arg), cdata.evb);
// Ready to get data
bufferevent_enable(cdata.evb, EV_READ);


Чтение выполняется в функции on_read примерно следующим образом:

size_t len = EVBUFFER_LENGTH(evb->input);
u_char * data = new u_char[len];
size_t read = bufferevent_read(evb, data, len);


Обработка отключения выполняется в функции on_error: необходимо проверить флаг EVBUFFER_EOF переменной what.

if (what & EVBUFFER_EOF) {
// Disconnected
}


В клиентской части работа с буферизированными событиями — аналогична. После выполнение подключения к серверу, инициализируем буфер:

event_base * base = event_base_new();
bufferevent * evb = bufferevent_new(sfd, on_read, on_write, on_error, base);

bufferevent_base_set(base, evb);
bufferevent_enable(evb, EV_WRITE | EV_READ);


Стоит отметить, что создание буфера и запись в него осуществляется до вызова обработчика очереди событий:

const char * data1 = "Hello from client";
bufferevent_write(evb, data1, strlen(data1) + 1);

event_base_loop(base, 0);


Обработка событий чтения/записи/ошибок аналогична обработке в серверной части.
Работающий пример эхо-сервера и клиента можно взять здесь.

___
1. http://www.monkey.org/~provos/libevent/doxygen-1.4.10/

пятница, 10 июля 2009 г.

Архитектура рекламной системы

Вопрос разработки высоконагруженных систем является очень актуальным. лично для меня, с точки зрения ресурсопотребления.
Можно спроектировать систему так, чтобы она хорошо масштабировалась горизонтально, но она все равно будет расходовать слишком много ресурсов для решения поставленных для нее задач. В качестве примера такой системы могу привести всем известный OpenX (бывший OpenAds) [1].
Реальная ситуация: для открутки 10 баннеров, при нагрузке в чуть более чем 4.5 миллиона показа в сутки, система на 5 серверах (2х процессорные машины с 4GB RAM) задыхается в Load Average 200%. Оптимизации базы данных (LOAD DATA INFILE + кэширование) ситуацию сильно не изменили.
Здесь уже приходится искать узкие места. Первое — система написана на PHP (OpenAds на PHP4), второе — на каждый клик выполняется INSERT запрос к БД.
Как вариант оптимизации — перевод системы на FastCGI, но на долго ли хватит этой оптимизации?
В итоге решили отказаться от этой системы в пользу собственной разработки.

Сегодня на сайте конференции разработчиков высоконагруженных систем highload++ [2] увидел презентацию архитектуры рекламной системы Mail.RU.


Цифры говорят сами за себя: 3 миллиарда показов в сутки на 6 фронтендах и 3х бекендах.

_____
1. Официальный сайт OpenX — http://www.openx.org/en/products
2. Сайт конференции HighLoad++ — http://highload.ru

пятница, 3 июля 2009 г.

Разработка RPC приложений с использованием libevent

Разрабатывая клиент-серверные системы часто возникает вопрос, какой протокол выбрать.
Этому вопросу следует уделить достаточно внимания, ведь протокол, новый или уже существующий,
накладывает ряд ограничений и дополнительных трудозатрат на разработку и поддержку системы в будущем.

При выборе протокола следует, как минимум, ответить на следующие вопросы:
— как распределяются задачи между взаимодействующими узлами; Задачи для клиента, задачи для сервера;
— какой объем данных будет минимальный для передачи, а какой максимальный;
— как часто необходимо обмениваться данными;
— какая топология узлов? Будут ли клиент и сервер находиться в одной подсети;
— сколько потребуется времени/ресурсов для расширения протокола и сколько кода это может затронуть;
— как предполагается организовывать защиту трафика;
— критична ли скорость обмена данными и возможно ли масштабировать систему вертикально, например, за счет повышения пропускной способности канала.

Я хотел бы поделиться опытом использования RPC протокола в системе с трехуровневой логической архитектурой.
Решение использовать RPC с бинарным протоколом было следствием следующих факторов:
— вся вычислительная нагрузка приходится на серверную часть (возможно горизонтальное масштабирование);
— размер ответа небольшой, но сильно отличающийся для разных запросов;
— обмениваться данными необходимо достаточно часто и с многих клиентов;
— сервер будет находиться в одной гигабитной подсети с клиентами, поэтому размер пакета играет меньше роли чем время на установку нового соединения;
— расширять функционал сервера прийдется неоднократно;
— нет необходимости в защите трафика;
— поскольку основная часть сервера написана на С, не хотелось путать туда тяжеловесные XML-RPC решения;
— проверенный временем libevent предоставляет удобную и тонкую обвертку над процессом маршалинга и асинхронными вызовами.

О технологии вызова удаленных процедур можно почитать например на citforum.ru [1], так что я не буду вдаваться в теорию, а покажу на примере, как использовать RPC в приложении на C/C++.
Хочу добавить, что данное решение (libevent + RPC) является кроссплатформенным, проверял на FreeBSD, Mac OS X и Windows.

Вот то, что нам необходимо для работы:
1. Компилятор С/С++.
2. Библиотека libevent (1.4.x/2.x).
3. Python (необходим для скрипта event_rpcgen.py).
4. Сам event_rpcgen.py (устанавливается в систему, либо можно взять из дистрибутива).

Перед тем, как разрабатывать сервер и клиент, необходимо определиться с базовыми структурами запросов и ответов.
В качестве примера я приведу сервер, который возвращает время с момента загрузки системы (uptime(1)) и информацию о системе (uname(1)), причем в ответе будет лишь та информация, которую запросит клиент.

Что характерно для систем реализующих RPC, это процесс упорядочивания структур данных для передачи другому узлу, который может находиться как на локальной системе, так и на удаленной, таким образом, чтоб было возможно восстановить их в полной точности (сохраняя размеры и типы данных). Этот процесс называется маршалингом [2]. Практически любая библиотека реализующая RPC предоставляет средства автоматического маршалинга и демаршалинга. Не исключение и libevent.

Создаем файл, в котором описываются структуры для запросов/ответов, расширение файла должно быть .rpc.

Вот пример моего файла QueryTypes.rpc


struct StatRequest {
optional int uptime = 1;
optional int uname = 2;
}
struct StatReply {
optional string uptime = 1;
optional string uname = 2;
}


Доступны следующие типы данных:
1. struct — структура, аналог struct в C (в конце точка с запятой не ставится).
2. int — аналог uint32_t.
3. string — char *, строка переменной длины.
4. bytes — вектор uint8_t, можно указывать длину, например bytes data[24].
5. array struct[type] — вектор структур type.

Модификатор optional указывает на то, что это не обязательно поле, и его наличие необходимо проверять макросом EVTAG_HAS().

После указания имени поля (поля именуются тегами) указывается его числовой id.

Теперь можно генерировать код, для работы с этими структурами:

$ event_rpcgen.py QueryTypes.rpc

Если все указано без ошибок, можно увидеть примерно следующее:

Reading "QueryTypes.rpc"
Created struct: StatRequest
Added entry: uptime
Added entry: uname
Created struct: StatReply
Added entry: uptime
Added entry: uname
... creating "QueryTypes.gen.h"
... creating "QueryTypes.gen.c"

В результате, мы получили два файла, в котором реализованы все необходимые методы для работы с этими структурами, включая методы маршалинга и демаршалинга.

RPC сервер


1. Заголовочные файлы


Нам понадобятся следующие заголовочные файлы:

#include <event.h>
#include <evhttp.h>
#include <evrpc.h>

а так же заголовочный файл с нашими структурами. Тут есть один нюанс, необходимо включать его с модификатором "C":

#ifdef __cplusplus
extern "C" {
#endif

#include "QueryTypes.gen.h"

#ifdef __cplusplus
}
#endif


2. Регистрация удаленных процедур


Собственно то, ради чего была эта затея — процедуры для удаленного вызова. Их необходимо зарегистрировать и сгенерировать код для автоматизации рутинных вещей [2].
Все вызовы RPC имеют следующий прототип: ИмяПроцедуры(СтруктураЗапроса, СтруктураОтвета).
Я назвал процедуру GetServerStat:

// Регистрируем
EVRPC_HEADER(GetServerStat, StatRequest, StatReply);
// Генерируем автокод
EVRPC_GENERATE(GetServerStat, StatRequest, StatReply);
Также, необходимо объявить функцию, которая будет обрабатывать вызов процедуры:

void GetServerStatCB(EVRPC_STRUCT(GetServerStat)* rpc, void * arg);


3. Создание слушающего RPC сервера


Поскольку выбрана реализация на базе HTTP сервера, необходимо зарегистрировать сокет, и
инициализировать HTTP мини-сервер. Создание сокета — дело тривиальное, это можно посмотреть
в моем примере.

// Инициализируем базу механизма событий
event_base * serv_base = event_init();
// Создаем HTTP сервер (это, на самом деле, легковесная обвертка над сокетом)
evhttp * http_base = evhttp_new(serv_base);
// и указывем сокет, на каком он будет работать
evhttp_accept_socket(http_base, server_sock);
// Создаем базу для RPC протокола
evrpc_base * rpc_base = evrpc_init(http_base);

После этого, необходимо зарегистрировать все обработчики для всех удаленных процедур:

EVRPC_REGISTER(rpc_base, GetServerStat, StatRequest, StatReply, GetServerStatCB, NULL);

Все, можно запускать диспетчер событий, дальше работаем в обработчиках вызовов.

event_dispatch();

При выходе, порядок очистки структур следующий:

EVRPC_UNREGISTER(rpc_base, GetServerStat);
evrpc_free(rpc_base);
evhttp_free(http_base);

4. Обработка запросов


Теперь самое интересное — обработка запросов и формирование ответов.
В реализации обработчика выполняются нехитрые стандартные вещи:

void GetServerStatCB(EVRPC_STRUCT(GetServerStat)* rpc, void *arg)
{
// Связывание запроса
StatRequest * request = rpc->request;
// и ответа
StatReply * reply = rpc->reply;
// Проверка полей
if (EVTAG_HAS(request, uptime)) {
// и присвоение значений в ответной структуре
EVTAG_ASSIGN(reply, uptime, data.c_str());
}
// в конце обработчика, если все ОК, даем об этом знать
EVRPC_REQUEST_DONE(rpc);
}

Есть. А теперь клиент.

RPC клиент


1. Заголовки


Заголовки подключаются такие же, как и в серверной части.

2. Регистрация процедур


Процедуры регистрируем так же: EVRPC_HEADER и EVRPC_GENERATE
А вот обработчик результата другой, а именно:

void GotServerStat(struct evrpc_status *status, struct StatRequest *request, struct StatReply *reply, void *arg);

где

status — результат обработки удаленного вызова. Нас интересует случай EVRPC_STATUS_ERR_NONE — значит все Ok,
request и reply — наши структуры,
arg — произвольный аргумент (указывается при регистрации самого обработчика (см. далее).

3. Инициализация RPC



// Событийная база
event_base * base = event_init();
// HTTP здесь нужен только для обработки заголовка HTTP пакета ( этот вызов лишь инициализирует внутренние буферы).
evhttp * http = evhttp_new(base);
// Аналогично с RPC
evrpc_base * rpc = evrpc_init(http);


А теперь, поехали: создаем подключение к RPC серверу

// IP адрес и порт сервера
const char * host = "127.0.0.1";
u_short port = 8090;
evhttp_connection * evcon = evhttp_connection_new(host, port);

Создаем пул подключений. Пул подключений — это такая структура, в которую можно добавить много разных подключений, и запросить у них наш запрос.

evrpc_pool * pool = evrpc_pool_new(base);
// Добавляем подключение в пул
evrpc_pool_add_connection(pool, evcon);
// Создаем и инициализируем структуру запроса. Кстати, вызовы StatRequest_new, StatRequest_free,.. - генерируются
// автоматически скриптом event_rpcgen.py
StatRequest * stat_req = StatRequest_new();
EVTAG_ASSIGN(stat_req, uptime, 1); // запрос на получение uptime (флаг 1)
EVTAG_ASSIGN(stat_req, uname, 1); // запрос на получение uname
// Создаем структуру ответа
StatReply * stat_rep = StatReply_new();
// Делаем запрос, указав, что при получении ответа вызвать обработчик GotServerStat
EVRPC_MAKE_REQUEST(GetServerStat, pool, stat_req, stat_rep, GotServerStat, NULL);
// Обрабатываем запрос и ответ
event_dispatch();

В обработчике ответа вызовем функцию выхода из диспетчера, поэтому далее следует код очистки структур.

StatRequest_free(stat_req);
StatReply_free(stat_rep);
evrpc_pool_free(pool);
evrpc_free(rpc);
evhttp_free(http);


4. Обработчик ответа


Обработчик ответа вызовется после получении ответа и демаршалинга структуры ответа.

void GotServerStat(struct evrpc_status *status, struct StatRequest *request, struct StatReply *reply, void *arg)
{
// Проверяем статус возврата
if (status->error == EVRPC_STATUS_ERR_NONE) {
// и читаем доступные значения
char * value = NULL;
if (EVTAG_HAS(reply, uptime) && (EVTAG_GET(reply, uptime, &value) != -1)) {
printf("[Reply] uptime: %s\n", value);
}
if (EVTAG_HAS(reply, uname) && (EVTAG_GET(reply, uname, &value) != -1)) {
printf("[Reply] uname: %s\n", value);
}
}
// Можно выходить из цикла диспетчера событий
event_loopexit(NULL);
}

Как видно, здесь нет ничего сложного, внутренняя реализация этой технологии достаточно эффективна, о чем можно судить по результатам тестирования средствами Apachebench [3].

$ ab -c 100 -n 1000 -p post.http.txt -T application/octet-stream http://10.1.3.10:8080/.rpc.GetServerStat
Server Hostname: 10.1.3.10
Server Port: 8080

Document Path: /.rpc.GetServerStat
Document Length: 136 bytes

Concurrency Level: 100
Time taken for tests: 0.067 seconds
Complete requests: 1000
Failed requests: 0
Broken pipe errors: 0
Non-2xx responses: 1058
Total transferred: 283544 bytes
Total POSTed: 250930
HTML transferred: 143888 bytes
Requests per second: 14925.37 [#/sec] (mean)
Time per request: 6.70 [ms] (mean)
Time per request: 0.07 [ms] (mean, across all concurrent requests)
Transfer rate: 4232.00 [Kbytes/sec] received
3745.22 kb/s sent
7977.22 kb/s total

Connnection Times (ms)
min mean[+/-sd] median max
Connect: 0 1 0.4 1 4
Processing: 3 4 0.8 4 8
Waiting: 1 4 0.7 4 7
Total: 3 6 0.9 6 9

Percentage of the requests served within a certain time (ms)
50% 6
66% 6
75% 6
80% 7
90% 7
95% 8
98% 8
99% 9
100% 9 (last request)

Клиент и сервер находятся в одной сети, т.е. основное время тратится на соединение и обработку запросов.
На этом, конечно же, все возможности RPC не заканчиваются, но для того, чтобы переступить не самый низкий порог входжения этого вполне достаточно.

Скачать работающий пример RPC сервера и клинета (Makefile для UNIX/Max OS) можно отсюда.

Спасибо за внимание и успешной разработки.

___
1. Вызов удаленных процедур (RPC) — http://www.citforum.ru/operating_systems/sos/glava_12.shtml
2. Теоретические основы маршалинга — http://ru.wikipedia.org/wiki/Маршалинг
3. Документация по работе с инструментом Apachebench — http://httpd.apache.org/docs/2.2/programs/ab.html

среда, 20 мая 2009 г.

Вебинар от QNX SS "Exactly When Do You Need Realtime?"


21 мая, компания QNX Software Systems проведет бесплатный онлайн-семинар, который должен помочь при выборе операционной системы для встраиваемых систем.

Продолжительность: 1 час, включая вопросы.

Все ли встраиваемые проекты нуждаются в ОСРВ?.. Завтра узнаем.



пятница, 8 мая 2009 г.

К проекту прилагается отличная документация в формате С

Отличная статья про то, как хорошо и полезно уметь читать и читать исходный код:
http://gaperton.livejournal.com/32772.html

Когда я только начинал программировать, у меня совсем не было интернета, а в моем городе не было вменяемой литературы, поэтому я привык пользоваться справкой.
Читая сопроводительные тексты к среде разработки и к библиотекам, наткнулся на следующее высказывание: "Чтобы научиться программировать – нужно читать очень много кода, а писать еще больше".

Документация-документацией, но все же, не помешает (а часто и поможет) знание того, как это работает "под капотом".

четверг, 7 мая 2009 г.

Написание своего HTTP сервера с использованием libevent

Библиотека libevent содержит в себе простейший асинхронный HTTP сервер, который можно
без особого труда встроить в собственное приложение для обслуживания HTTP запросов.

Для использования этой возможности достаточно добавить в код следующее:

1. Подключить заголовочный файл <evhttp.h>:
#include <evhttp.h>

2. Инициализировать базу событийного движка:
event_base * serv_base = (event_base *)event_init();

3. Инициализировать HTTP сервер:
evhttp * http_server = evhttp_new(serv_base);

4. Указать, на каком сокете слушать подключения:
evhttp_accept_socket(http_server, server_sock);

5. Выставить callback'и на запросы. Можно добавлять на каждый URI свой обработчик:
evhttp_set_cb(http_server, "/news", on_request_news, NULL);

6. Выставить обработчик на остальные запросы:
evhttp_set_gencb(http_server, on_request, NULL);

7. Запустить цикл обработки запросов:
event_base_dispatch(serv_base);


Реализация HTTP сервера является потоко-безопасной (thread safe).

Есть еще один нюанс: настоятельно рекомендуется игнорировать сигнал SIGPIPE.
Делается это следующим вызовом:
signal(SIGPIPE, SIG_IGN);


Описание всех функция для работы с HTTP протоколом
можно найти в документации.

Вот работающий пример простого HTTP сервера, который на все запросы отдает динамическую
страничку с некоторой информацией о клиенте.

/*
* \file: http_server.cpp
* \description: Simple HTTP server
*
*/
#include <errno.h>
#include <event.h>
#include <evhttp.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <signal.h>

#include <netinet/in.h>
#include <arpa/inet.h>

#include <iostream>

const short  SERVER_BACKLOG    = 128;
const short  BUF_LEN           = 26;
const char   RESPONCE[BUF_LEN] = "<H1>Hello there</H1><BR/>";
const char * SERVER_NAME       = "Simple HTTP Server";

void on_request(struct evhttp_request *, void *);

int main(int argc, char **argv)
{
if (argc < 3) {
std::cout << "Start as:" << std::endl 
<< argv[0] << " host_address port" << std::endl;
return 1;
}

int server_sock = socket(AF_INET, SOCK_STREAM, 0);
if (server_sock == -1) {
std::cout << "Error socket(): " << strerror(errno) << std::endl;
return 1;
}

u_short      port  = atol(argv[2]);
const char * host  = argv[1];
sockaddr_in  sa;
int          on    = 1;
sa.sin_family      = AF_INET;
sa.sin_port        = htons(port);
sa.sin_addr.s_addr = inet_addr(host);

if (setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) {
std::cout << "Error setsockopt(): " << strerror(errno) << std::endl;
return 1;
}

// Bind server socket to ip:port
if (bind(server_sock, (const sockaddr*)&sa, sizeof(sa)) == -1) {
std::cout << "Error bind(): " << strerror(errno) << " on: " << host << ":" << port << std::endl;
return 1;
}
// Make server to listen
if (listen(server_sock, SERVER_BACKLOG) == -1) {
std::cout << "Error listen(): " << strerror(errno) << std::endl;
return 1;
}
// Init events
event_base * serv_base   = (event_base *)event_init();
evhttp     * http_server = evhttp_new(serv_base);

// Ignore SIGPIPE
signal(SIGPIPE, SIG_IGN);

if (evhttp_accept_socket(http_server, server_sock) == -1) {
std::cout << "Error evhttp_accept_socket(): " << strerror(errno) << std::endl;
return 1;
}
// Set HTTP request callback                               
evhttp_set_gencb(http_server, on_request, NULL);
// Dispatch events
event_base_dispatch(serv_base);

return 0;
}

void on_request(struct evhttp_request * req, void * arg)
{
// Create responce buffer
struct evbuffer *evb = evbuffer_new();
if (!evb) { return; }

// Add heading text
evbuffer_add_printf(evb, "<HTML><HEAD><TITLE>%s Page</TITLE></HEAD><BODY>\n", SERVER_NAME);
// Add buffer
evbuffer_add(evb, RESPONCE, BUF_LEN);
// Add formatted text
evbuffer_add_printf(evb, "Your request is <B>%s</B> from <B>%s</B>.<BR/>Your user agent is '%s'\n", 
req->uri, req->remote_host, evhttp_find_header(req->input_headers, "User-Agent"));
// Add footer
evbuffer_add_printf(evb, "</BODY></HTML>");

// Set HTTP headers
evhttp_add_header(req->output_headers, "Server", SERVER_NAME);
evhttp_add_header(req->output_headers, "Connection", "close");

// Send reply
evhttp_send_reply(req, HTTP_OK, "OK", evb);

// Free memory
evbuffer_free(evb);
}


Makefile для сборки:
SRCS=http_server.cpp
LDFLAGS=-levent
CXXFLAGS=-I/usr/local/include -L/usr/local/lib
PROG=http_server

all:
c++ ${SRCS} -o ${PROG} ${LDFLAGS} ${CXXFLAGS}

clean:
rm -f ${PROG}


А вот результаты тестирования Apache Benchmark:
ab -c 100 -n 1000 "http://192.168.1.110:10000/test"

Server Software:        SimpleHTTPServer
Server Hostname:        192.168.1.110
Server Port:            10000

Document Path:          /test
Document Length:        199 bytes

Concurrency Level:      100
Time taken for tests:   0.185 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      308000 bytes
HTML transferred:       199000 bytes
Requests per second:    5393.74 [#/sec] (mean)
Time per request:       18.540 [ms] (mean)
Time per request:       0.185 [ms] (mean, across all concurrent requests)
Transfer rate:          1622.34 [Kbytes/sec] received

Connection Times (ms)
min  mean[+/-sd] median   max
Connect:        0    5   2.8      5      16
Processing:     5   13   3.9     13      23
Waiting:        2   10   3.9      9      22
Total:         11   18   3.4     18      27

Percentage of the requests served within a certain time (ms)
50%     18
66%     19
75%     20
80%     21
90%     22
95%     24
98%     25
99%     26
100%     27 (longest request)


Сервер был запущен локально на моем ноутбуке, на сервере результаты конечно же получше.
Но для сотни параллельных запросов – это неплохо.

понедельник, 27 апреля 2009 г.

Обновление библиотеки libevent

17 апреля вышла новая версия libevent 2.0, в которой существенно переработано и улучшено API для
работы с асинхронными событиями.

Судя по тому, что указано на официальном сайте, практически весь код, который работал
на версии 1.4, будет работать на libevent 2.0, но настоятельно рекомендуется для нового кода
использовать новую версию библиотеки. Так же необходимо перелинковать старые приложения.

Что изменилось:

  • стала более удобной структура заголовочных файлов;
  • улучшился потоко-безопасный API
  • появились перекрываемые функции работающие с памятью;
  • создание event_base стало конфигурируемое: в рантайме можно выбирать любой из доступных бекендов;
  • сокеты стали абстрактным типом: теперь нет необходимости приводить дескрипторы к int;
  • стало возможным выставлять таймауты для постоянных событий (раньше это не имело смысла);
  • теперь возможно выставлять множество одинаковых событий на один дескриптор;
  • многие вещи стали действительно thread-safe
  • появилась поддержка edge-triggered событий (если поддерживается бекендом);
  • улучшения в evbuffers (множество исправлений в работе с памятью);
  • переработан механизм работы с внутренней памятью (раньше это были цельные блоки,которые приходилось двигать и изменять в размере, что не есть хорошо. Теперь это связный список страниц); 
  • более гибкий механизм чтения из буфера
  • поддержка zero-copy в буферах evbuffers;
  • множественные колбеки на один буфер evbuffer;
  • новый интерфейс колбеков;
  • фильтрация буферизированного ввода-вывода;
  • и много чего другого.

четверг, 23 апреля 2009 г.

Работа с PostgreSQL. Использование libpqxx

Существует, как минимум, два официальных клиентских C++ API для работы с PostgreSQL:
libpq++ [1] и libpqxx [2]. Обе библиотеки распространяются свободно, с открытым
исходным кодом и хорошо документированы.
Для себя я выбрал libpqxx: являясь очень гибкой, она остается так же достаточно простой в использовании.

Хотя, наверное, это и не самая простая библиотека, но однообразный интерфейс к данным
и привычные для C++ разработчика итераторы вместо "get_next_row" для доступа к результатам
запроса, делают ее использование интуитивно понятным. Так же поддерживаются потоки,
где это имеет смысл, а поля могут быть считаны в переменные шаблонными методами.
В общем, все, как в нормальных C++ библиотеках.

Вот несколько причин, по которым можно выбрать именно libpqxx:

  1. Согласованность с языком. В разумной мере используются фичи современного
    C++ такие, как шаблоны (templates), исключения (exceptions), различного типы итераторов.
    В библиотеке, по возможности, используются стандартные вещи из STL.
  2. Структура, позволяющая писать "хороший" код. Многие проблемы обнаруживаются
    на этапе компиляции или тестирования, а не во время работы.
  3. Мощность. Такие встроенные фичи, как автоматическое восстановление соединения и
    управление транзакциями освобождают разработчика от низкоуровневой рутинной работы.
  4. Гибкость. С учетом описанного выше, можно сделать вывод, что это не фреймворк.
    Ничто не заставляет втискивать свой код в чужие обработчики. В ней нет доморощенных
    строк и классов исключений. Код библиотеки живет в своем пространстве имен (pqxx)
    и полностью прячет нативный C API.
Остальные достопримечательности библиотеки можно почитать на официальном сайте [2].

Итак, что же необходимо для работы?

Во-первых, необходима нативная libpq.so/libpq.dll.
Во-вторых, сама libpqxx.so/libpqxx.dll.

Что касается подхода к работе с базой данных, тут стоит обратить внимание, что
доступ к базе данных выполняются через объект транзакции pqxx::transaction.
Поэтому, знание интерфейсов этих классов является фундаментальным [3].

Для работы с libpqxx™ нам необходимо, как минимум следующие классы:
  • pqxx::connection – представляет собой обвертку к соединению в программе к back-end'у PostgreSQL™.
    Можно открывать множество соединений ко многим базам данных.
  • pqxx::work – тип для шаблона pqxx::transaction, представляющий собой саму транзакцию,
    которая выполняется в контексте подключения (pqxx::connection). Если произойдет
    какая-нибудь проблема в рамках этой транзакции – выполнится полный ее откат к исходному
    состоянию.
  • pqxx::result – контейнер, содержащий результат работы запроса или команды выполненных
    в транзакции.
В качестве примера, приведу программу, которая выполняет запрос и выводит результат
на экран в виде field='value'.

#include <pqxx/connection>
#include <pqxx/transaction>
#include <iostream>
#include <string>
#include <sstream>

void help()
{
std::cout << "Usage: <host> <user> <password> <database>" << std::endl;
}

int main(int argc, char **argv)
{
// Check arguments
if (argc < 5) {
help();
return 1;
}

// Prepare connection string
std::ostringstream conn_string("");
conn_string << "host=" << argv[1]
<< " user=" << argv[2]
<< " password=" << argv[3]
<< " dbname=" << argv[4];

do {
// Create connection
try {
pqxx::connection conn(conn_string.str());
pqxx::work xact(conn, "SampleSelect");

std::string query("SELECT * from news limit 10");
// Execute query
try {
pqxx::result res = xact.exec(query);
if (!res.size()) {
std::cout << "Empty result set." << std::endl;
break;
}
// Show results
for (pqxx::result::const_iterator i = res.begin(), r_end = res.end(); i != r_end; ++i) {
// Iterate fields
for (pqxx::result::const_fielditerator f = i->begin(), f_end = i->end(); f != f_end; ++f) {
std::cout << f->name() << " = '" << f->c_str() << "'" << std::endl;
}
std::cout << std::endl;
}
} catch (...) {
std::cout << "Failed to execute query: " << query << std::endl;
break;
}
} catch (pqxx::broken_connection) {
std::cout << "Failed to establish connection." << std::endl;
break;
}
std::cout << "Query successfully executed." << std::endl;

return 0;
} while (false);

return 1;
}


Makefile выглядит следующим образом:

SRC=pqxx_sample.cpp
PROG=pqxx_sample

all:
c++ ${SRC} -o ${PROG} `pkg-config --libs --cflags libpqxx`
clean:
rm -f *.o ${PROG}

Как видно из примера, строкой подключения является стандартное перечисление ключ-значение,
которое можно собрать, например, в std::ostringstream [4].

Далее, подключение заворачивается в try-catch(pqxx::broken_connection).
После выполнения запроса выполняется двойной цикл: первый по записям, второй по полям.

Конструкция do {} while (false) здесь используется для уменьшения цикломатической сложности.

На этом возможности библиотеки libpqxx, естественно, не заканчиваются. О них можно
почитать в справочнике [3].

  1. libpq++ – http://www.postgresql.org/docs/7.2/static/libpqplusplus.html
  2. libpqxx – http://pqxx.org/development/libpqxx/
  3. Справочная информация – http://pqxx.org/devprojects/libpqxx/doc/3.0/html/Reference/
  4. std::ostringstream – http://www.cplusplus.com/reference/iostream/ostringstream/