Получается, что при обоих вариантах попытка выполнить за один раз сразу два этапа не дает никаких преимуществ в быстродействии, но зато повышает вероятность ложных срабатываний события
FD_READ
в то время, когда сервер находится на этапе отправки данных клиенту. А ложные срабатывания на этом этапе вредны тем, что сервер принимает их за поступление данных, которое нужно запомнить, чтобы обработать после того, как ответ будет отправлен клиенту. В принципе, эти ложные срабатывания в итоге не приводят ни к чему плохому, кроме незначительного увеличения нагрузки на процессор, но раз от них нет пользы, и мы можем избавиться от них совсем небольшой ценой, лучше это сделать.
Отправка
данных клиенту выполняется при обработке события
FD_WRITE
. Это событие генерируется библиотекой сокета в двух случаях: при начале работы сокета и когда возможность отправки данных восстановлена после отказа из-за нехватки места в буфере. Пока речь не идет об обмене сообщениями размером в десятки мегабайтов, ситуация с нехваткой места в выходном буфере крайне маловероятна, т.е. библиотека сокетов будет генерировать это событие лишь один раз для каждого клиента. Но никто не мешает нам помещать соответствующее сообщение в очередь вручную, что мы и делаем при обработке события
FD_READ
после завершения этапа получения строки, т.е. когда сервер согласно протоколу должен отправить ответ. Таким образом. один и тот же участок кода используется для отправки данных как тогда, когда сервер видит в этом необходимость, так и тогда, когда их вновь можно отправлять после переполнения буфера.
При обработке события
FD_WRITE
в очередь сообщений также помещается сообщение
WM_SOCKETMESSAGE
, если было зафиксировано получение события
FD_READ
на этапе отправки данных. В принципе, это может дать ложное срабатывание
FD_READ
в двух случаях: когда исходное событие
FD_READ
было ложным и когда событие
FD_READ
уже присутствует в очереди на момент вызова
PostMessage
. Но, как мы уже отметили ранее, никаких неприятных последствий, кроме незначительного увеличения нагрузки на процессор, ложные срабатывания не приносят, так что с ними можно смириться.
В итоге у нас получился сервер, который, как и сервер на неблокирующих сокетах, никогда не блокируется и устойчив к нарушению клиентом протокола. По сравнению с сервером на неблокирующих сокетах сервер на асинхронных событиях имеет два преимущества. Во-первых, немного снижена нагрузка на процессор, т.к. попытка чтения данных из сокета выполняется не периодически, а только когда это необходимо. Во-вторых, сообщения клиента обрабатываются несколько быстрее, т.к. сообщение помещается в очередь сразу при получении данных, и, если сервер не занят ничем другим, он сразу приступает к его обработке, а не ждет, пока истечет период опроса.
2.2.7. Асинхронный режим, основанный на событиях
Асинхронный режим, основанный на событиях, появился во второй версии Windows Sockets. В его основе лежат события — специальные объекты, служащие для синхронизации работы нитей.
Существуют события, поддерживаемые на уровне системы. Они создаются с помощью функции
CreateEvent
. Каждое событие может находиться в сброшенном или взведенном состоянии. Нить с помощью функций
WaitForSingleObject
и
WaitForMultipleObjects
может дожидаться, пока одно или несколько событий не окажутся во взведенном состоянии. В режиме ожидания нить не требует процессорного времени. Другая нить может установить событие с помощью функции
SetEvent
, в результате чего первая нить выйдет из состояния ожидания и продолжит свою работу. Подробно о системных событиях и прочих объектах синхронизации написано в [2].
Аналогичные
объекты определены и в Windows Sockets. Сокетные события отличаются от стандартных системных событий прежде всего тем, что они могут быть связаны с событиями
FD_XXX
, происходящими на сокете, и взводиться при наступлении этих событий.
Так как сокетные события поддерживаются только в WinSock 2, модуль WinSock не содержит объявлений типов и функций, требуемых для их поддержки. Поэтому их придется объявлять самостоятельно. Прежде всего, должен быть объявлен тип дескриптора событий, который в MSDN называется
WSAEVENT
. В Delphi он может быть объявлен следующим образом:
PWSAEvent = ^TWSAEvent;
TWSAEvent = THandle;
Событие создается с помощью функции
WSACreateEvent
, прототип которой приведен в листинге 2.55.
Листинг 2.55. Функция
WSACreateEvent
// ***** Описание на C++ *****
WSAEVENT WSACreateEvent(void);
// ***** Описание на Delphi *****
function WSACreateEvent: TWSAEvent;
Событие, созданное этой функцией, находится в сброшенном состоянии, при ожидании автоматически не сбрасывается, не имеет имени и обладает стандартными атрибутами безопасности. В MSDN отмечено, что сокетное событие на самом деле является простым системным событием, и его можно создавать с помощью стандартной функции
CreateEvent
, управляя значениями всех перечисленных параметров.
Функция создает событие и возвращает его дескриптор. Если произошла ошибка, функция возвращает значение
WSA_INVALID_EVENT
(0). Для ручного взведения и сброса события предназначены функции
WSASetEvent
и
WSAResetEvent
соответственно, прототипы которых приведены в листинге 2.56.
Листинг 2.56. Функции для управления событиями
// ***** Описание на C++ *****
BOOL WSASetEvent(WSAEVENT hEvent);
BOOL WSAResetEvent(WSAEVENT hEvent);
// ***** Описание на Delphi *****
function WSASetEvent(hEvent: TWSAEvent): BOOL;
function WSAResetEvent(hEvent: TWSAEvent): BOOL;
Функции возвращают
True
, если операция прошла успешно, и
False
— в противном случае.
После завершения работы с событием оно уничтожается с помощью функции
WSACloseEvent
(листинг 2.57).
Листинг 2.57. Функция
WSACloseEvent
// ***** Описание на C++ *****
BOOL WSACloseEvent(WSAEVENT nEvent);
// ***** Описание на Delphi *****
function WSACloseEvent(hEvent: TWSAEvent): BOOL;
Функция уничтожает событие и освобождает связанные с ним ресурсы. Дескриптор, переданный в качестве параметра, становится недействительным. Для ожидания взведения событий служит функция