
Программирование_распределенных_систем / Использование сокетов в Delphi_2
.pdf(одну из констант FD_XXX), а старшее слово - код ошибки, если она произошла. Для
выделения кода события и кода ошибки из lParam в библиотеке WinSock предусмотрены макросы WSAGETSELECTEVENT и WSAGETSELECTERROR соответственно. В модуле WinSock они заменены функциями WSAGetSelectEvent и WSAGetSelectError. Одно сообщение может информировать только об одном событии на сокете. Если произошло несколько событий, в очередь окна будет добавлено несколько сообщений.
Сокет, созданный при вызове функции Accept, наследует режим того сокета, который принял соединения. Таким образом, если сокет, находящийся в режиме ожидания подключения, является асинхронным, то и сокет, порождённый функцией Accept, будет асинхронным, и тот же набор его событий будет связан с тем же сообщением, что и у исходного сокета.
Рассмотрим подробнее каждое из вышеперечисленных событий.
Событие FD_Read возникает, когда во входной буфер сокета поступают данные (если на момент вызова WSAAsyncSelect, разрешающего такие события, в буфере сокета уже есть данные, то событие также возникает). Как только соответствующее сообщение помещается
вочередь окна, дальнейшая генерация таких сообщений для этого сокета блокируется, т.е. получение новых данных не будет приводить к появлению новых сообщений (при этом сообщения, связанные с другими событиями этого сокета или с событием FD_Read других сокетов, будут по-прежнему помещаться при необходимости в очередь окна). Генерация сообщений снова разрешается после того, как будет вызвана функция для чтения данных из буфера сокета (это может быть функция Recv, RecvFrom, WSARecv или WSARecvFrom; мы в дальнейшем будем говорить только о функции Recv, потому что остальные ведут себя
вэтом отношении полностью аналогично).
Если после вызова Recv в буфере асинхронного сокета остались данные, в очередь окна снова помещается это же сообщение. Благодаря этому программа может обрабатывать большие массивы по частям. Действительно, пусть в буфер сокета приходят данные, которые программа хочет забирать оттуда по частям. Приход этих данных вызывает событие FD_Read, сообщение о котором помещается в очередь. Когда программа начинает обрабатывать это сообщение, она вызывает Recv и читает часть данных из буфера. Так как данные в буфере ещё есть, снова генерируется сообщение о событии FD_Read, которое ставится в конец очереди. Через некоторое время программа снова начинает обрабатывать это сообщение. Если и в этот раз данные будут прочитаны не полностью, в очередь снова будет добавлено такое же сообщение. И так будет продолжаться до тех пор, пока не будут прочитаны все полученные данные.
Описанная схема, в принципе, достаточно удобна, но следует учитывать, что в некоторых случаях она может давать ложные срабатывания, т.е. при обработке сообщения о событии FD_Read функция Recv завершится с ошибкой WSAEWouldBlock, показывающей, что входной буфер сокета пуст.
Если программа читает данные из буфера не только при обработке FD_Read, может возникнуть следующая ситуация: в буфер сокета поступают данные. Сообщение о событии FD_Read помещается в очередь. Программа в это время обрабатывает какое-то другое сообщение, при обработке которого также читаются данные. В результате все данные извлекаются из буфера, и он остаётся пустым. Когда очередь доходит до обработки FD_Read, читать из буфера уже нечего.
Другой вариант ложного срабатывания возможен, если программа при обработке FD_Read читает данные из буфера по частям, вызывая Recv несколько раз. Каждый вызов Recv, за исключением последнего, приводит к тому, что в очередь ставится новое сообщение о событии FD_Read. Чтобы избежать появления пустых сообщений в подобных случаях, MSDN рекомендует перед началом чтения отключить для данного сокета реакцию на поступление данных, вызвав для него WSAAsyncSelect без FD_Read, а перед последним вызовом Recvснова включить.
И, наконец, следует помнить, что сообщение о событии FD_Read можно получить и после того, как с помощью WSAAsyncSelect сокет будет переведён в синхронный режим. Это может случиться в том случае, когда на момент вызова WSAAsyncSelect в очереди ещё остались необработанные сообщения о событиях на данном сокете. Впрочем, это касается не только FD_Read, а вообще любого события.
Событие FD_Write информирует программу о том, что в выходном буфере сокета есть место для данных. Вообще говоря, оно там есть практически всегда, если только программа не отправляет постоянно большие объёмы данных. Следовательно, механизм генерации этого сообщения должен быть таким, чтобы не забивать очередь программы постоянными сообщениями о том, что в буфере есть место, а посылать эти сообщения только тогда, когда программа действительно нуждается в такой информации.
При использовании TCP первый раз сообщение, уведомляющее о событии FD_Write,
присылается сразу после успешного завершения операции подключения к серверу с помощью Connect, если речь идёт о клиенте, или сразу после создания сокета функцией Accept или её аналогом в случае сервера. При использовании UDP это событие возникает после привязки сокета к адресу явным или неявным вызовом функции Bind. Если на момент вызова WSAAsyncSelect описанные действия уже выполнены, событие FD_Write также генерируется.
В следующий раз событие может возникнуть только в том случае, если функция Send (или SendTo) не смогла положить данные в буфер из-за нехватки места в нём (в этом случае функция вернёт значение, меньшее, чем размер переданных данных, или завершится с ошибкой WSAEWouldBlock). Как только в выходном буфере сокета снова появится свободное место, возникнет событие FD_Write, показывающая, что программа может продолжить отправку данных. Если же программа отправляет данные не очень большими порциями и относительно редко, не переполняя буфер, то второй раз событие FD_Write не возникнет никогда.
Событие FD_Accept во многом похоже на FD_Read, за исключением того, что событие возникает не при получении данных, а при подключении клиента. После постановки сообщения о событии FD_Accept в очередь новые сообщения о FD_Accept для данного сокета в очередь не ставятся, пока не будет вызвана функция Accept или WSAAccept. При вызове одной из этих функций сообщение о событии вновь помещается в очередь окна, если в очереди подключений после вызова функции остаются подключения.
Событие FD_Connect возникает при установлении соединения для сокетов, поддерживающих соединение. Для клиентских сокетов оно возникает после завершения процедуры установления связи, начатой с помощью функции Connect, для серверных - после создания нового сокета с помощью функции Accept (событие возникает именно на новом сокете, а не на том, который находится в режиме ожидания подключения). В MSDN'е
написано, что оно должно возникать также и после выполнения Connect для сокетов, не поддерживающих соединение, однако мне не удалось получить это событие при использовании протокола UDP. Событие FD_Connect также возникает, если при попытке установить соединение произошла ошибка (например, оказался недоступен указанный сетевой адрес). Поэтому при получении этого события необходимо анализировать старшее слово параметра lParam, чтобы понять, удалось ли установить соединение.
Событие FD_Close возникает только для сокетов, поддерживающих соединение, при разрыве этого соединения нормальным образом или в результате ошибки связи. Если удалённая сторона для завершения соединения использует функцию Shutdown, то FD_Close возникает после вызова этой функции с параметром SD_Send. При этом соединение закрыто ещё не полностью, удалённая сторона ещё может получать данные, поэтому при обработке FD_Close можно попытаться отправить те данные, которые в этом нуждаются. При этом нет гарантии, что вызов функции отправки не завершится неудачей, т.к. удалённая сторона может и не использовать функцию Shutdown, а закрывать сокет сразу.
Рекомендуемая последовательность действий при завершении связи такова. Сначала клиент завершает отправку данных через сокет, вызывая функцию Shutdown с параметром SD_Send. Сервер при этом получает событие FD_Close. Сервер отсылает данные клиенту (при этом клиент получает одно или несколько событий FD_Read), а затем также завершает отправку данных с помощью Shutdown с параметром SD_Send. Клиент при этом получает событие FD_Close, в ответ на которое закрывает сокет с помощью CloseSocket. Сервер, в свою очередь, сразу после вызова Shutdown также вызывает CloseSocket.
Ниже приведён пример кода сервера, использующего асинхронные сокеты. Сервер работает в режиме запрос-ответ, т.е. посылает какие-то данные клиенту только в ответ на его запросы. Константа WM_SocketEvent, определённая в коде для сообщений, связанных с сокетом, может, в принципе, иметь и другие значения.
unit Unit1;
interface
uses
Windows,Messages,SysUtils,Classes,Graphics,Controls,Forms,Dialogs,WinSock;
const
WM_SocketEvent=WM_User+1;
type
TForm1=class(TForm)
procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject);
private
ServSock:TSocket;
procedure WMSocketEvent(var Msg:TMessage);message WM_SocketEvent; end;
var
Form1: TForm1;
implementation
{$R *.DFM}
procedure TForm1.FormCreate(Sender: TObject); var Data:TWSAData;
Addr:TSockAddr; begin
WSAStartup($101,Data);
//Обычная последовательность действий по созданию сокета,
//привязке его к адресу и установлению на прослушивание
ServSock:=Socket(AF_Inet,Sock_Dgram,0); Addr.sin_family:=AF_Inet; Addr.sin_addr.S_addr:=InAddr_Any;
Addr.sin_port:=HToNS(3320);
FillChar(Addr.sin_zero,SizeOf(Addr.sin_zero),0);
Bind(ServSock,Addr,SizeOf(Addr));
Listen(ServSock,SOMaxConn);
//Перевод сокета в асинхронный режим. Кроме события FD_Accept
//указаны также события FD_Read и FD_Close, которые никогда не
//возникают на сокете, установленном в режим прослушивания.
//Это сделано потому, что сокеты, созданные с помощью функции
//Accept, наследуют асинхронный режим, установленный для
//слушающего сокета. Таким образом, не придётся вызывать
//функцию WSAAsyncSelect для этих сокетов - для них сразу
//будет назначен обработчик событий FD_Read и FD_Close.
WSAAsyncSelect(ServSock,Handle,
WM_SocketEvent,FD_Read or FD_Accept or FD_Close)
end;
procedure TForm1.FormDestroy(Sender: TObject); begin
CloseSocket(ServSock); WSACleanup
end;
procedure TForm1.WMSocketEvent(var Msg:TMessage); var Sock:TSocket;
SockError:Integer; begin
Sock:=TSocket(Msg.WParam);
SockError:=WSAGetSelectError(Msg.lParam); if SockError<>0 then
begin
// Здесь должен быть анализ ошибки
CloseSocket(Sock); Exit
end;
case WSAGetSelectEvent(Msg.lParam) of FD_Read:
begin
//Пришёл запрос от клиента. Необходимо прочитать данные,
//сформировать ответ и отправить его.
end; FD_Accept:
begin
//Просто вызываем функцию Accept. Её результат нигде не
//сохраняется, потому что вновь созданный сокет автоматически
//начинает работать в асинхронном режиме, и его дескриптор при
//необходимости будет передан через Msg.wParam при возникновении
//события
Accept(Sock,nil,nil) end;
FD_Close: begin
//Получив от клиента сигнал завершения, сервер, в принципе,
//может попытаться отправить ему данные. После этого сервер
//также должен соединение со своей стороны
Shutdown(Sock,SD_Send); CloseSocket(Sock)
end end
end;
end.
Преимущество такого сервера по сравнению с сервером, основанным на функции Select (пример кода такого сервера приведён в предыдущей статье), заключается в том, что при отсутствии событий на сокетах он не находится в приостановленном режиме и может выполнять какие-то другие действия (например, реагировать на действия пользователя). Кроме того, этот сервер не имеет ограничений по количеству подключаемых клиентов,
связанных с размером типа TFDSet. Впрочем, последнее не является существенным преимуществом, так как при таком количестве клиентов сервер обычно использует другие, более производительные способы взаимодействия с клиентами.
[ К содержанию ]
Асинхронный режим, основанный на событиях
Асинхронный режим, основанный на событиях, появился во второй версии 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 не готовый сокет, а только адрес сервера, к которому необходимо подключиться. Создание сокета и установление связи с сервером при этом выполняет сама нить перед началом цикла ожидания событий. Такой подход очень удобен для независимой работы с несколькими
однотипными серверами.
[ К содержанию ]
Перекрытый ввод-вывод
Прежде чем переходить к рассмотрению перекрытого ввода-вывода, вспомним, какие модели ввода-вывода нам уже известны. Появление разных моделей связано с тем, что операции ввода-вывода не всегда могут быть выполнены немедленно.
Самая простая модель ввода-вывода - блокирующая. В блокирующем режиме, если операция не может быть выполнена немедленно, работа нити приостанавливается до тех пор, пока не возникнут условия для выполнения операции. В неблокирующей модели ввода-вывода операция, которая не может быть выполнена немедленно, завершается с ошибкой. И, наконец, в асинхронной модели ввода-вывода предусмотрена система уведомлений о том, что операция может быть выполнена немедленно.
При использовании перекрытого ввода-вывода операция, которая не может быть выполнена немедленно, формально завершается ошибкой - в этом заключается сходство перекрытого ввода-вывода и неблокирующего режима. Однако, в отличие от неблокирующего режима, при перекрытом вводе-выводе WinSock начинает выполнять операцию в фоновом режиме, и после её завершения начавшая операцию программа получает уведомление об успешно выполненной операции или о возникшей при её выполнении фатальной ошибке. Несколько операций ввода-вывода могут одновременно выполняться в фоновом режиме, как бы перекрывая работу инициировавшей их нити и друг друга. Именно поэтому данная модель получила название модели перекрытого вводавывода.
Перекрытый ввод-вывод существовал и в спецификации WinSock 1, но реализовывался только для линии NT. Специальных функций для перекрытого ввода-вывода в WinSock 1 не
было, надо было использовать функции ReadFile и WriteFile, в которые вместо дескриптора файла подставлялся дескриптор сокета. В WinSock 2 появилась полноценная поддержка перекрытого ввода-вывода для всех версий Windows, а в спецификацию добавились новые функции для его реализации, избавившие от необходимости использования функций файлового ввода-вывода. В данной статье мы будем рассматривать перекрытый вводвывод только в спецификации WinSock 2, т.к. старый вариант из-за своих ограничений уже не имеет практического смысла.
Существуют два варианта уведомления о завершении операции перекрытого вводавывода: через событие и через процедуру завершения. Кроме того, программа может не дожидаться уведомления, а проверять состояние запроса перекрытого ввода-вывода с помощью функции WSAGetOverlappedResult (её мы рассмотрим позже).
Чтобы сокет мог использоваться в операциях перекрытого ввода-вывода, при его создании должен быть установлен флаг WSA_Flag_Overlapped (функция Socket неявно устанавливает этот флаг). Для выполнения операций перекрытого ввода-вывода сокет не нужно переводить в какой-либо особый режим, достаточно вместо обычных функций Send и Recv использовать WSARecv и WSASend. Сначала мы рассмотрим функцию WSARecv, которая имеет следующий прототип:
int WSARecv( SOCKET s,
LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
function WSARecv( S:TSocket; lpBuffers:PWSABuf;
dwBufferCount:DWORD;
var NumberOfBytesRecvd:DWORD; var Flags:DWORD; lpOverlapped:PWSAOverlapped;
lpCompletionRoutine:TWSAOverlappedCompletionRoutine):Integer;
Перекрытым вводом-выводом управляют два последних параметра функции, но функция WSARecv обладает и другими дополнительными по сравнению с функцией Recv возможностями, не связанными с перекрытым вводом-выводом. Если оба этих параметра равны nil, или сокет создан без указания флага WSA_Flag_Overlapped, функция работает в
обычном блокирующем или неблокирующем режиме, который установлен для сокета. При
этом её поведение отличается от поведения функции Recv только тремя незначительными аспектами: во-первых, вместо одного буфера ей можно передать несколько буферов, которые заполняются последовательно. Во-вторых, флаги передаются ей не как значение, а как параметр-переменная, и при некоторых условиях функция WSARecvможет их изменять (при использовании TCP и UDP флаги никогда не меняются, поэтому мы не будем рассматривать здесь эту возможность). В-третьих, при успешном завершении функция WSARecv возвращает не количество прочитанных байт, а ноль, а количество прочитанных байт возвращается через параметр lpNumberOfBytesRecvd.
Буферы, в которые нужно поместить данные, передаются функции WSARecv через параметр lpBuffers. Он содержит указатель на начало массива структур TWSABuf, а параметр dwBufferCount - количество элементов в этом массиве. Выше мы знакомились со структурой TWSABuf: она содержит указатель на начало буфера и его размер. Соответственно, массив таких структур определяет набор буферов. При чтении данных заполнение буферов начинается с первого буфера в массиве lpBuffers, затем, если в нём не хватает места, используется второй буфер и т.д. Функция не переходит к следующему буферу, пока не заполнит предыдущий до последнего байта. Таким образом, данные, получаемые с помощью функции WSARecv, могут быть помещены в несколько несвязных областей памяти, что иногда бывает удобно, если принимаемые сообщения имеют строго определённый формат с фиксированными размерами компонентов пакета: в этом случае
можно каждый компонент поместить в свой независимый буфер.
Теперь переходим непосредственно к рассмотрению перекрытого ввода-вывода с использованием событий. Для использования этого режима при вызове функции WSARecv параметр lpCompletionRoutine должен быть равен nil, а через параметр lpOverlapped передаётся указатель на структуру TWSAOverlapped, которая определена следующим образом:
typedef struct _WSAOVERLAPPED {
DWORD |
Internal; |
DWORD |
InternalHigh; |
DWORD |
Offset; |
DWORD |
OffsetHigh; |
WSAEVENT |
hEvent; |
} WSAOVERLAPPED, *LPWSAOVERLAPPED;
type PWSAOverlapped=^TWSAOverlapped; TWSAOverlapped=packed record
Internal,InternalHigh,Offet,OffsetHigh:DWORD;
hEvent:TWSAEvent; end;
Поля Internal, InternalHigh, Offset и OffsetHigh предназначены для внутреннего использования системой, программа не должна выполнять никаких действий с ними. Поле hEvent задаёт событие, которое будет взведено при завершении операции перекрытого ввода-вывода. Если на момент вызова функции WSARecv данные в буфере сокета отсутствуют, она вернёт значение Socket_Error, а функция WSAGetLastError - WSA_IO_Pending (997). Это значит, что операция начала выполняться в фоновом режиме. В этом случае функция WSARecv не изменяет значения параметров NumberOfBytesRecvd и Flag. Поля структуры TWSAOverlapped при этом также модифицируются, и эта структура должна быть сохранена программой в неприкосновенности до окончания операции перекрытого ввода-вывода без изменений. После окончания операции будет взведено событие, указанное в поле hEvent параметра lpOverlapped. При необходимости программа может дождаться этого взведения с помощью функции WSAWaitForMultipleEvents.
Как только запрос будет выполнен, в буферах, переданных через параметр lpBuffers, оказываются принятые данные. Но знания одного только факта, что запрос выполнен, недостаточно, чтобы этими данными воспользоваться, потому что, во-первых, неизвестен размер этих данных, а во-вторых, неизвестно, успешно ли завершена операция перекрытого ввода-вывода. Для получения недостающей информации служит функция
WSAGetOverlappedResult, имеющая следующий прототип:
BOOL WSAGetOverlappedResult(
SOCKET s,
LPWSAOVERLAPPED lpOverlapped,
LPDWORD lpcbTransfer,
BOOL fWait,
LPDWORD lpdwFlags);
function WSAGetOverlappedResult( S:TSocket; lpOverlapped:PWSAOverlapped;