вторник, 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/

3 комментария:

Andrew комментирует...

Познавательная статья. Спасибо. Становлюсь постоянным читателем :)

Andrew комментирует...

Просьба. Не могли бы Вы перезалить пример кода на другой хост? ;) На депозитфайлс меня не пускают :(

Андрей Булавинов комментирует...

На upload.com.ua