О чём не пишут в книгах по Delphi

2.2. Сокеты Windows

В предыдущих разделах мы рассмотрели те методы работы с сокетами, которые восходят еще к сокетам Беркли. Разработчики библиотеки сокетов для Windows добавили в нее также поддержку новых методов, упрощающих работу с сокетами для приложений, имеющих традиционную для Windows событийно-ориентированную модель. В Windows можно использовать асинхронные сокеты и перекрытый ввод-вывод. Далее мы рассмотрим эти расширения, а также ряд новых функций, пришедших на смену "морально устаревшим" функциям из стандартных сокетов.
Материал здесь, как и ранее, не претендует на полноту, а предназначен лишь для знакомства с наиболее часто употребляемыми возможностями библиотеки сокетов. По-прежнему рассматриваются только протоколы TCP и UDP. Не будут затронуты такие вопросы, как поддержка качества обслуживания, пространства имен, простые сокеты (RAW_SOCK) и SPI (Service Provider Interface); Тем, кто захочет самостоятельно разобраться с данными вопросами, рекомендуем книгу [3].

2.2.1. Версии Windows Sockets

При рассмотрении функции WSAStartup уже упоминалось, что существуют разные версии библиотеки сокетов, которые заметно различаются по функциональности. К сожалению, полный перечень существующих на сегодняшний день версий Windows Sockets и их особенностей в документации в явном виде не приводится, но, изучая разрозненную информацию, можно сделать некоторые выводы, которые приведены в табл. 2.1. В дальнейшем, если не оговорено иное, под WinSock 1 мы будем подразумевать версию 1.1, под WinSock 2 — версию 2.2.

 

Таблица 2.1. Версии Windows Sockets
Версия Комментарий
1.0 Упоминается только вскользь. Видимо, настолько старая версия, что ее поддержка в чистом виде в современных системах отсутствует
1.1 Основная подверсия первой версии библиотеки. По умолчанию входила во все версии Windows до Windows 95 включительно. Ориентирована на 16-разрядные системы с корпоративной многозадачностью
2.0 В чистом виде никуда не ставилась. Ориентирована на 32-разрядные системы с вытесняющей многозадачностью. Исключены некоторые устаревшие функции
2.2 Основная подверсия второй версии библиотеки. По умолчанию входит в состав Windows 98/NT 4/2000 а также видимо, и более поздних версий. Для Windows 95 существует возможность обновления Windows Sockets до этой версии
WinSock 1 в 16-разрядных версиях Windows реализуется библиотекой WinSock.dll, в 32-разрядных — WSock32.dll. WinSock 2 реализуется библиотекой WS2_32.dll, и. кроме того, часть функций вынесена в отдельную библиотеку MSWSock.dll. При этом для сохранения совместимости WS2_32.dll содержит даже те устаревшие функции, которые формально исключены из спецификации WinSock 2. В тех системах, в которых установлена библиотека WinSock 2, WSock32.dll не реализует самостоятельно практически ни одной функции, а просто импортирует их из WS2_32.dll и MSWSock.dll. WSock32.dll требуется только для обратной совместимости, в новых программах необходимости в этой библиотек нет.
Как это ни удивительно, но в Delphi даже 2007-й версии (не говоря уже о более ранних) отсутствует поддержка WinSock 2. Стандартный модуль WinSock импортирует функции только из WSock32.dll, поэтому программисту доступны только функции WinSock 1. Разумеется, импортировать функции WinSock 2 самостоятельно не составит труда. Более того, в Интернете можно найти уже готовые модули, импортирующие их (например, на сайте Алекса Коншина http://home.carthlink.net/~akonshin/delphi_ru.htm). Тем не менее, чтобы избежать разночтений, мы не будем использовать какой-либо готовый модуль для импорта и примем следующее соглашение: если прототип функции приведен только на Паскале, значит, эта функция есть в модуле WinSock. Если же прототип приведен и на C/C++ и на Паскале, значит, функция в WinSock не описана. В этом случае прототип функции на C/C++ берется из MSDN, а перевод на Паскаль — импровизация автора книги. В некоторых случаях возможны несколько вариантов перевода, поэтому не стоит рассматривать приведенный здесь перевод как истину в последней инстанции. Тем, кто будет самостоятельно импортировать функции из WS2_32.dll, следует помнить, что они имеют модель вызова stdcall (при описании прототипов функций мы для краткости будем опускать эту директиву).
Примечание
С Delphi поставляется библиотека Indy (Internet Direct), в состав которой входит модуль IdWinSock2, импортирующий почти все функции WinSock 2 из системных библиотек. Импорт в нем динамический, над каждой функцией сделана обертка, которая при первом вызове проверяет, была ли уже загружена функция из библиотеки, и при необходимости загружает ее. Чтобы реализовать это, имена всех функций изменены, а вызов идет через процедурные переменные, имена которых совпадают с оригинальными именами соответствующих функций.
WinSock 2 предлагает разработчику Service Provider Interface (SPI), с помощью которого можно добавлять в систему поддержку своих протоколов. Устаревшими объявлены функции, имеющие привязку к конкретным протоколам (например, уже знакомая нам функция inet_addr, которая имеет смысл только при использовании протокола IP). Добавлены новые функции, которые призваны унифицировать операции с разными протоколами. Фактически если работать с WinSock 2, то программа может быть написана так, что сможет использовать даже те протоколы, которые не существовали на момент её разработки. Кроме того, добавлена возможность связи асинхронных сокетов с событиями вместо оконных сообщений, а также поддержка перекрытого ввода-вывода (в WinSock 1 он поддерживался только в линии NT и не в полном объеме). Добавлена поддержка качества обслуживания (Quality of Service, QoS — резервирование части пропускной способности сети для нужд конкретного соединения), поддержка портов завершения, многоадресной рассылки и регистрации имен. Большинство этих нововведений требуются для пользовательских программ относительно редко (или вообще не нужны), поэтому мы не будем заострять на них внимание. Далее будут рассмотрены асинхронные сокеты (связанные как с сообщениями, так и с событиями), перекрытый ввод-вывод, методы универсализации работы с протоколами и многоадресная рассылка.

2.2.2. Устаревшие функции WinSock 1

В этом разделе мы познакомимся с теми устаревшими функциями, которые не стоит применять в 32-разрядных программах. Рассмотрим мы их, разумеется, очень обзорно, только для того, чтобы после прочтения книги вас не смущали упоминания этих функций и связанных с ними ошибок, которые иногда встречаются в MSDN.
В 16-разрядных версиях Windows реализована так называемая корпоративная многозадачность: каждая программа время от времени должна добровольно возвращать управление операционной системе, чтобы та могла передать управление другой программе. Если какая-то программа при этом поведет себя некорректно и не вернет управление системе, то все остальные приложения не смогут продолжать работу. Другой недостаток такой модели — в ней невозможно распараллеливание работы в рамках одного процесса, т.е. создание нитей.
При такой модели многозадачности использование блокирующих сокетов может привести к остановке всей системы, если не будут приняты дополнительные меры. В Windows проблема решается следующим образом: библиотека сокетов во время ожидания периодически вызывает заранее указанную функцию. В 16-разрядных версиях Windows эта функция по умолчанию извлекает сообщение из системной очереди и передает его соответствующему приложению. Таким образом, остальные приложения не прекращают работу во время блокирующего вызова.
В очереди могут находиться сообщения и для того приложения, которое выполняет блокирующий вызов. В этом случае будет снова вызвана оконная процедура, инициировавшая блокирующую операцию. Это напоминает рекурсию, при которой процедура вызывает сама себя: в памяти компьютера будут одновременно две активации этой процедуры. Упрощенно это выглядит так: оконная процедура вызывает блокирующую функцию (например, accept), а та, в свою очередь, снова вызывает ту же самую оконную процедуру. При этом вторая активация не может выполнять никаких операций с сокетами: они будут завершены с ошибкой WSAEINPROGRESS. Эта ошибка не фатальная, она указывает, что в данный момент выполняется блокирующая операция, и программа должна подождать ее завершения и лишь потом пытаться работать с сокетами (т.е. не раньше, чем первая активация оконной процедуры вновь получит управление). Существует специальная функция WSAIsBlocking, которая возвращает True, если в данный момент выполняется блокирующая операция и работа с сокетами невозможна.
Вторая активация процедуры может прервать блокирующий вызов с помощью функции WSACancelBlockingСаll. При этом первая активация получит ошибку WSAECANCELLED.
Программа может устанавливать свою процедуру, которая будет вызываться во время выполнения блокирующей операции. Для этого предусмотрены функции WSASetBlockingHook и WSAUnhookBlockingHook.
Данная модель неудобна, поэтому разработчики WinSock 1 рекомендуют модель асинхронных сокетов, более приспособленную к особенностям Windows.
В 32-разрядных версиях WinSock такая модель работы поддерживается в полном объеме, за исключением того, что по умолчанию при блокирующем вызове не вызывается никакая функция. Поэтому если не вызове не вызывается никакая функция. Поэтому если не использовать WSASetBlockingHook, то в 32-разрядном приложении невозможно получить ситуацию, когда операция с сокетом не будет выполнена из-за того, что в этот момент уже выполняется другая операция, и второй активации оконной процедуры из-за блокирующего вызова тоже не будет создано. Отметим, что разные нити могут одновременно выполнять блокирующие операции с сокетами, и это не приведет к появлению ошибки WSAEINPROGRESS.
Все перечисленные функции формально исключены из спецификации WinSock 2, хотя фактически они присутствуют в библиотеке WS2_32.dll и при необходимости могут быть задействованы (это, правда, осложняется тем, что в новых версиях MSDN отсутствует их описание). Тем не менее причин ориентироваться на эту неудобную модель в 32-разрядных версиях Windows, видимо, нет. Описание этих функций мы здесь привели только для того, чтобы упоминания об ошибках WSAEINPROGRESS и WSAECANCELLED, которые иногда встречаются в MSDN, не смущали вас.

2.2.3. Информация о протоколе

Ранее мы уже видели, что передача данных через сокет осуществляется одними и теми же функциями независимо от протокола. Но при этом программа должна учитывать, является ли протокол потоковым, дейтаграммным или иным. Кроме того, информация о протоколе требуется для создания сокета и для распределения ролей между клиентом и сервером при установлении соединения. Чтобы работать с любым протоколом, программа должна иметь возможность получить всю эту информацию и выполнить на основе ее те или иные действия. Могут также понадобиться такие сведения, как максимальное число сокетов, поддерживаемых провайдером протокола, допустимый диапазон адресов, максимальный размер сообщений для дейтаграммных протоколов и т.д. Для хранения полного описания протокола и его провайдера в WinSock 2 предусмотрена структура WSAPROTOCOL_INFO. Она не описана в модуле WinSock, т.к. в WinSock 1 ее нет. Тем, кто захочет использовать эту структуру, придется самостоятельно добавлять ее описание в программу. Листинг 2.36 показывает, как выглядит эта структура.
Листинг 2.36. Тип WSAPROTOCOL_INFO
// ***** Описание на C++ *****
typedef struct _WSAPROTOCOLCHAIN {
 int ChainLen;
 DWORD ChainEntries[MAX_PROTOCOL_CHAIN];
} WSAPROTOCOLCHAIN, *LPWSAPROTOCOLCHAIN;

 

typedef struct _WSAPROTOCOL_INFO {
 DWORD dwServiceFlags1;
 DWORD dwServiceFlags2;
 DWORD dwServiceFlags3;
 DWORD dwServiceFlgs4;
 DWORD dwProviderFlags;
 GUID ProviderId;
 DWORD dwCatalogEntryId;
 WSAPROTOCOLCHAIN ProtocolChain;
 int iVersion;
 int iAddressFamily;
 int iMaxSockAddr;
 int iMinSockAddr;
 int iSocketType;
 int iProtocol;
 int iProtocolMaxOffset;
 int iNetworkByteOrder;
 int iSecurityScheme;
 DWORD dwMessageSize;
 DWORD dwProviderReserved;
 TCHAR szProtocol[WSAPROTOCOL_LEN - 1];
} WSAPROTOCOL_INFO, *LPWSAPROTOCOL_INFO;

 

// ***** Описание на Delphi *****
TWSAProtocolChain = packed record
 ChainLen: Integer;
 ChainEntries: array[0..MAX_PROTOCOL_CHAIN - 1] of DWORD;
end;

 

//Структура на C++ содержит тип TCHAR, который, как мы
// говорили в главе 1, может означать как Char,
// так и WideChar, т.е. структура должна иметь
// два варианта описания: TWSAProtocolInfoA для
// однобайтной кодировки и TWSAProtocolInfo для
// двухбайтной. Соответственно, все функции
// использующие эту структуру, реализованы
// в системных библиотеках в двух вариантах.
// Здесь мы приводим только ANSI-вариант.
PWSAProtocolInfo = ^TWSAProtocolInfo;
TWSAProtocolInfo = packed record
 dwServiceFlags1: DWORD;
 dwServiceFlags2: DWORD;
 dwServicsFlags3: DWORD;
 dwServiceFlags4: DWORD;
 dwProviderFlags: DWORD;
 ProviderId: GUID;
 dwCatalogEntryId: DWORD;
 ProtocolChain: TWSAProtocolChain;
 iVersion: Integer;
 iAddressFamily: Integer;
 iMaxSockAddr: Integer;
 iMinSockAddr: Integer;
 iSocketType: Integer;
 iProtocol: Integer;
 iProtocolMaxOffset: Integer;
 iNetworkByteOrder: Integer;
 iSecurityScheme: Integer;
 dwMessageSize: DWORD;
 dwProviderReserved: DWORD;
 szProtocol: array [0..WSAPROTOCOL_LEN] of Char;
end;
Расшифровка полей типа TWSAProtocolInfo есть в MSDN, мы здесь не будем ее приводить.
Сама функция WSAEnumProtocols, которая позволяет получить список всех протоколов, провайдеры которых установлены на компьютере, приведена в листинге 2.37.
Листинг 2.37. Функция WSAEnumProtocols
// ***** описание на C++ *****
int WSAEnumProtocols(LPINT lpiProtocols, LPWSAPROTOCOL_INFO lpProtocolBuffer, LPDWORD lpdwBufferLength);

 

// ***** Описание на Delphi *****
function WSAEnumProtocols(lpiProtocols: PInteger; lpProtocolBuffer: PWSAProtocolInfo; var BufferLength: DWORD): Integer;
Примечание
В старых версиях MSDN в описании этой функции есть небольшая опечатка: тип параметра lpdwBufferLength назван LLPDWORD вместо LPDWORD.
Библиотека WS2_32.dll придерживается тех же правил насчет ANSI- и Unicode-вариантов функций, что и другие системные библиотеки (см. разд. 1.1.12), поэтому в ней нет функции с именем WSAEnumProtocols, а есть WSAEnumProtocolsA и WSAEnumProtocolsW. Эти функции работают с разными вариантами структуры WSAPROTOCOL_INFO, которые различаются типом элементов в последнем массиве — CHAR или WCHAR.
Параметр lpiProtocols указывает на первый элемент массива, содержащего список протоколов, информацию о которых нужно получить. Если этот указатель равен nil, то возвращается информация обо всех доступных протоколах. Параметр lpProtocolBuffer содержит указатель на начало массива структур типа TWSAProtocolInfo. Программа должна заранее выделить память под этот массив. Параметр BufferLength при вызове должен содержать размер буфера lpProtocolBuffer в байтах (именно размер в байтах, а не количество элементов). После завершения функции сюда помешается минимальный размер буфера, необходимый для размещения информации обо всех запрошенных протоколах. Если это значение больше переданного, функция завершается с ошибкой.
Если параметр lpiProtocols не равен нулю, он должен содержать указатель на массив, завершающийся нулем. Следовательно, если количество протоколов, запрашиваемых программой, равно N, этот массив должен состоять из N+1 элементов, и первые N элементов должны содержать номера протоколов, а последний элемент — ноль.
В системе может быть установлено несколько провайдеров для одного протокола. В этом случае информация о каждом провайдере будет помещена в отдельный элемент массива. Из-за этого число задействованных элементов в массиве lpProtocolBuffer может превышать количество протоколов, определяемых параметром lpiProtocols.
К сожалению, полную информацию о том, каким протоколам какие номера соответствуют, в документации найти не удалось. Можно только сказать, что для получения информации о протоколе TCP в массив lpiProtocols необходимо поместить константу IPPROTO_TCP, о протоколе UDP — константу IPPROTO_UDP.
Возвращаемое функцией значение равно числу протоколов, информация о которых помещена в массив, если функция выполнена успешно, и SOCKET_ERROR, если при ее выполнении возникла ошибка. Конкретная ошибка определяется стандартным методом, с помощью WSAGetLastError. Если массив lpProtocolBuffer слишком мал для хранения всей требуемой информации, функция завершается с ошибкой WSAENOBUFS.
WinSock 1 содержит аналогичную по возможности функцию EnumProtocols, возвращающую массив структур PROTOCOL_INFO. Эта структура содержит меньше информации о протоколе, чем WSAPROTOCOL_INFO и, в отличие от последней, не используется никакими другими функциями WinSock. Несмотря на то, что функция EnumProtocols и структура PROTOCOL_INFO описаны в первой версии WinSock, модуль WinSock их не импортирует, при необходимости их нужно импортировать самостоятельно. Но функция EnumProtocols считается устаревшей, использовать ее в новых приложениях не рекомендуется, поэтому практически всегда, за исключением редких случаев, требующих совместимости с WinSock 1, лучше выбрать более современную функцию WSAEnumProtocols.

2.2.4. Новые функции

В этом разделе мы рассмотрим некоторые новые функции, появившиеся в WinSock 2. Большинство из них позволяет выполнять действия, уже знакомые нам из предыдущих разделов, но предоставляет большие возможности, чем стандартные сокетные функции.
Для создания сокета предназначена функция WSASocket со следующим прототипом (листинг 2.38).
Листинг 2.38. Функция WSASocket
// ***** Описание на C++ *****
SOCKET WSASocket(int af, int SockType, int protocol, LPWSAPROTOCOL_INFO lpProtocolInfo, GROUP g, DWORD dwFlags);

 

// ***** Описание на Delphi *****
function WSASocket(AF, SockType, Protocol: Integer; lpProtocolInfo: PWSAProtocolInfo; g: TGroup; dwFlags: DWORD): TSocket;
Первые три параметра совпадают с тремя параметрами функции socket. Параметр lpProtocolInfo указывает на структуру TWSAProtocolInfo, содержащую информацию о протоколе, для которого создается сокет. Если этот указатель равен nil, функция создает сокет на основании первых трёх параметров так же, как это делает функция socket. С другой стороны, если этот параметр не равен nil, то структура, на которую он указывает, содержит всю информацию, необходимую для создания сокета, поэтому первые три параметра должны быть равны константе FROM_PROTOCOL_INFO (-1). Параметр g зарезервирован для использования в будущем и должен быть равен нулю (тип TGroup совпадает с DWORD). Последний параметр dwFlags определяет, какие дополнительные возможности имеет создаваемый сокет. Вызов функции socket эквивалентен вызову функции WSASocket с флагом WSA_FLAG_OVERLAPPED, который показывает, что данный сокет можно использовать для перекрытого ввода-вывода (см. разд. 2.2.9). Остальные флаги нужны при многоадресной рассылке (не все из них допустимы для протоколов TCP и UDP). Эти флаги мы рассмотрим в разд. 2.2.11.
В случае TCP и UDP функция WSASocket дает следующие преимущества по сравнению с функцией socket. Во-первых, через параметр lpProtocolInfo появляется возможность явно указать провайдера, который будет выбран программой. Во-вторых, если программа не использует перекрытый ввод-вывод, можно создавать сокеты без флага WSA_FLAG_OVERLAPPED, экономя при этом некоторое незначительное количество ресурсов. Кроме того, как это будет обсуждаться далее, с помощью WSASocket две разных программы могут работать с одним и тем же сокетом.
Функция WSAConnect — это более мощный аналог connect. Ее прототип приведен в листинге 2.39.
Листинг 2.39. Функция WSAConnect и связанные с ней типы
// ***** Описание на C++ *****
int WSAConnect(SOCKET s, const struct sockaddr FAR* name, int name len, LPWSABUF lpCallerData, LPWSABUF lpCalleeData, LPQOS lpSQOS, LPQOS lpGQOS);

 

typedef struct __WSABUF {
 u_long len;
 char FAR *buf;
} WSABUF, FAR* LPWSABUF;

 

// ***** Описание на Delphi ******
function WSAConnect(S: TSocket; var Name: TSockAddr; NameLen: Integer; lpCollerData, lpCalleeData: PWSABuf; lpSQOS, lpGQOS: PQOS): Integer;

 

PWSABuf = ^TWSABuf;
TWSABuf = packed record
 Len: Cardinal;
 Buf: PChar;
end;
Функция WSAConnect устанавливает соединение со стороны клиента. Ее первые три параметра совпадают с параметрами функции connect. Параметр lpCallerData и lpCalleeData служат для передачи данных от клиента серверу и от сервера клиенту при установлении соединения. Они оба являются указателями на структуру TWSABuf тип TWSABuf, которая содержит размер буфера Len и указатель на буфер Buf. Протоколы стека TCP/IP не поддерживают передачу данных при соединении, поэтому для TCP и UDP lpCallerData и lpCalleeData должны быть равны nil. Параметры lpSQOS и lpGQOS — это указатели на структуры, с помощью которых программа передает свои требования к качеству обслуживания, причем параметр lpGQOS связан с не поддерживаемым в настоящий момент групповым качеством и всегда должен быть равен nil. Параметр lpSQOS также должен быть равен nil, если программа не предъявляет требований к качеству обслуживания. Так как рассмотрение качества обслуживания выходит за рамки данной книги, мы не приводим здесь определение структуры SQOS, которое при необходимости легко найти в MSDN.
Между функциями connect и WSAConnect существует небольшое различие при работе с сокетами, не поддерживающими соединение. Как вы знаете из разд. 2.1.9, функция connect может использоваться с такими сокетами для задания адреса отправки по умолчанию и автоматической фильтрации входящих пакетов. Для того чтобы отменить такое "соединение", нужно при вызове функции connect указать адрес INADDR_ANY и нулевой порт. В случае WSAConnect для отмены "соединения" требуется, чтобы все без исключения поля структуры Name, включая sin_family, были нулевыми. Это сделано для того, чтобы обеспечить независимость от протокола: при любом протоколе для разрыва "соединения" должно устанавливаться одно и то же значение Name.
Если программа не предъявляет требований к качеству обслуживания, то для протоколов TCP и UDP функция WSAConnect не предоставляет никаких преимуществ по сравнению с connect.
Функция accept из стандартной библиотеки сокетов позволяет серверу извлечь из очереди соединений информацию о подключившемся клиенте и создать сокет для его обслуживания. Эти действия выполняются безусловно, для любых подключившихся клиентов. Если сервер допускает подключение не любых клиентов, а только тех, которые отвечают некоторым условиям (для протокола TCP эти условия могут заключаться в том, какие IP-адреса и какие порты допустимо использовать клиентам), сразу после установления соединения его приходится разрывать, если клиент не удовлетворяет этим условиям. Для упрощения этой операции в WinSock 2 предусмотрена функция WSAAccept, прототип которой приведен в листинге 2.40. 
Листинг 2.40. Функция WSAAccept
// ***** Описание на C++ *****
SOCKET WSAAccept(SOCKET S, struct sockaddr FAR* addr, LPINT addrlen, LPCONDITIONPROC lpfnCondition, dwCallbackData);

 

// ***** описание на Delphi *****
function WSAAccept( S: TSocket; Addr: PSockAddr; AddrLen: PInteger; lpfnCondition: TConditionProc; dwCallbackData: DWORD): TSocket;
По сравнению с уже известной нам функцией accept функция WSAAccept имеет два новых параметра: lpfnCondition и dwCallbackData. lpfnCondition является указателем на функцию обратного вызова. Эта функция объявляется и реализуется программой. WSAAccept вызывает ее внутри себя и в зависимости от ее результата принимает или отклоняет соединение. Параметр dwCallbackData не имеет смысла для самой функции WSAAccept и передается без изменений в функцию обратного вызова. Тип TConditionProc должен быть объявлен следующим образом (листинг 2.41).
Листинг 2.41. Тип TConditionProc
// ***** Описание на C++ *****
typedef (int*)(LPWSABUF lpCallerId, LPWSABUF lpCallerData, LPQOS lpSQOS, LPQOS lpGQOS, LPWSABUF lpCalleeId, LPWSABUF lpCalleeData, GROUP FAR* g, DWORD dwCallbackData) LPCONDITIONPROC;

 

// ***** Описание на Delphi *****
TConditionProc = function(lpCallerId, lpCallerData: PWSABuf; lpSQOS, lpGQOS: PQOS; lpCalleeID, lpCalleeData: PWSABuf; g: PGroup; dwCallbackData: DWORD): Integer; stdcall;
Параметр lpCallerId указывает на буфер, в котором хранится адрес подключившегося клиента. При работе со стеком TCP/IP lpCallerId^.Len будет равен SizeOf(TSockAddr), a lpCallerId^.Buf будет указывать на структуру TSockAddr, содержащую адрес клиента. Параметр lpCallerData определяет буфер, в котором хранятся данные, переданные клиентом при соединении. Как уже отмечалось, протоколы стека TCP/IP не поддерживают передачу данных при соединении, поэтому для них этот параметр будет равен nil. Параметры lpSQOS и lpGQOS задают требуемое клиентом качество обслуживания для сокета и для группы соответственно. Так как группы сокетов в текущей реализации WinSock не поддерживаются, параметр lpGQOS будет равен nil. Параметр lpSQOS тоже будет равен nil, если клиент не задал качество обслуживания при соединении.
Параметр lpCalleeId содержит адрес интерфейса, принявшего соединение (поля структуры при этом используются так же, как у параметра lpCallerId). Ранее уже обсуждалось, что сокет, привязанный к адресу INADDR_ANY, прослушивает все сетевые интерфейсы, имеющиеся на компьютере, но каждое подключение, созданное с его помощью, использует конкретный интерфейс. Параметр lpCalleeId содержит адрес, привязанный к конкретному соединению. Параметр lpCalleeData указывает на буфер, в который сервер может поместить данные для отправки клиенту. Этот параметр также не имеет смысла для протокола TCP, не поддерживающего отправку данных при соединении.
Параметр g выходной, он позволяет управлять присоединением создаваемого функцией WSAAccept сокета к группе. Параметр, как и все, связанное с группами, зарезервирован для использования в будущем.
Примечание
Если вы пользуетесь старой версией MSDN, то можете не обнаружить там описания параметра g — оно там отсутствует. Видимо, просто по ошибке.
И наконец, через параметр dwCallbackData в функцию обратного вызова передается значение параметра dwCallbackData, переданное в функцию WSAAccept. Программист должен сам решить, как ему интерпретировать это значение.
Функция должна вернуть CF_ACCEPT (0), если соединение принимается, CF_REJECT (1), если оно отклоняется, и CF_DEFER (2), если решение о разрешении или запрете соединения откладывается. Если функция обратного вызова вернула CF_REJECT, to WSAAccept завершается с ошибкой WSAECONNREFUSED, если CF_DEFER — то с ошибкой WSATRY_AGAIN (в последнем случае соединение остаётся в очереди, и информация о нем вновь будет передана в функцию обратного вызова при следующем вызове WSAAccept). Обе эти ошибки не фатальные, сокет остается в режиме ожидания соединения и может принимать подключения от новых клиентов.
Ранее уже обсуждалось, что функция connect на стороне клиента считается успешно завершенной тогда, когда соединение встало в очередь, а не тогда, когда оно реально принято сервером через функцию accept. По умолчанию для клиента, соединение с которым сервер отклонил, нет разницы, вызвал ли сервер функцию WSAAccept и сразу отклонил соединение, или установил его с помощью accept, а потом разорвал. В обоих случаях клиент сначала получит информацию об успешном соединении с сервером, а потом это соединение будет разорвано. Но при использовании WSAAccept можно установить такой режим работы, когда сначала выполняется функция. заданная параметром lpCondition, и лишь потом клиенту отправляется разрешение или запрет на подключение. Включается этот режим установкой параметра слушающего сокета SO_CONDITIONAL_ACCEPT, что иллюстрирует листинг 2.42.
Листинг 2.42. Включение режима ожидания реального подключения
var
 Cond: BOOL;
begin
 Cond := True;
 setsockopt(S, SOL_SOCKET, SO_CONDITIONAL_ACCEPT, PChar(@Cond), SizeOf(Cond));
Этот режим снижает нагрузку на сеть и повышает устойчивость сервера против DoS-атак, заключающихся в многократном подключении-отключении посторонних клиентов, поэтому в серьезных серверах рекомендуется использовать эту возможность.
Из сказанного следует, что при использовании протокола TCP функция WSAAccept по сравнению с accept даёт два принципиальных преимущества: позволяет управлять качеством обслуживания и запрещать подключение нежелательных клиентов.
Некоторые протоколы поддерживают передачу информации не только при установлении связи, но и при её завершении. Для таких протоколов в WinSock2 предусмотрены функции WSASendDisconnect и WSARecvDisconnect. Так как протокол TCP не поддерживает передачу данных при закрытии соединения, для него эти функции не дают никаких преимуществ по сравнению с вызовом функции shutdown, поэтому мы не будем их здесь рассматривать.
Далее мы рассмотрим несколько новых функций, унифицирующих работу с различными протоколами.
Функция inet_addr, как это уже упоминалось, жестко связана с протоколом IP и не имеет смысла для других протоколов. WinSock 2 предлагает вместо нее функцию WSAStringToAddress, имеющую следующий прототип (листинг 2.43).
Листинг 2.43. Функция WSAStringToAddress
// ***** Описание на C++ *****
INT WSAStringToAddress(LPTSTR AddressString, INT AddressFamily, LPWSAPROTOCOL_INFO lpProtocolInfo, LPSOCKADDR lpAddress, LPINT lpAddressLength);
// ***** Описание на Delphi *****
function WSAStringToAddress(AddresString: PChar; AddressFamily: Integer; lpProtocolInfo: PWSAProtocolInfo; var Address: TSockAddr; var AddressLength: Integer): Integer;
Данная функция преобразует строку, задающую адрес сокета, в адрес, хранящийся в структуре TSockAddr. Параметр AddressString указывает на строку, хранящую адрес, параметр AddressFamily — на семейство адресов, для которого осуществляется трансляция. Если есть необходимость выбрать конкретный провайдер для протокола, в функцию может быть передан параметр lpProtocolInfo, в котором указан идентификатор провайдера. Если же программу устраивает провайдер по умолчанию, параметр lpProtocolInfo должен быть равен nil. Адрес возвращается через параметр Address. Параметр AddressLength при вызове функции должен содержать размер буфера, переданного через Address, а на выходе содержит реально использованное число байтов в буфере.
Функция возвращает 0 в случае успешного выполнения и SOCKET_ERROR — при ошибке. 
Допустимый формат строки определяется протоколом (некоторые протоколы вообще не поддерживают текстовую запись адреса, и для них функция WSAStringToAddress неприменима). Для семейства AF_INET, к которому относятся TCP и UDP, адрес может задаваться в виде "IP1.IP2.IP3.IР4:Port" или "IP1.IP2.IP3.IP4", где IРn — n-й компонент IP-адреса, записанною в виде 4-байтных полей, Port — номер порта. Если порт явно не указан, устанавливается нулевой номер порта.
Таким образом, чтобы в структуре TSockAddr оказался, например, адрес 192.168.100.217 и порт с номером 5000, необходимо выполнить следующий код (листинг 2.44).
Листинг 2.44. Пример использования функции WSAStringToAddress
var
 Addr: TSockAddr;
 AddrLen: Integer;
begin
 AddrLen := SizeOf(Addr);
 WSAStringToAddress('192.168.100.217:5000', AF_INET, nil, Addr, AddrLen);
Существует также функция WSAAddressToString, обратная к WSAStringToAddrеss. Ее прототип приведен в листинге 2.45.
Листинг 2.45. Функция WSAAddressToString
// ***** Описание на C++ *****
INT WSAAddressToString(LPSOCKADDR lpsaAddress, DWORD dwAddressLength, LWSAPROTOCOL_INFO lpProtocolInfo, LPTSTR lpszAddressString, LPDWORD lpdwAddressStringLength);

 

// ***** Описание на Delphi *****
function WSAAddressToString(var Address: TSockAddr; dwAddressLength: DWORD; lpProtocolInfo: PWSAProtocolInfo; lpszAddressString: PChar; var AddressStringLength: DWORD): Integer;
Как нетрудно догадаться по названию функции, она преобразует адрес, заданный структурой TSockAddr, в строку. Адрес задаётся параметром Address, параметр dwAddressLength определяет длину буфера Address. Необязательный параметр lpProtocolInfo содержит указатель на структуру TWSAProtocolInfo, с помощью которой можно определить, какой именно провайдер должен выполнить преобразование. Параметр lpszAddressString содержит указатель на буфер, заранее выделенный программой, в который будет помещена строка. Параметр AddressStringLength на входе должен содержать размер буфера, заданного параметром lpszAddressString, а на выходе содержит длину получившейся строки.
Функция возвращает ноль в случае успеха и SOCKET_ERROR — при ошибке. Ранее мы уже обсуждали различные форматы представления целых чисел, а также то, что формат, предусмотренный сетевым протоколом, может не совпадать с форматом, используемым узлом. Напомним, что для преобразования из сетевого формата в формат узла предназначены функции htons, ntohs, htonl и ntohl, привязанные к протоколам стека TCP/IP (другие протоколы могут иметь другой формат представления чисел). WinSock 2 предлагает аналоги этих функций WSAHtons, WSANtohs, WSAHtonl и WSANtohl, которые учитывают особенности конкретного протокола. Мы здесь рассмотрим только функцию WSANtohl, преобразующую 32-битное целое из сетевого формата в формат узла. Остальные три функции работают аналогично. Листинг 2.46 содержит прототип функции WSANtohl.
Листинг 2.46. Функция WSANtohl
// ***** Описание на C++ *****
int WSANtohl(SOCKET s, u_long netlong, u_long FAR *lphostlong);

 

// ***** Описание на Delphi *****
function WSANtohl(S: TSocket; NetLong: Cardinal; var HostLong: Cardinal): Integer;
Параметр S задает сокет, для которого осуществляется преобразование. Так как сокет всегда связан с конкретным протоколом, этого параметра достаточно, чтобы библиотека могла определить, по какому закону преобразовывать число из сетевого формата в формат хоста. Число в сетевом формате задаётся параметром NetLong, результат преобразования помещается в параметр HostLong.
Функция возвращает ноль в случае успешного выполнения операции и SOCKET_ERROR — при ошибке.
Если программа работает только с протоколами стека TCP/IP, старые варианты функций удобнее новых, потому что возвращают непосредственно результат преобразования, который можно использовать в выражениях. При работе с новыми функциями для получения результата следует заводить отдельную переменную, поэтому эти функции целесообразны тогда, когда программа должна единым образом работать с разными протоколами. Последняя функция, которую мы здесь рассмотрим, не имеет прямых аналогов среди старых функций. Называется она WSADuplicateSocket и служит для копирования дескриптора сокета в другой процесс. Прототип функции WSADuplicateSocket приведен в листинге 2.47.
Листинг 2.47. Функция WSADuplicateSocket
// ***** Описание на C++ *****
int WSADuplicateSocket(SOCKET s, DWORD dwProcessId, LPWSAPROTOCOL_INFO lpProtocolInfo);

 

// ***** Описание на Delphi *****
function WSADuplicateSocket(S: TSocket; dwProcessID: DWORD; var ProtocolInfo: TWSAProtocolInfo): Integer;
Параметр S задает сокет, дескриптор которого нужно скопировать, параметр dwProcessID — идентификатор процесса, для которого предназначена копия, функция помещает в структуру ProtocolInfo информацию, необходимую для создания копии дескриптора другим процессом. Затем эта структура должна быть каким-то образом передана другому процессу, который передаст ее в функцию WSASocket и получит свою копию дескриптора для работы с данным сокетом.
Функция WSADuplicateSocket возвращает ноль при успешном завершении и SOCKET_ERROR — в случае ошибки. Как мы помним, сокет является объектом, внутренняя структура которого остается скрытой от использующей его программы. Программа манипулирует только дескриптором сокета — некоторым уникальным идентификатором этого объекта. Функция WSADuplicateSocket позволяет другой программе получить новый дескриптор для уже существующего сокета. Старый и новый дескриптор становятся равноправными. Чтобы освободить сокет, нужно закрыть все его дескрипторы с помощью функции closesocket. Если во входной буфер сокета поступают данные, их получит та программа, которая первой вызовет соответствующую функцию чтения, поэтому совместное использование одного сокета разными программами требует синхронизации их работы. MSDN рекомендует такую схему работы, при которой одна программа только создаёт сокет и устанавливает соединение, а затем передает сокет другой программе, которая реализует через него ввод-вывод. Первая программа при этом закрывает свой дескриптор. Такой алгоритм работы позволяет полностью исключить проблемы, возникающие при совместном доступе разных программ к одному сокету.
Отметим, что функция WSADuplicateSocket может быть полезна только для копирования дескрипторов между разными процессами. Разные нити одного процесса не нуждаются в этой функции, т.к., находясь в одном адресном пространстве, они могут работать с одним и тем же дескриптором.

2.2.5. Асинхронный режим, основанный на сообщениях

Все операции с сокетами, которые мы рассматривали раньше, являлись синхронными. Программа, использующая такие сокеты, должна сама время от времени проверять тем или иным способом, пришли ли данные, установлена ли связь и т.п. Асинхронные сокеты позволяют программе получать уведомления о событиях, происходящих с сокетом: поступлении данных, освобождении места в буфере, закрытии и т.п. Такой способ работы лучше подходит для событийно-ориентированных программ, типичных для Windows. Поддержка асинхронных сокетов впервые появилась в WinSock 1 и была основана на сообщениях, которые обрабатывались оконными процедурами. В WinSock 2 этот асинхронный режим остался без изменений. Программист указывает, какое сообщение какому окну должно приходить при возникновении события на интересующем его сокете.
Асинхронный режим с уведомлением через сообщения устанавливается функцией WSAAsyncSelect, имеющей следующий прототип:
function WSAAsyncSelect(S: TSocket; HWindow: HWND; wMsg: u_int; lEvent: LongInt): Integer;
Параметр S определяет сокет, для которого устанавливается асинхронный режим работы. Параметр HWindow — дескриптор окна, которому будут приходить сообщения, wMsg — сообщение, a lEvent задает события, которые вызывают отправку сообщения. Для этого параметра определены константы, комбинация которых задает интересующие программу события. Мы не будем рассматривать здесь все возможные события, остановимся только на самых главных (табл. 2.2).

 

Таблица 2.2. Асинхронные события сокета
Событие Комментарий
FD_READ Сокет готов к чтению
FD_WRITE Сокет готов к записи
FD_ACCEPT В очереди сокета есть подключения (применимо только для сокетов, находящихся в режиме ожидания подключения)
FD_CONNECT Соединение установлено (применимо только для сокетов, для которых вызвана функция connect или аналогичная ей)
FD_CLOSE Соединение закрыто
Каждый последующий вызов WSAAsyncSelect для одного и того же сокета отменяет предыдущий вызов. Таким образом, в результате выполнения следующего кода форма будет получать только сообщения, показывающие готовность сокета к чтению, а готовность к записи не приведет к отправке сообщения (листинг 2.48).
Листинг 2.48. Последовательный вызов функции WSAAsyncSelect
WSAAsyncSelect(S, Form1.Handle, WM_USER, FD_WRITE);
// Второй вызов отменит результаты первого
WSAAsyncSelect(S, Form1.Handle, WM_USER, FD_READ);
// Теперь окно не будет получать уведомления о возможности записи
WSAAsyncSelect связывает с сообщением именно сокет, а не его дескриптор. Это означает, что если две программы используют один сокет (копия дескриптора которого была создана с помощью функции WSADuplicateSocket), и первая программа вызывает WSAAsyncSelect со своим дескриптором, а затем вторая — со своим, то вызов WSAAsyncSelect, сделанный во второй программе, отменит вызов, сделанный в первой.
Для того, чтобы получать сообщения при готовности сокета как к чтению, так и к записи, нужно выполнить следующий код.
WSAAsyncSelect(S, Form1.Handle, WM_USER, FD_READ or FD_WRITE);
При необходимости с помощью or можно комбинировать и большее число констант.
Из сказанного следует, что нельзя связать с разными событиями одного и того же сокета разные сообщения (или отправлять сообщения разным окнам), т.к. при одном вызове WSAAsyncSelect можно передать только один дескриптор окна и один номер сообщения, а следующий вызов этой функции, с другим дескриптором и/или номером, отменит предыдущий. Функция WSAAsyncSelect переводит сокет в неблокирующий режим. Если необходимо использовать асинхронный сокет в блокирующем режиме, после вызова WSAAsyncSelect требуется перевести его в этот режим вручную.
Сообщение, которое связывается с асинхронным сокетом, может быть любым. Обычно его номер выбирают от WM_USER и выше, чтобы исключить путаницу со стандартными сообщениями.
При получении сообщения его параметр wParam содержит дескриптор сокета, на котором произошло событие. Младшее слово lParam содержит произошедшее событие (одну из констант 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) не смогла положить данные в буфер из-за нехватки места в нем (в этом случае функция вернет значение, меньшее, чем размер переданных данных, или завершится с ошибкой WSAEWOULBBLOCK). Как только в выходном буфере сокета снова появится свободное место, возникнет событие 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. В листинге 2.49 приведен пример кода сервера, использующего асинхронные сокеты. Сервер работает в режиме запрос-ответ, т.е. посылает какие-то данные клиенту только в ответ на его запросы. Константа WM_SOCKETEVENT, определенная в коде для сообщений, связанных с сокетом, может, в принципе, иметь и другие значения.
Листинг 2.49. Пример простого сервера на асинхронных сокетах
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: TObjеct);
 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_STREAM, 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_АССЕРТ: begin
  // Просто вызываем функция accept. Ее результат нигде не
  // сохраняется, потому что вновь созданный сокет автоматически
  // начинает работать в асинхронном режиме, и его дескриптор
  // при необходимости будет передан через Msg.wParam при
  // возникновение события
  accept(Sock, nil, nil);
 end;
 FD_CLOSE:
 begin
  // Получив от клиента сигнал завершения, сервер, в принципе,
  // может попытаться отправить ему данные. После этого сервер
  // также должен закрыть соединение со своей стороны
  shutdown(Sock, SD_SEND);
  closesocket(Sock);
 end;
 end;
end;

 

end.
Преимущество такого сервера по сравнению с сервером, основанным на функции select, заключается в том, что он не должен постоянно проверять наличие полученных данных — когда данные поступят, он без дополнительных усилий получит уведомление об этом. Кроме того, этот сервер не имеет проблем, связанных с количеством сокетов в множестве типа TFDSet. Впрочем, последнее несущественно, т.к. при таком количестве клиентов сервер обычно реализует другие, более производительные способы взаимодействия с клиентами.

2.2.6. Пример сервера, основанного на сообщениях

В этом разделе мы напишем сервер, использующий асинхронные сокеты и их сообщения (пример AsyncSelectServer на компакт-диске). Этот сервер будет во многом похож на сервер на основе неблокирующих сокетов (см. разд. 2.1.16), только он не станет проверять по таймеру наличие данных в буфере и возможность отправки данных, а будет выполнять это тогда, когда поступят соответствующие сообщения.
Такая схема работы требует более осторожного подхода. По сигналу от таймера мы сами проверяем, на каком этапе в данный момент находится обмен данными с клиентом. Если, например, идет этап отправки данных, то проверять входной буфер сокета не нужно, можно оставить это до тех пор, пока не наступит этап чтения данных. При использовании сообщений приходится учитывать, что сообщение о поступлении данных в буфер сокета может прийти в любой момент, в том числе и тогда, когда обмен с клиентом находится на этапе отправки строки. По протоколу сервер не должен читать сообщение в этот момент, необходимо сначала закончить отправку, поэтому приходится данное уведомление игнорировать. Но второго уведомления система не пришлет, соответственно, после окончания отправки данных сервер должен сам вспомнить, что было уведомление, и перейти к операции чтения.
Примечание
Вообще говоря, ситуация, когда сервер не отправит данные за один раз, и их отправка растянется на несколько итераций петли сообщений, настолько редка, что при разработке сервера, к которому не предъявляются повышенные требования по надежности, ее можно было бы вообще не учитывать. Соответственно, возможность получения сервером нового уведомления о поступлении данных до того, как на старое сообщение будет дан ответ, возможна только тогда, когда клиент не соблюдает принятый протокол и посылает несколько сообщений подряд, не дожидаясь ответа. Наш пример призван продемонстрировать наиболее надежный к подобным действиям клиента сервер, поэтому мы его напишем "по всем правилам".
Как обычно, работа сервера начинается с инициализации слушающего сокета, выполняющейся при нажатии кнопки Запустить (листинг 2.50).
Листинг 2.50. Инициализация сервера, основанного на сообщениях
procedure TServerForm.BtnStartServerClick(Sender: TObject);
var
 // Адрес, к которому привязывается слушающий сокет
 ServerAddr: TSockAddr;
begin
 // Формируем адрес для привязки.
 FillChar(ServerAddr.sin_zero, SizeOf(ServerAddr.sin_zero), 0);
 ServerAddr.sin_family := AF_INET;
 ServerAddr.sin_addr.S_addr := INADDR_ANY;
 try
  ServerAddr.sin_port := htons(StrToInt(EditPortNumber.Text));
  if ServerAddr.sin_port = 0 then
  begin
   MessageDlg('Номер порта должен находиться в диапазоне 1-65535',
    mtError, [mbOK], 0);
   Exit;
  end;
  // Создание сокета
  FServerSocket := socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if FServerSocket = INVALID_SOCKET then
  begin
   MessageDlg('Ошибка при создании сокета:'#13#10 +
    GetErrorString, mtError, [mbOK], 0);
   Exit;
  end;
  // Привязка сокета к адресу
  if bind(FServerSocket, ServerAddr, SizeOf(ServerAddr)) = SOCKET_ERROR then
  begin
   MessageDlg('Ошибка при привязке сокета к адресу:'#13#10 +
    GetErrorString, mtError, [mbOK], 0);
   closesocket(FServerSocket);
   Exit;
  end;
  // Перевод сокета в режим прослушивания
  if listen(FServerSocket, SOMAXCONN) = SOCKET_ERROR then
  begin
   MessageDlg('Ошибка при переводе сокета в режим прослушивания:'#13#10 +
    GetErrorString, mtError, [mbOK], 0);
   closesocket(FServerSocket);
   Exit;
  end;
  // Связь слушающего сокета с событием FD_ACCEPT
  if WSAAsyncSelect(FServerSocket, Handle,
WM_ACCEPTMESSAGE, FD_ACCEPT) = SOCKET_ERROR then
  begin
   MessageDlg('Ошибка при установке асинхронного режима ' +
    'cлушающего сокета:'#13#10 + GetErrorString, mtError, [mbOK], 0);
   closesocket(FServerSocket);
   Exit;
  end;
  // Перевод элементов управления в состояние "Сервер работает"
  LabelPortNumber.Enabled := False;
  EditPortNumber.Enabled := False;
  BtnStartServer.Enabled := False;
  LabelServerState.Caption := 'Сервер работает';
 except
  on EConvertError do
   // Это исключение может возникнуть только в одном месте -
   // при вызове StrToInt(EditPortNumber.Text)
   MessageDlg('"' + EditPortNumber.Text +
    '" не является целый числом', mtError, [mbOK], 0);
  on ERangeError do
   // Это исключение может возникнуть только в одном месте -
   // при присваивании значения номеру порта
   MessageDlg('Номер порта должен находиться в диапазоне 1-65535',
    mtError, [mbOK], 0);
 end;
end;
Этот код мало чем отличается от того, что мы уже видели (сравните, например, с листингами 2.19 и 2.30). Единственное существенное отличие здесь — вызов функции WSAAsyncSelect после перевода сокета в режим прослушивания. Этот вызов связывает событие FD_ACCEPT с сообщением WM_ACCEPTMESSAGE.
Сообщение WM_ACCEPTMESSAGE нестандартное, мы должны сами определить его. Использовать это сообщение сервер будет только для определения момента подключения нового клиента, определять момент прихода данных мы будем с помощью другого сообщения — WM_SOCKETMESSAGE, которое тоже нужно определить. И, чтобы легче было писать обработчики для этих сообщений, объявим тип TWMSocketMessage, "совместимый" с типом TMessage (листинг 2.51).
Листинг 2.51. Сообщения, связанные с сокетами, и тип TWMSocketMessage
const
 WM_ACCEPTMESSAGE = WM_USER + 1;
 WM_SOCKETMESSAGE = WM_USER + 2;
type
 TWMSocketMessage = packed record
  Msg: Cardinal;
  Socket: TSocket;
  SockEvent: Word;
  SockError: Word;
 end;
Прежде чем реализовывать реакцию на эти сообщения, нужно позаботиться об обработке ошибок. Функция GetErrorString (см. листинг 2.6), столько времени служившая нам верой и правдой, нуждается в некоторых изменениях. Это связано с тем, что теперь код ошибки может быть получен не только в результате вызова функции WSAGetLastError, но и через параметр SockError сообщения. Новый вариант функции GetErrorString иллюстрирует листинг 2.52.
Листинг 2.52. Новый вариант функции GetErrorString
// функция GetErrorString возвращает сообщение об ошибке,
// сформированное системой на основе значения, которое
// передано в качестве параметра. Если это значение
// равно нулю (по умолчанию), функция сама определяет
// код ошибки, используя функцию WSAGetLastError.
// Для получения сообщения используется системная функция
// FormatMessage.
function GetErrorString(Error: Integer = 0): string;
var
 Buffer: array[0..2047] of Char;
begin
 if Error = 0 then Error := WSAGetLastError;
 FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, nil, Error, $400,
  @Buffer, SizeOf(Buffer), nil);
 Result := Buffer;
end;
Сам обработчик сообщения WM_ACCEPTMESSAGE приведен в листинге 2.53.
Листинг 2.53. Обработчик сообщения WM_ACCEPTMESSAGE
procedure TServerForm.WMAcceptMessage(var Msg: TWMSocketMessage);
var
 NewConnection: PConnection;
 // Сокет, который создаётся для вновь подключившегося клиента
 ClientSocket: TSocket;
 // Адрес подключившегося клиента
 ClientAddr: TSockAddr;
 // Длина адреса
 AddrLen: Integer;
begin
 // Страхуемся от "тупой" ошибки
 if Msg.Socket <> FServerSocket then
  raise ESocketError.Create(
  'Внутренняя ошибка сервера - неверный серверный сокeт');
 // Обрабатываем ошибку на сокете, если она есть.
 if Msg.SockError <> 0 then
 begin
  MessageDlg('Ошибка при подключении клиента:'#13#10 +
   GetErrorString(Msg.SockError) +
   #13#10'Сервер будет остановлен', mtError, [mbOK], 0);
  ClearConnections;
  closesocket(FServerSocket);
  OnStopServer;
  Exit;
 end;
 // Страхуемся от еще одной "тупой" ошибки
 if Msg.SockEvent <> FD_ACCEPT then
  raise ESocketError.Create(
   'Внутренняя ошибка сервера — неверное событие на сокете');
 AddrLen := SizeOf(TSockAddr);
 ClientSocket := accept(FServerSocket, @ClientAddr, @AddrLen);
 if ClientSocket = INVALID_SOCKET then
 begin
  // Если произошедшая ошибка - WSAEWOULDBLOCK, это просто означает,
  // что на данный момент подключений нет, а вообще все в порядке,
  // поэтому ошибку WSAEWOULDBLOCK мы просто игнорируем. Прочие же
  // ошибки могут произойти только в случае серьезных проблем,
  // которые требуют остановки сервера.
  if WSAGetLastError <> WSAEWOULDBLOCK then
  begin
   MessageDlg('Ошибка при подключении клиента:'#13#10 + GetErrorString +
    #13#10'Сервер будет остановлен', mtError, [mbOK], 0);
   ClearConnections;
   closesocket(FServerSocket);
   OnStopServer;
  end;
 end
 else
 begin
  // связываем сообщение с новым сокетом
  if WSAAsyncSelect(ClientSocket, Handle, WM_SOCKETMESSAGE,
   FD_READ or FD_WRITE or FD_CLOSE) = SOCKET_ERROR then
  begin
   MessageDlg('Ошибка при установке асинхронного режима ' +
    'подключившегося сокета:'#13#10 +
    GetErrorString, mtError, [mbOK], 0);
   closesocket(ClientSocket);
   Exit;
  end;
  // Создаем запись для нового подключения и заполняем ее
  New(NewConnection);
  NewConnection.ClientSocket := ClientSocket;
  NewConnection.ClientAddr := Format('%u.%u.%u.%u.%u', [
   Ord(ClientAddr.sin_addr.S_un_b.s_b1),
   Ord(ClientAddr.sin_addr.S_un_b.s_b2),
   Ord(ClientAddr.sin_addr.S_un_b.s_b3),
   Ord(ClientAddr.sin_addr.S_un_b.s_b4),
   ntohs(ClientAddr.sin_port)]);
  NewConnection.Phase := tpReceiveLength;
  NewConnection.Offset := 0;
  NewConnection.BytesLeft := SizeOf(Integer);
  NewConnection.SendRead := False;
  // Добавляем запись нового соединения в список
  FConnections.Add(NewConnection);
  AddMessageToLog('Зафиксировано подключение с адреса ' +
   NewConnection.ClientAddr);
 end;
end;
Для каждого подключившегося клиента создается запись типа TConnection, указатель на которую добавляется в список FConnections — здесь полная аналогия с сервером на неблокирующих сокетах. Отличие заключается в том, что в типе TConnection по сравнению с тем сервером (см. листинг 2.31) добавилось поле SendRead логического типа. Оно равно True, если возникло событие FD_READ в то время, как сервер находится на этапе отправки данных.
Каждый сокет, созданный функцией accept, связывается с сообщением WM_SOCKETMESSAGE. Обработчик этого сообщения приведен в листинге 2.54. 
Листинг 2.54. Обработчик сообщения WM_SOCKETMESSAGE
// Метод GetConnectionBySocket находит в списке FConnections
// запись, соответствующую данному сокету
function TServerForm.GetConnectionBySocket(S: TSocket): PConnection;
var
 I: Integer;
begin
 for I := 0 to FConnections.Count - 1 do
  if PConnection(FConnections[I]).ClientSocket = S then
  begin
   Result := FConnections[I];
   Exit;
  end;
 Result := nil;
end;

 

procedure TServerForm.WMSocketMessage(var Msg: TWMSocketMessage);
var
 Connection: PConnection;
 Res: Integer;
 // Вспомогательная процедура, освобождающая ресурсы, связанные
 // с клиентом и удаляющая запись подключения из списка
 procedure RemoveConnection;
 begin
  closesocket(Connection.ClientSocket);
  FConnections.Remove(Connection);
  Dispose(Connection);
 end;
begin
 // Ищем соединение по сокету
 Connection := GetConnectionBySocket(Msg.Socket);
 if Connection = nil then
 begin
  AddMessageToLog(
   'Внутренняя ошибка сервера — не найдено соединение для сокета');
  Exit;
 end;
 // Проверяем, были ли ошибки при взаимодействии
 if Msg.SockError <> 0 then
 begin
  AddMessageToLog('Ошибка при взаимодействии с клиентом ' +
   Connection.ClientAddr + ': ' + GetErrorString(Msg.SockError));
  RemoveConnection;
  Exit;
 end;
 // Анализируем, какое событие произошло
 case Msg.SockEvent of
 FD_READ: begin
  // Проверяем, на каком этапе находится взаимодействие с клиентом.
  if Connection.Phase = tpReceiveLength then
  begin
   // Этап получения от клиента длины строки. При выполнении этого
   // этапа сервер получает от клиента длину строки и размещает ее
   // в поле Connection.MsgSize. Здесь приходится учитывать, что
   // теоретически даже такая маленькая (4 байта) посылка может
   // быть разбита на несколько пакетов, поэтому за один раз этот
   // этап не будет завершен, и второй раз его придется
   // продолжать, загружая оставшиеся байты. Connection.Offset -
   // количество уже прочитанных на данном этапе байтов -
   // одновременно является смещением, начиная с которого
   // заполняется буфер.
   Res := recv(Connection.ClientSocket,
    (PChar((PConnection.MsgSize + Connection.Offset)^, Connection.BytesLeft, 0);
   if Res > 0 then
   begin
    // Если Res > 0, это означает, что получено Res байтов.
    // Соответственно, увеличиваем на Res количество прочитанных
    // на данном этапе байтов и на такую же величину уменьшаем
    // количество оставшихся.
    Inc(Connection.Offset, Res);
    Dec(Connection.BytesLeft, Res);
    // Если количество оставшихся байтов равно нулю, нужно
    // переходить к следующему этапу.
    if Connection.BytesLeft = 0 then
    begin
     // Проверяем корректность принятой длины строки
     if Connection.MsgSize <= 0 then
     begin
      AddMessageToLog('Неверная длина строки, от клиента ' +
       Connection.ClientAddr + ': ' + IntToStr(Connection.MsgSize));
      RemoveConnection;
      Exit;
     end;
     // Следующий этап - это чтение самой строки
     Connection.Phase := tpReceiveString;
     // Пока на этом этапе не прочитано ни одного байта
     Connection.Offset := 0;
     // Осталось прочитать Connection.MsgSize байтов
     Connection.BytesLeft := Connection.MsgSize;
     // Сразу выделяем память под строку
     SetLength(Connection.Msg, Connection.MsgSize);
    end;
   end
   elsе if Res = 0 then
   begin
    AddMessageToLog('Клиент ' + Connection.ClientAddr +
     ' закрыл соединение');
    RemoveConnection;
    Exit;
   end
   else
    // Ошибку WSAEWOULDBLOCK игнорируем, т.к. она говорит
    // только о том, что входной буфер сокета пуст, но в целом
    // все в порядке - такое вполне возможно при ложных
    // срабатываниях сообщения
    if WSAGetLastError <> WSAEWOULDBLOCK then
    begin
     AddMessageToLog('Ошибка при получении данных от клиента ' +
      Connection.ClientAddr + ': ' + GetErrorString);
     RemoveConnection;
     Exit;
    end;
  end
  else if Connection.Phase = tpReceiveString then
  begin
   // Следующий этап - чтение строки. Он практически не отличается
   // по реализации от этапа чтения длины строки, за исключением
   // того, что теперь буфером, куда помещаются полученные от
   // клиента данные, служит не Connection.MsgSize,
   // a Connection.Msg.
   Res :=
    recv(Connection.ClientSocket, Connection.Msg(Connection.Offset + 1),
     Connection.BytesLeft, 0);
   if Res > 0 then
   begin
    Inc(Connection.Offset, Res);
    Dec(Connection.BytesLeft, Res);
    // Если количество оставшихся байтов равно нулю, можно
    // переходить к следующему этапу.
    if Connection.BytesLeft = 0 then
    begin
     AddMessageToLog('От клиента ' + Connection.ClientAddr +
      ' получена строка: ' + Connection.Msg);
     // Преобразуем строку. В отличие от предыдущих примеров,
     // здесь мы явно добавляем к строке #0. Это связано с тем,
     // что при отправке, которая тоже может быть выполнена не
     // за один раз, мы указываем индекс того символа строки,
     // начиная с которого нужно отправлять данные. И (хотя
     // теоретически вероятность этого очень мала) может
     // возникнуть ситуация, когда за один раз будут отправлены
     // все символы строки, кроме завершающего #0, и тогда при
     // следующей отправке начинать придется с него. Если мы
     // будем использовать тот #0, который добавляется к концу
     // строки автоматически, то в этом случае индекс выйдет за
     // пределы диапазона. Поэтому мы вручную добавляем ещё один
     // #0 к строке, чтобы он стал законной ее частью.
     Connection.Msg :=
      AnsiUpperCase(StringReplace(Connection.Msg, #0, '#0', [rfReplaceAll])) +
      '(AsyncSelect server)'#0;
     // Следующий этап - отправка строки клиенту
     Connection.Phase := tpSendString;
     // Отправлено на этом этапе 0 байт
     Connection.Offset := 0;
     // Осталось отправить Length(Connection.Msg) байтов.
     // Единицу к длине строки, в отличие от предыдущих
     // примеров, не добавляем, т.к. там эта единица нужна была
     // для того, чтобы учесть добавляемый к строке
     // автоматически символ #0. Здесь мы еще один #0 добавили
     // к строке явно, поэтому он уже учтен в функции Length.
     Connection.BytesLeft := Length(Connection.Msg);
     // Ставим в очередь сообщение с событием FW_WRITE.
     // Его получение заставит сервер отправить данные
     PostMessage(Handle, WM_SOCKETMESSAGE, Msg.Socket, FD_WRITE);
    end;
   end
else if Res = 0 then
   begin
    AddMessageToLog('Клиент ' + Connection.ClientAddr +
     ' закрыл соединение');
    RemoveConnection;
    Exit;
   end
   elsе
    // Как обычно, "ошибку" WSAEWOULDBLOCK просто игнорируем
    if WSAGetLastError <> WSAEWOULDBLOCK then
    begin
     AddMessageToLog('Ошибка при получении данных от клиента ', +
      Connection.ClientAddr + ': ' + GetErrorString);
     RemoveConnection;
     Exit;
    end;
   end
   else if Connection.Phase = tpSendString then
    // Если сервер находится на этапе отправки данных,
    // а событие FD_READ все же произошло, отмечаем это
    Connection.SendRead := True;
 end;
 FD_WRITE: begin
  if Connection.Phase = tpSendString then
  begin
   // При наступлении события FD_WRITE проверяем, находится ли
   // сервер на этапе отправки данных, и если да, отправляем их
   Res :=
    send(Connection.ClientSocket, Connection.Msg[Connection.Offset + 1],
    Connection.BytesLeft, 0);
   if Res > 0 then
   begin
    Inc(Connection.Offset, Res);
    Dec(Connection.BytesLeft, Res);
    // Если Connections. BytesLeft = 0, значит, строка отправлена
    // полностью.
    if Connection.BytesLeft = 0 then
    begin
     AddMessageToLog('Клиенту ' + Connection.ClientAddr +
      ' отправлена строка: ' + Connection.Msg);
     // Очищаем строку, просто чтобы сэкономить память
     Connection.Msg := '';
     // Следующий этап - снова получение длины строки от клиента
     Connection.Phase := tpReceiveLength;
     // Получено - 0 байт
     Connection.Offset := 0;
     // Осталось прочитать столько, сколько занимает целое число
     Connection.BytesLeft := SizeOf(Integer);
     // Если были промежуточные события FD_READ, вызываем их
     // снова искусственно
     it Connection.SendRead then
     begin
      PostMessage(Handle, WM_SOCKETMESSAGE, Msg.Socket, FD_READ);
      Connection.SendRead := False;
     end;
    end;
   end
   else if WSAGetLastError <> WSAEWOULDBLOCK then
   begin
    AddMessageToLog('Ошибка при отправке данных клиенту ' +
     Connection.ClientAddr + ': ' + GetErrorString);
    RemoveConnection;
    Exit;
   end;
  end;
 end;
 FD_CLOSE: begin
  // Клиент вызвал функцию shutdown. Закрываем соединение.
  AddMessageToLog('Клиент ' + Connection.ClientAddr +
   ' закрыл соединение');
  shutdown(Connection.ClientSocket, SD_BOTH);
  RemoveConnection;
 end
 else
 begin
  AddMessageToLog('Неверное событие при обмене с клиентом ' +
   Connection.ClientAddr);
  RemoveConnection;
 end;
 end;
end;
В этом примере можно найти много общего с кодом из листинга 2.32 — получение и отправка данных в зависимости от этапа выполняется практически одинаково, различаются только условия, при которых эти участки кода выполняются. Обратите внимание, что теперь проверка того, какой этап чтения выполняется, сделана взаимоисключающей, т.е. при обработке одного сообщения не может быть прочитана и длина строки, и сама строка. Это сделано, чтобы убрать ложные срабатывания. Рассмотрим два возможных варианта. Первый вариант — когда во входном буфере сокета оказывается сразу длина и строка (или ее часть). После того как будет прочитана длина, сообщение WM_SOCKETMESSAGE с параметром FD_READ вновь будет помещено в очередь, поскольку функция recv помещает это сообщение в очередь, если после ее вызова во входном буфере сокета остались данные. Если мы немедленно перейдем ко второму этапу, то прочитаем из буфера сокета все оставшиеся там данные, но сообщение в очереди все равно останется, что даст нам ложное срабатывание, когда петля сообщений извлечет и диспетчеризует это сообщение. Таким образом, выполнение сразу двух этапов при обработке одного сообщения не даст выигрыша в производительности, т.к. все равно придется извлекать и обрабатывать два сообщения.
Второй вариант — когда на момент обработки события FD_READ во входном буфере находится только длина строки. В этом случае функция recv не будет помещать в очередь второе сообщение WM_SOCKETMESSAGE, т.к. данных в буфере после ее выполнения не останется, но и попытка выполнить этап чтения строки окажется бесполезной работой, т.к. строка еще не получена. В любом случае этап чтения строки будет выполнен только при обработке следующего сообщения WM_SOCKETMESSAGE, когда от клиента будут получены новые данные.
Получается, что при обоих вариантах попытка выполнить за один раз сразу два этапа не дает никаких преимуществ в быстродействии, но зато повышает вероятность ложных срабатываний события 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;
Функция уничтожает событие и освобождает связанные с ним ресурсы. Дескриптор, переданный в качестве параметра, становится недействительным. Для ожидания взведения событий служит функция WSAWaitForMultiрleEvents (листинг 2.58).
Листинг 2.58. Функция WSAWaitForMultipleEvents
// ***** Описание на C++ *****
DWORD WSAWaitForMultipleEvents(DWORD cEvents, const WSAEVENT FAR *lphEvents, BOOL fWaitAll, WORD dwTimeout, BOOL fAlertable);

 

// ***** Описание на Delphi *****
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 нужен при перекрытом вводе-выводе: мы рассмотрим его позже в разд. 2.2.9. Если перекрытый ввод-вывод не используется, 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). Смысл этого результата и условия, при которых он может быть возвращен, мы рассмотрим в разд. 2.2.9.
Функции, которые мы рассматривали до сих пор, являются аналогами системных функций для стандартных событий. Теперь мы переходим к рассмотрению тех функций, которые отличают сокетные события от стандартных. Главная из них — WSAEventSelect, позволяющая привязать события, создаваемые с помощью WSACreateEvent, к тем событиям, которые происходят на сокете. Прототип этой функции приведен в листинге 2.59.
Листинг 2.59. Функция WSAEventSelect
// ***** Описание на C++ *****
int WSAEventSelect(SOCKET s, WSAEVENT hEventObject, long lNetworkEvents);

 

// ***** описание на Delphi *****
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 разных сокетов можно привязать к одному сокетному событию, но этой возможностью обычно не пользуются, т.к. в WinSock2 отсутствуют средства, позволяющие определить, событие на каком из сокетов привело к взведению сокетного события. Поэтому приходится для каждого сокета создавать отдельное событие.
Как и в случае с 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, прототип которой приведен в листинге 2.60.
Показать оглавление

Комментариев: 0

Оставить комментарий