Механизм обработки сетевых событий в Winsock2

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

FD_ACCEPT
FD_READ
FD_WRITE
FD_CLOSE
FD_CONNECT

Итак, давайте рассмотрим весь процесс создания, отслеживания и обработки сетевых событий.

Во-первых нам прийдётся инициализировать библиотеку winsock2. Впринципе существует нескольколько способов сделать это, например так:

WSADATA wsd;
LPFN_WSASTARTUP lpf = (LPFN_WSA_STARTUP)::GetProcAddress( ::LoadLibrary("WS2_32.DLL"),
"WSAStartup");
lpf(0x0202, &wsd);

Инициализировать можно в любом месте программы, но обязательно до вызова каких-либо функций winsock. Следующий важный момент - это создание события, которое мы хотим отслеживать на данном сокете. Для этого будем использовать вызов Winsock2 API  ::WSACreateEvent(); После того, как событие создано, его нужно связать с сокетом, события которого мы хотим контролировать и обрабатывать. Это делается функцией WSAEventSelect(...).  Следующий пример показывает, как отследить событие, сигнализирующее о том, что на сокет пришёл запрос на установление канала связи. Обычно такую операцию можно проделывать на прослушивающем (listening) сокете.  В примере это SOCKET m_listen . Итак, как это выглядит:: 

WSAEVENT hEvent = WSA_INVALID_EVENT;
hEvent = WSACreateEvent();
::WSAEventSelect(m_listen, m_hEvent, FD_ACCEPT);

Если мы хотим, чтобы сокет (в данном случае это SOCKET m_socket ) информировал нас о том, что готов принять или передать данные, то событие создаётся следующим образом:

WSAEVENT hDataEvent = WSA_INVALID_EVENT;
hDataEvent = WSACreateEvent();
::WSAEventSelect(m_socket, hDataEvent, FD_WRITE | FD_READ | FD_CLOSE);

Необходимо заметить, что для одного и того же сокета невозможно создать два объекта событий, то есть следующий код неверен:

WSAEVENT hEvent1 = WSA_INVALID_EVENT;
WSAEVENT hEvent2 = WSA_INVALID_EVENT;

hEvent1 = WSACreateEvent();
hEvent2 = WSACreateEvent();

::WSAEventSelect(m_socket, hEvent1, FD_READ);
::WSAEventSelect(m_socket, hEvent2, FD_WRITE); 

Обработка уведомлений о событиях

Теперь, когда события заданы, нам необходимо ожидать их и, соответственно, обрабатывать. Для ожидания событий можно использовать функцию WSAWaitForMultipleEvents(...). Эта функция будет работать как поток в спящем режиме до тех пор, пока не произойдёт событие, на которое мы хотели бы отреагировать. Давайте посмотрим на пример вызова этой функции:

// m_listen  и  m_data два существующих сокета:
WSAEVENT hEvent1 = WSACreateEvent();
WSAEVENT hEvent2 = WSACreateEvent();

::WSAEventSelect(m_listen, hEvent1, FD_ACCEPT);
::WSAEventSelect(m_data, hEvent2, FD_READ | FD_CLOSE);

WSAEVENT* pEvents = (WSAEVENT*)::calloc(2, WSAEVENT);
pEvents[0] = hEvent1;
pEvents[1] = hEvent2;

int nReturnCode = ::WSAWaitForMultipleEvents(2, pEvents, FALSE, INFINITE, FALSE);

Если же мы хотим ожидать только одного события, то эту функцию можно вызвать следующим образом:

 int nReturnCode = ::WSAWaitForMultipleEvents(1, &hEvent1, FALSE, INFINITE, FALSE); 

Первый параметр - это количество событий, которые мы хотим ожидать. Второй параметр - это указатель на массив событий, которые мы хотим ожидать. Третий параметр имеет значение BOOL, которое определяет - будет ли функция оставаться в спящем режиме до тех пор пока не сработают все события. Обычно этот параметр задаётся как false, но возможно Вам может понадобиться ожидать наступления всех событий. Четвёртый параметр определяет - как долго ожидать наступления события. Обычно я запускаю отдельный поток и оставляю его как infinite. Но, если Вы будете запускать функцию в основном потоке, то может понадобиться поставить ограничение в 5 (или больше) секунд, чтобы дать возможность приложению обрабатывать другие события. Пятый параметр указывает на то, хотим мы или нет получать алерты.

Теперь необходимо позаботиться об обработчиках каждого события. Перво-наперво нам необходимо получить достоверную информацию о том, какое событие возникло. Для этого существует функция ::WSAEnumNetworkEvents(...). Один из параметров которой - это структура под названием WSANETWORKEVENTS. Принимая во внимание код, приведённый выше, получим следующее:

WSANETWORKEVENTS hConnectEvent;
WSANETWORKEVENTS hProcessEvent;

::WSAEnumNetworkEvents(m_listen, hConnectEvent, &wsaConnectEvents);
::WSAEnumNetworkEvents(m_data, hProcessEvent, &wsaProcessEvents);

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

if( (wsaConnectEvents.lNetworkEvents & FD_ACCEPT) &&	//Нужное ли нам событие ?
(wsaConnectEvents.iErrorCode[FD_ACCEPT_BIT] == 0) ) //если нет ошибок,
{ //то обрабатываем
/*
....
Здесь находится обработчик
....
*/

}

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