вторник, 29 ноября 2016 г.

Простой echo сервер на libev

В одном из своих проектов я активно использую поллинг при помощи epoll(7). Проект этот - HTTP(s) прокси с довольно развесистой бизнес логикой. В приложении постоянно приходится менять маску событий, а так же одновременно обрабатывать довольно большое количество сетевых соединений.

Когда проект начинался, всю обвязку вокруг epoll мы сделали сами, ну и разумеется вдоволь прошлись по граблям. За пример, при разработке, я брал тоже прокси, написанное как раз с целью понять как обрабатывать большое количество соединений в однопоточном приложении и при этом не утонуть в "code spaghetti". Ссылочка на этот проект: https://github.com/gpjt/rsp

Ссылку на свой проект дать не могу - код проприетарный.

Когда все более-менее влетело, я начал смотреть по сторонам на предмет "а нельзя ли немного упростить код". И разумеется код упростить можно было, используя что-то типа libevent или libev. Обе эти библиотеки предоставляют API для портируемой работы с событиями и избавляют от необходимости самому писать event-loop.
libevent, насколько я понял, по-старше будет, но на данный момент не развивается, поэтому ее я оставил на потом и решил написать небольшой примерчик использую libev.

Итак...


Пишем ECHO-сервер на libev.


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

Начало стандартное - готовим структуру sockaddr_in
sock.sin_port = htons(port);
sock.sin_addr.s_addr = htonl(INADDR_ANY);
sock.sin_family = AF_INET;
и создаем прослушиваемый неблокируемый сокет:
listen_fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
bind(listen_fd, (const struct sockaddr *)listen_addr, sizeof(struct sockaddr_in));
listen(listen_fd, BACKLOG_SIZE); 
Если все прошло успешное, создаем цикл событий в котором будем обрабатывать прием новых соединений, а так же прием данных от уже установленных соединений.

Инициализируем структуру типа ev_loop:

loop = ev_loop_new(EVFLAG_NOENV | EVBACKEND_EPOLL);

Описание флагов можно найти в man 3 libev или по адресу:
 http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod

Хочу лишь заметить, что libev позволяет указывать тип механизма поллинга при создании цикла событий, а так же позволяет это делать не перекомпилируя приложение, через переменные окружения(LIBEV_FLAGS).
Рекомендуемое, авторами библиотеки знание: EVFLAG_AUTO. В этом случае, при создании цикла обработки событий библиотекой будет выбран наилучший метод поллинга.

Далее инициализируем watcher. Это некий тип данных ev_TYPE, где TYPE это тип событий, поддерживаемый библиотекой. Для обработки ввода/вывода это io и соответственно тип будет ev_io. Функции(а на самом деле макросы) относящиеся к работе с тем или иным типом watcher, так же в своем имени имеют соответствующий префикс типа. К примеру:
ev_io_start() или ev_io_stop().

Watcher можно инициализировать двумя способами:
вызовами ev_init(), а затем ev_TYPE_set(). Либо: ev_TYPE_init().

Я использовал второй способ:

ev_io_init(&sock_watcher, accept_connect, listen_fd, EV_READ);

, где:
sock_watcher - указатель на соответствующую структуру типа ev_io;
accept_connect - указатель на callback; должен быть типа: void(*)(struct ev_loop *loop, ev_TYPE *w, int revents)
listen_fd - файловый дескриптор, на котором ожидаются события;
EV_READ - тип событий, на указанном дескрипторе, которые будут отрабатываться указанным callback.

В моем случае, callback вызываемый при принятии нового соединения это функция accept_connect(). На io, насколько я понял, может быть только два типа событий, это - EV_READ и EV_WRITE. Есть еще один, специальный тип это - EV_ERROR. Этим типом библиотека сигнализирует о внутренних проблемах приложения.

Далее сообщаем, что хотим начать обрабатывать события на указанном watcher:
ev_io_start(loop, &sock_watcher);
и запускаем цикл обработки событий:
ev_run(loop, 0); 
В первом приближении это все.
Дальше, по сути, делается тоже самое - при создании нового подключения(в accept_connect()) создается новый watcher типа io, на его события вешается callback on_read(), в котором обрабатываются события ввода/вывода на обслуживаемом соединении. В созданном watcher есть поле data, в котором можно храниться указатель на пользовательские данные. В моем случае, это указатель на структуру типа buffer_t, которая представляет буфер через который идет обмен данными в данном конкретном соединении. Все операции с сокетами выполняются в неблокируемом режиме. На этом все.

Для себя я не уяснил только один момент - так как библиотека может использовать различные backend, в том числе и epoll, то какой из типов поллинга в этом случае используется - edge-triggered или level-triggered. Явного указания я сходу не нашел.

Ссылка на код: https://github.com/apofiget/echo_srv

Комментариев нет:

Отправить комментарий