- PVSM.RU - https://www.pvsm.ru -
При проектировании высокопроизводительных сетевых приложения с неблокирующими сокетами важно решить, какой именно метод мониторинга сетевых событий мы будем использовать. Их есть несколько и каждый хорош и плох по-своему. Выбор правильного метода может быть критически важной вещью для архитектуры вашего приложения.
В этой статье мы рассмотрим:
Старый, проверенный годами работяга select() создавался ещё в те времена, когда «сокеты» назывались "сокетами Беркли [1]". Данный метод не вошел в самую первую спецификацию тех самих сокетов Беркли, поскольку в те времена вообще ещё не существовало концепции неблокирующего ввода-вывода. Но где-то в 80-ых годах она появилась, а вместе с ней и select(). С тех пор в его интерфейсе ничего существенно не менялось.
Для использования select() разработчику необходимо инициализировать и заполнить несколько структур fd_set дескрипторами и событиями, которые необходимо мониторить, а затем уже вызвать select(). Типичный код выглядит примерно вот так:
fd_set fd_in, fd_out;
struct timeval tv;
// обнуляем структуры
FD_ZERO( &fd_in );
FD_ZERO( &fd_out );
// Будем мониторить события о входящих данных для sock1
FD_SET( sock1, &fd_in );
// Будем мониторить события об исходящих данных для sock2
FD_SET( sock2, &fd_out );
// Определим сокет с максимальным числовым значением (select требует это значение)
int largest_sock = sock1 > sock2 ? sock1 : sock2;
// Будем ждать до 10 секунд
tv.tv_sec = 10;
tv.tv_usec = 0;
// Вызываем select
int ret = select( largest_sock + 1, &fd_in, &fd_out, NULL, &tv );
// Проверяем успешность вызова
if ( ret == -1 )
// ошибка
else if ( ret == 0 )
// таймаут, событий не произошло
else
{
if ( FD_ISSET( sock1, &fd_in ) )
// входящее событие на sock1
if ( FD_ISSET( sock2, &fd_out ) )
// исходящее событие на sock2
}
Когда проектировался select() никто, вероятно, не ожидал, что в будущем у нас появится необходимость писать многопоточные приложения, обслуживающие тысячи соединений. У select() есть сразу несколько существенных недостатков, делающих его плохо пригодным для работы в такого рода системах. Основными являются следующие:
Конечно, всё вышесказанное не является какой-то новостью. Разработчики операционных систем давно осознали данные проблемы и многие из них были учтены при проектировании метода poll. В этом месте вы можете спросить, а зачем мы вообще сейчас изучаем древнюю историю и есть ли сегодня какие-то причины использовать древний select? Да, такие причины есть и их целых две. Не факт, что они когда-то вам пригодятся, но почему бы о них не узнать.
Первая причина — портируемость. select() с нами уже миллион лет. В какие бы дебри программно-аппаратных платформ вас не занесло, если там есть сеть — там будет и select. Там может не быть никаких других методов, но select будет практически гарантированно. И не думайте, что я сейчас впадаю в старческий маразм и вспоминаю что-то типа перфокарт и ENIAC, нет. Более современного метода poll нет, например, в Windows XP [2]. А вот select есть.
Вторая причина более экзотична и имеет отношению к тому факту, что select может (теоретически) работать с таймаутами порядка одной наносекунды (если позволит аппаратная часть), в то время как и poll и epoll поддерживают лишь миллисекундную точность. Это не должно играть особой роли на обычных десктопах (или даже серверах), где у вас всё равно нет аппаратного таймера наносекундной точности. Но всё же в мире есть системы реального времени, имеющие такие таймеры. Так что я вас умоляю, когда будете писать прошивку ядерного реактора или ракеты — не поленитесь измерять время до наносекунд. Я, знаете ли, хочу жить.
Описанный выше случай, вероятно, единственный в котором у вас и правда нет выбора, что использовать (подходит лишь select). Однако, если вы пишете обычное приложение для работы на обычном железе, и вы будете оперировать адекватным количеством сокетов (десятками, сотнями — и не больше), то разница в производительности poll и select будет не заметна, так что выбор будет основываться на других факторах.
poll — это более новый метод опроса сокетов, созданный после того, как люди начали пытаться писать большие и высоконагруженные сетевые сервисы. Он спроектирован намного лучше и не страдает от большинства недостатков метода select. В большинстве случаев при написании современных приложений вы будете выбирать между использованием poll и epoll/libevent.
Для использования poll разработчику нужно инициализировать члены структуры pollfd наблюдаемыми дескрипторами и событиями, а затем вызвать poll().
Типичный код выглядит вот так:
// два события
struct pollfd fds[2];
// от sock1 мы будем ожидать входящих данных
fds[0].fd = sock1;
fds[0].events = POLLIN;
// а от sock2 - исходящих
fds[1].fd = sock2;
fds[1].events = POLLOUT;
// ждём до 10 секунд
int ret = poll( &fds, 2, 10000 );
// проверяем успешность вызова
if ( ret == -1 )
// ошибка
else if ( ret == 0 )
// таймаут, событий не произошло
else
{
// обнаружили событие, обнулим revents чтобы можно было переиспользовать структуру
if ( pfd[0].revents & POLLIN )
pfd[0].revents = 0;
// обработка входных данных от sock1
if ( pfd[1].revents & POLLOUT )
pfd[1].revents = 0;
// обработка исходящих данных от sock2
}
Poll был создан для решения проблем метода select, давайте посмотрим, как у него это получилось:
О недостатках метода poll мы уже говорили выше: его нет на некоторых платформах, вроде Windows XP. Начиная с Vista он существует, но называется WSAPoll. Прототип тот же, так что для платформенно-независимого кода можно написать переопределение, вроде:
#if defined (WIN32)
static inline int poll( struct pollfd *pfd, int nfds, int timeout) { return WSAPoll ( pfd, nfds, timeout ); }
#endif
Ну и точность таймаутов в 1 мс, которой будет недостаточно очень редко. Однако, у poll есть и другие недостатки:
Однако, всё вышеперечисленное можно считать относительно несущественным для большинства клиентских приложений. Исключение составляют, наверное, лишь p2p протоколы, где каждый из клиентов может быть связан с тысячами других. Эти проблемы могут игнорироваться даже большинством серверных приложений. Таким образом poll должен быть вашим предпочтением по-умолчанию перед select, если только вас не ограничивает одна из двух вышеуказанных причин.
Забегая наперёд, скажу, что poll является более предпочтительным даже по сравнению с более современным epoll (рассматривается ниже) в следующих случаях:
epoll — это новейший и лучший метод ожидания событий в Linux (и только в Linux). Ну, не то чтобы прям «новейший» — он в ядре с 2002 года. От poll и select он отличается тем, что предоставляет API для добавления/удаления/модификации списка наблюдаемых дескрипторов и событий.
Использование epoll требует чуть более тщательных приготовлений. Разработчик должен:
Типичный код выглядит вот так:
// Создаём дескриптор epoll. Нам нужен лишь один на всё приложение, он будет мониторить все сокеты
// Аргумент функции игнорируется (раньше это было не так, но сейчас так), так что напишите здесь своё любимое число
int pollingfd = epoll_create( 0xCAFE );
if ( pollingfd < 0 )
// ошибка
// Инициализируем структуру epoll_event
struct epoll_event ev = { 0 };
// Ассоциируйте соединение с наблюдаемым событием. Вы можете ассоциировать всё, что угодно
// epoll никак не использует эту информацию. Можно, например, сохранить указатель на объект класса соединения
ev.data.ptr = pConnection1;
// Наблюдаем события прихода данных, по одному за раз
ev.events = EPOLLIN | EPOLLONESHOT;
// Добавляем дескриптор в список наблюдаемых. Это можно сделать даже из другого потока
// пока первый ожидает в вызове epoll_wait - всё сработает правильно
if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev ) != 0 )
// report error
// будем выбирать из очереди событий по 20 событий за раз
struct epoll_event pevents[ 20 ];
// Ждём 10 секунд
int ready = epoll_wait( pollingfd, pevents, 20, 10000 );
// Проверяем успешность вызова
if ( ret == -1 )
// ошибка
else if ( ret == 0 )
// таймаут, событий не произошло
else
{
// просматриваем полученный список событий
for ( int i = 0; i < ret; i++ )
{
if ( pevents[i].events & EPOLLIN )
{
// получаем ранее ассоциированный с событием указатель на соединение, обрабатываем его
Connection * c = (Connection*) pevents[i].data.ptr;
c->handleReadEvent();
}
}
}
Давайте начнём с недостатков epoll — они очевидны из кода. Данный метод сложнее использовать, нужно написать больше кода, он делает больше системных вызовов.
Достоинства тоже налицо:
Но нужно также помнить и о том, что epoll — это не «во всём улучшенный poll». У него есть и недостатки по сравнению с poll:
Таким образом, использовать epoll следует только тогда, когда выполняется всё нижесказанное:
Если один или несколько пунктов не выполняются — рассмотрите использование poll или libevent.
libevent [3] — это библиотека, которая «оборачивает» методы опроса, перечисленные в данной статье (а также некоторые другие) в унифицированный API. Преимущество здесь в том, что, однажды написав код, вы можете собрать и запустить его на разных операционных системах. Тем не менее, важно понимать, что libevent — это всего лишь обёртка, внутри которой работают всё те же вышеперечисленные методы, со всеми их преимуществами и недостатками. libevent не заставит select слушать более 1024 сокетов, а epoll — модифицировать список событий без дополнительного системного вызова. Так что знать лежащие в основе технологии по-прежнему важно.
Необходимость поддерживать разные методы опроса приводит к усложнению API библиотеки libevent. Но всё же его использование проще, чем вручную писать два разных движка выборки событий для, например, Linux и FreeBSD (используя epoll и kqueue).
Рассмотреть возможность использования libevent стоит при сочетании двух событий:
Автор: tangro
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/sistemnoe-programmirovanie/284087
Ссылки в тексте:
[1] сокетами Беркли: http://en.wikipedia.org/wiki/Berkeley_sockets
[2] нет, например, в Windows XP: http://msdn.microsoft.com/en-us/library/windows/desktop/ms741669%28v=vs.85%29.aspx
[3] libevent: http://libevent.org/
[4] Источник: https://habr.com/post/415259/?utm_campaign=415259
Нажмите здесь для печати.