* При перепечатке материалов ссылка на www.SeoLiga.ru обязательна! RSS



Асинхронный режим, основанный на событиях
19 марта 2009

Асинхронный режим, основанный на событиях, появился во второй версии Windows Sockets. В его основе лежат события - специальные объекты, служащие для синхронизации работы нитей.
Существуют события, поддерживаемые на уровне системы. Эти события создаются с помощью функции CreateEvent. Каждое событие может находится в сброшенном или взведённом состоянии. Нить с помощью функций WaitForSingleObject и WaitForMultipleObjects может дожидаться, пока одно или несколько событий не окажутся во взведённом состоянии. В режиме ожидания нить не требует процессорного времени. Другая нить может установить событие с помощью функции SetEvent, в результате чего первая нить выйдет из состояния ожидания и продолжит свою работу.
Аналогичные объекты определены и в Windows Sockets. Сокетные события отличаются от стандартных системных событий прежде всего тем, что они могут быть связаны с событиями FD_XXX, происходящими на сокете, и взводиться при наступлении этих событий.
Так как сокетные события поддерживаются только в WinSock 2, модуль WinSock не содержит объявлений типов и функций, требуемых для их поддержки. Поэтому их придётся объявлять самостоятельно. Прежде всего, должен быть объявлен тип дескриптора событий, который в MSDN'е называется WSAEVENT. В Delphi он может быть объявлен следующим образом:
type PWSAEvent=^TWSAEvent;
TWSAEvent=THandle;

Событие создаётся с помощью функции WSACreateEvent, имеющей следующий прототип:
WSAEVENT WSACreateEvent(void);

function WSACreateEvent:TWSAEvent;

Событие, созданное этой функцией, находится в сброшенном состоянии, при ожидании автоматически не сбрасывается, не имеет имени и обладает стандартными атрибутами безопасности. В MSDN'е отмечено, что сокетное событие на самом деле является простым системным событием, и его можно создавать с помощью стандартной функции CreateEvent, управляя значениями всех вышеперечисленных параметров.
Функция создаёт событие и возвращает его дескриптор. Если произошла ошибка, функция возвращает значение WSA_Invalid_Event (0).
Для ручного взведения и сброса события используются функции WSASetEvent и WSAResetEvent соответственно, прототипы которых выглядят следующим образом:
BOOL WSASetEvent(WSAEVENT hEvent);
BOOL WSAResetEvent(WSAEVENT hEvent);

function WSASetEvent(hEvent:TWSAEvent):BOOL;
function WSAResetEvent(hEvent:TWSAEvent):BOOL;

Функции возвращают True, если операция прошла успешно, и False в противном случае.
После завершения работы с событием оно уничтожается с помощью функции WSACloseEvent:
BOOL WSACloseEvent(WSAEVENT hEvent);

function WSACloseEvent(hEvent:TWSAEvent):BOOL;

Функция уничтожает событие и освобождает связанные с ним ресурсы. Дескриптор, переданный в качестве параметра, становится недействительным.
Для ожидания взведения событий используется функция WSAWaitForMultipleEvents, имеющая следующий прототип:
DWORD WSAWaitForMultipleEvents(
DWORD cEvents,
const WSAEVENT FAR *lphEvents,
BOOL fWaitAll,
DWORD dwTimeout,
BOOL fAlertable);

function WSAWaitForMultipleEvents(
cEvents:DWord;
lphEvents:PWSAEvent;
fWaitAll:BOOL;
dwTimeout:DWord;
fAlertable:BOOL):DWord;

Дескрипторы событий, взведения которых ожидает нить, должны храниться в массиве, размер которого передаётся через параметр cEvents, а указатель - через параметр lphEvents. Параметр fWaitAll определяет, что является условием окончания ожидания: если этот параметр равен TRUE, ожидание завершается, когда все события из переданного массива оказываются во взведённом состоянии, если FALSE - когда оказывается взведённым хотя бы одно из них. Параметр dwTimeout определяет таймаут ожидания в миллисекундах. В WinSock 2 определена константа WSA_Infinite (совпадающая по значению со стандартной константой Infinite), которая задаёт бесконечное ожидание. Параметр fAlertable используется при перекрытом вводе-выводе; мы рассмотрим его позже в соответствующем разделе. Если перекрытый ввод-вывод не используется, fAlertable должен быть равен FALSE.
Существует ограничение на число событий, которое можно ожидать с помощью данной функции. Максимальное число событий определяется константой WSA_Maximum_Wait_Events, которая в данной реализации равна 64.
Результат, возвращаемый функцией, позволяет определить, по каким причинам закончилось ожидание. Если ожидалось взведение всех событий (fWaitAll=TRUE), и оно произошло, функция возвращает WSA_Wait_Event_0 (0). Если ожидалось взведение хотя бы одного из событий, возвращается WSA_Wait_Event_0 + Index, где Index - индекс взведённого события в массиве lphEvents (отсчёт индексов начинается с нуля). Если ожидание завершилось по таймауту, возвращается значение WSA_Wait_Timeout (258). И, наконец, если произошла какая-либо ошибка, функция возвращает WSA_Wait_Failed ($FFFFFFFF).
Существует ещё одно значение, которое может возвратить функция WSAWaitForMultipleEvents: Wait_IO_Completion (это константа из стандартной части Windows API, она объявлена в модуле Windows). Смысл этого результата и условия, при которых он может быть возвращён, мы рассмотрим при изучении перекрытого ввода-вывода.
Функции, которые мы рассматривали до сих пор рассматривали, являются аналогами системных функций для стандартных событий. Теперь мы переходим к рассмотрению тех функций, которые отличают сокетные события от стандартных. Главная из этих функций - WSAEventSelect, позволяющая привязать события, создаваемые с помощью WSACreateEvent, к тем событиям, которые происходят на сокете. Прототип этой функции выглядит следующим образом:
int WSAEventSelect(SOCKET s,WSAEVENT hEventObject,long lNetworkEvents);

function WSAEventSelect(S:TSocket;hEventObject:TWSAEvent;
lNetworkEvents:LongInt):Integer;

Эта функция очень похожа на функцию WSAAsyncSelect, за исключением того, что события FD_XXX привязываются не к оконным сообщениям, а к сокетным событиям. Параметр S определяет сокет, события которого отслеживаются, параметр hEventObject - событие, которое должно взводиться при наступлении отслеживаемых событий, lNetworkEvents - комбинация констант FD_XXX, определяющая, с какими событиями на сокете связывается событие hSocketEvent.
Функция WSAEventSelect возвращает ноль, если операция прошла успешно, и Socket_Error при возникновении ошибки.
Событие, связанное с сокетом функцией WSAEventSelect, взводится при тех же условиях, при которых в очередь окна помещается сообщение при использовании WSAAsyncSelect. Так, например, функция Recv взводит событие, если после её вызова в буфере сокета ещё остаются данные. Но, с другой стороны, функция Recv не сбрасывает событие, если данных в буфере сокета нет. А так как сокетные события не сбрасываются автоматически функцией WSAWaitForMultipleEvents, программа всегда должна сбрасывать события сама. Так, при обработке FD_Read наиболее типичной является ситуация, когда сначала сбрасывается событие, а потом вызывается функция Recv, которая при необходимости снова взводит событие. Здесь мы снова имеем проблему ложных срабатываний в тех случаях, когда данные извлекаются из буфера по частям с помощью нескольких вызовов Recv, но в данном случае проблему решить легче: не надо отменять регистрацию событий, достаточно просто сбросить событие непосредственно перед последним вызовом Recv.
В принципе, события FD_XXX разных сокетов можно привязать к одному сокетному событию, но этой возможностью обычно не пользуются, так как в WinSock отсутствуют средства, позволяющие определить, событие на каком из сокетов привело к взведению сокетного события.
Как и в случае с WSAAsyncSelect, при вызове WSAEventSelect сокет переводится в неблокирующий режим. Повторный вызов WSAEventSelect для данного сокета отменяет результаты предыдущего вызова (т.е. невозможно связать разные события FD_XXX одного сокета с разными сокетными событиями). Сокет, созданный в результате вызова Accept или WSAAccept, наследует связь с сокетными событиями, установленную для слушающего сокета.
Существует весьма важное различие между использованием оконных сообщений и сокетных событий для оповещения о том, что происходит на сокете. Предположим, с помощью функции WSAAsyncSelect события FD_Read, FD_Write и FD_Connect связаны с некоторым оконным сообщением. Пусть происходит событие FD_Connect. В очередь окна помещается соответствующее сообщение. Затем, до того, как предыдущее сообщение будет обработано, происходит FD_Write. В очередь окна помещается ещё одно сообщение, которое информирует об этом. И, наконец, при возникновении FD_Read в очередь будет помещено третье сообщение. Затем оконная процедура получит их по очереди и обработает.
Теперь рассмотрим ситуацию, когда те же события связаны с сокетным событием. Когда происходит FD_Connect, сокетное событие взводится. Теперь, если FD_Write и FD_Read произойдут до того, как сокетное событие будет сброшено, оно уже не изменит своего состояния. Таким образом, программа, использующая асинхронные сокеты, основанные на событиях, должна, во-первых, учитывать, что взведённое событие может означать несколько событий FD_XXX, а во-вторых, иметь возможность узнать, какие именно события произошли с момента последней проверки. Для получения этой информации используется функция WSAEnumNetworkEvents, имеющая следующий прототип:
int WSAEnumNetworkEvents(
SOCKET s,
WSAEVENT hEventObject,
LPWSANETWORKEVENTS lpNetworkEvents);

function WSAEnumNetworkEvents(
S:TSocket;
hEventObject:TWSAEvent;
var NetworkEvents:TWSANetworkEvents):Integer;

Функция WSAEnumNetworkEvents через параметр NetworkEvents возвращает информацию о том, какие события произошли на сокете S с момента последнего вызова этой функции для данного сокета (или с момента запуска программы, если функция вызывается в первый раз). Параметр hEventObject является необязательным. Он определяет сокетное событие, которое нужно сбросить. Использование этого параметра позволяет обойтись без явного вызова функции WSAResetEvent для сброса события. Как и большинство функций WinSock, функция WSAEnumNetworkEvents возвращает ноль в случае успеха и ненулевое значение в случае ошибки.
Структура TWSANetworkEvents содержит информацию о произошедших событиях и об ошибках. Она объявлена следующим образом:
typedef struct _WSANETWORKEVENTS {
long lNetworkEvents;
int iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, *LPWSANETWORKEVENTS;

type TWSANetworkEvents=packed record
lNetworkEvents:LongInt;
iErrorCode:array[0..FD_Max_Events-1] of Integer;
end;

Константа FD_Max_Events определяет количество разных типов событий и данной реализации равна 10.
Значения констант FD_XXX являются степенями двойки, поэтому их можно объединять операцией or без потери информации. Поле lNetworkEvents является таким объединением всех констант, задающих события, которые происходили на сокете. Другими словами, если результат операции (lNetworkEvents and FD_XXX) не равен нулю, значит, событие FD_XXX происходило на сокете.
Массив iErrorCode содержит информацию об ошибках, которыми сопровождались события FD_XXX. Для каждого события FD_XXX определена соответствующая константа FD_XXX_Bit (т.е. константы FD_Read_Bit, FD_Write_Bit и т.д.). Элемент массива с индексом FD_XXX_Bit содержит информацию об ошибке, связанной с событием FD_XXX. Если операция прошла успешно, этот элемент содержит ноль, в противном случае - код ошибки, которую в аналогичной ситуации вернула бы функция WSAGetLastError после выполнения соответствующей операции на синхронном сокете.
Таким образом, программа, использующая асинхронный режим, основанный на событиях, должна выполнить следующие действия. Во-первых, создать сокет и установить соединение. Во-вторых, привязать события FD_XXX к сокетному событию. В-третьих, организовать цикл, начинающийся с вызова WSAWaitForMultipleEvents, в котором с помощью WSAEnumNetworkEvents определять, какое событие произошло, и обрабатывать его. При возникновении ошибки на сокете цикл должен завершаться.
Сокетные события могут взводиться не только в результате событий на сокете, но и вручную, с помощью функции SetEvent. Это даёт нити, вызвавшей функцию WSAWaitForMultipleEvents, возможность выходить из состояния ожидания не только при возникновении событий на сокете, но и по сигналам от других нитей. Типичная область применения этой возможности - для тех случаев, когда программа может как отвечать на запросы от удалённого партнёра, так и отправлять ему что-то по собственной инициативе. В этом случае могут использоваться два сокетных события: одно связывается с событием FD_Read для оповещения о поступлении данных, а второе не связывается ни с одним из событий FD_XXX, а устанавливается другой нитью тогда, когда необходимо отправить сообщение. Нить, работающая с сокетом, ожидает взведения одного из этих событий и в зависимости от того, какое из них взведено, читает или отправляет данные.
Ниже приведён пример кода такой нити. Она использует три сокетных события: одно для уведомления о событиях на сокете, второе - для уведомления о необходимости отправить данные, третье - для уведомления о необходимости завершиться. В данном примере мы предполагаем, что, во-первых, сокет создан и подключен до создания нити и передаётся ей в качестве параметра, а во-вторых, три сокетных события хранятся в глобальном массиве SockEvents:array[0..2] of TWSAEvent, причём нулевой элемент этого массива содержит событие, связываемое с событиями FD_XXX, первый элемент - событие отправки данных, второй - событие завершения нити. Прототип функции, образующей нить, совместим с функцией BeginThread из модуля SysUtils.
function ProcessSockEvents(Parameter:Pointer):Integer;
var S:TSocket;
NetworkEvents:TWSANetworkEvents;
begin
// Так как типы TSocket и Pointer занимают по 4 байта, такое
// приведение типов вполне возможно, хотя и некрасиво
S:=TSocket(Parameter);
// Связываем событие SockEvents[0] с FD_Read и FD_Close
WSAEventSelect(S,SockEvents[0],FD_Read or FD_Close);
while True do
begin
case WSAWaitForMultipleEvents(3,@SockEvents[0],True,
WSA_Infinite,False) of
WSA_Wait_Event_0:
begin
WSAEnumNetworkEvents(S,SockEvents[0],NetworkEvents);
if NetworkEvents.lNetworkEvents and FD_Read>0 then
if NetworkEvents.iErrorCode[FD_Read_Bit]=0 then
begin
// Пришли данные, которые надо прочитать
end
else
begin
// Произошла ошибка. Надо сообщить о ней и завершить нить
CloseSocket(S);
Exit
end;
if NetworkEvents.lNetworkEvents and FD_Close>0 then
begin
// Связь разорвана
if NetworkEvents.iErrorCode[FD_Close_Bit]=0 then
begin
// Свзяь закрыта корректно
end
else
begin
// Связь разорвана в результате сбоя сети
end;
// В любом случае надо закрыть сокет и завершить нить
CloseSocket(S);
Exit
end
end;
WSA_Wait_Event_0+1:
begin
// Получен сигнал о необходимости отправить данные
// Здесь должен быть код отправки данных
// После отправки событие надо сбросить вручную
ResetEvent(SockEvents[1])
end;
WSA_Wait_Event_0+2:
begin
// Получен сигнал о необходимости завершения работы нити
CloseSocket
ResetEvents(SockEvents[2]);
Exit
end
end
end
end;

Как и во всех предыдущих примерах, здесь для краткости не проверяются результаты, возвращаемые функциями и не отлавливаются возникающие ошибки. Кроме того, не используется процедура завершения связи с вызовом Shutdown.
Данный пример может рассматриваться как фрагмент кода простого сервера. В отдельной нити такого сервера выполняется цикл, состоящий из вызова Accept и создания новой нити для обслуживания полученного таким образом сокета. Затем другие нити при необходимости могут давать таким нитям команды (необходимо только предусмотреть для каждой нити, обслуживающей сокет, свою копию массива SockEvents). Благодаря этому каждый клиент будет обслуживаться независимо.
К недостаткам такого сервера следует отнести его низкую устойчивость против DoS-атак, при которых к серверу подключается очень большое число клиентов. Если сервер будет создавать отдельную нить для обслуживания каждого подключения, количество нитей очень быстро станет слишком большим, и вся система окажется неработоспособной, т.к. большая часть процессорного времени будет тратиться на переключение между нитями. Более защищённым является вариант, при котором сервер заранее создаёт некоторое разумное количество нитей (пул нитей) и обработку запроса или выполнение команды поручает любой свободной нити из этого пула. Если ни одной свободной нити в пуле нет, задание ставится в очередь. По мере освобождения нитей задания извлекаются из очереди и выполняются. При DoS-атаках такой сервер также не справляется с поступающими заданиями, но это не приводит к краху всей системы. Но сервер с пулом нитей реализуется сложнее (обычно - через порты завершения, которые мы здесь не рассматриваем). Тем не менее, простой для реализации сервер без пула нитей тоже может оказаться полезным, если вероятность DoS-атак низка (например, в изолированных технологических подсетях).
Приведённый пример может рассматриваться также как заготовка для клиента. В этом случае целесообразнее передавать в функцию ProcessSockEvents не готовый сокет, а только адрес сервера, к которому необходимо подключиться. Создание сокета и установление связи с сервером при этом выполняет сама нить перед началом цикла ожидания событий. Такой подход очень удобен для независимой работы с несколькими однотипными серверами.


Теги: asus socket, IP, borland delphi, User Datagram Protocol, Unix, SQL-сервер, socket, Создание сокета, сокет, sockets Borland Delphi

Статьи по теме:

FilePathFull
Классификация продуктов OLAP по способу представления данных
ППП автоматизированного проектирования
Новая сетевая технология АТМ
Объединение сетей в интерсеть
Пространства имен
Выключение секций
Управляющие средства пользовательского интерфейса
Объекты
OnIndexMissing
Сравнение типов информационных систем
Компонент TButton
Translate
Компонент TQRSysData
Разрешение и запрет
| Borland Delphi | Alex |
 


Пн Вт Ср Чт Пт Сб Вс
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30


     



Rambler's Top100

Данный сайт или домен продается ICQ: 403-353-727

© 2009 Seoliga.ru | Borland Delphi | Асинхронный режим, основанный на событиях. Регион сайта: Москва и Санкт-Петербург