А. Григорьев - О чём не пишут в книгах по Delphi
Функция возвращает ноль в случае успешного выполнения операции и 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. Последовательный вызов функции WSAAsyncSelectWSAAsyncSelect(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, потому что остальные ведут себя в этом отношении аналогично).