Листинг 2.60. Функция WSAEnumNetworkEvents
// ***** Описание на C++ *****
int WSAEnumNetworkEvents(SOCKET s, WSAEVENT hEventObject, LPWSANETWORKEVENTS lpNetworkEvents);
// ***** Описание на Delphi *****
function WSAEnumNetworkEvents(S: TSocket; hEventObject: TWSAEvent; var NetworkEvents: TWSANetworkEvents): Integer;
Функция WSAEnumNetworkEvents через параметр NetworkEvents возвращает информацию о том, какие события произошли на сокете S с момента последнего вызова этой функции для данного сокета (или с момента запуска программы, если функция вызывается в первый раз). Параметр hEventObject необязательный, он определяет сокетное событие, которое нужно сбросить. Использование этого параметра позволяет обойтись без явного вызова функции WSAResetEvent для сброса события. Как и большинство функций WinSock, функция WSAEnumNetworkEvents возвращает ноль в случае успеха и ненулевое значение при возникновении ошибки.
Запись TWSANetworkEvents содержит информацию о произошедших событиях об ошибках (листинг 2.61).
Листинг 2.61. Тип TWSANetworkEvents
// ***** Описание на C++ *****
typedef struct _WSANETWORKEVENTS {
long lNetworkEvents;
int iErrorCode[FD_MAX_EVENTS];
} WSANETWORKEVENTS, *LPWSANETWORKEVENTS;
// ***** Описание на Delphi *****
TWSANetworkEvents = packed record
lNetworkEvents: LongInt;
iErrorCode: array[0..FD_MAX_EVENTS - 1] of Integer;
end;
Константа FD_MAX_EVENTS определяет количество разных типов событий и в данной реализации равна 10.
Значения констант FD_XXX представляют собой степени двойки, поэтому их можно объединять операцией арифметического ИЛИ без потери информации. Поле lNetworkEvents является таким объединением всех констант, задающих события, которые происходили на сокете. Другими словами, если результат операции (lNetworkEvents and FD_XXX) не равен нулю, значит, событие FD_XXX происходило на сокете.
Массив iErrorCode содержит информацию об ошибках, которыми сопровождались события FD_XXX. Для каждого события FD_XXX определена соответствующая константа FD_XXX_BIT (т.е. константы FD_READ_BIT, FD_WRITE_BIT и т.д.). Элемент массива с индексом FD_XXX_BIT содержит информацию об ошибке, связанной с событием FD_XXX. Если операция прошла успешно, этот элемент содержит ноль, в противном случае — код ошибки, которую в аналогичной ситуации вернула бы функция WSAGetLastError после выполнения соответствующей операции на синхронном сокете.
Таким образом, программа, использующая асинхронный режим, основанный на событиях, должна выполнить следующие действия. Во-первых, создать сокет и установить соединение. Во-вторых, привязать события FD_XXX к сокетному событию. В-третьих, организовать цикл, начинающийся с вызова WSAWaitForMultipleEvents, в котором с помощью WSAEnumNetworkEvents определять, какое событие произошло, и обрабатывать его. При возникновении ошибки на сокете цикл должен завершаться.
Сокетные события могут взводиться не только в результате событий на сокете, но и вручную, с помощью функции WSASetEvent. Это дает нити, вызвавшей функцию WSAWaitForMultipleEvents, возможность выходить из состояния ожидания не только при возникновении событий на сокете, но и по сигналам от других нитей. Типичная область применения этой возможности — для тех случаев, когда программа может как отвечать на запросы от удаленного партнера, так и отправлять ему что-то по собственной инициативе. В этом случае могут использоваться два сокетных события: одно связывается с событием FD_READ для оповещения о поступлении данных, а второе не связывается ни с одним из событий FD_XXX, а устанавливается другой нитью тогда, когда необходимо отправить сообщение. Нить, работающая с сокетом, ожидает взведения одного из этих событий и в зависимости от того, какое из них взведено, читает или отправляет данные.
В листинге 2.62 приведен пример кода такой нити. Она задействует три сокетных события: одно для уведомления о событиях на сокете, второе — для уведомления о необходимости отправить данные, третье — для уведомления о необходимости завершиться. В данном примере мы предполагаем, что, во-первых, сокет создан и подключен до создания нити и передается ей в качестве параметра, а во-вторых, три сокетных события хранятся в глобальном массиве SockEvents: array[0..2] of TWSAEvent, причем нулевой элемент этого массива содержит событие, связываемое с событиями FD_XXX, первый элемент — событие отправки данных, второй — событие завершения нити. Прототип функции, образующей нить, совместим с функцией BeginThread из модуля SysUtils.
Листинг 2.62. Схема нити, использующей события асинхронного сокета
function ProcessSockEvents(Parameter: Pointer): Integer;
var
S: TSocket;
NetworkEvents: TWSANetworkEvents;
begin
// Так как типы TSocket и Pointer занимают по 4 байта, такое
// приведение типов вполне возможно, хотя и некрасиво
S := TSocket(Parameter);
// Связываем событие SockEvents[0] с FD_READ и FD_CLOSE
WSAEventSelect(S, SockEvents[0], FD_READ or FD_CLOSE);
while True do
begin
case WSAWaitForMultipleEvents(3, @SockEvents[0], True, WSA_INFINITE, False) of
WSA_WAIT_EVENT_0: begin
WSAEnumNetworkEvents(S, SockEvents[0], NetworkEvents);
if NetworkEvents.lNetworkEvents and FD_READ > 0 then
if NetworkEvents.iErrorCode[FD_READ_BIT] = 0 then
begin
// Пришли данные, которые нужно прочитать
end
else
begin
// произошла ошибка. Нужно сообщить о ней и завершить нить
closesocket(3);
Exit;
end;
if NetworkEvents.lNetworkEvents and FD_CLOSE > 0 then
begin
// Связь разорвана
if NetworkEvents.iErrorCode[FD_CLOSE_BIT] = 0 then begin
// Связь закрыта корректно
end
else
begin
// Связь разорвана в результате сбоя сети
end;
// В любом случае нужно закрыть сокет и завершить нить
closesocket(S);
Exit;
end;
end;
WSA_WAIT_EVENT_0 + 1: begin
// Получен сигнал о необходимости отправить данные
// Здесь должен быть код отправки данных
// После отправки событие нужно сбросить вручную
ResetEvent(SockEvents[1]);
end;
WSA_WAIT_EVENT_0 + 2: begin
// Получен сигнал о необходимости завершения работы нити
closesocket;
ResetEvents(SockEvents[2]);
Exit;
end
end;
end;
end;
Как и во всех предыдущих примерах, здесь для краткости не проверяются результаты, возвращаемые функциями и не отлавливаются возникающие ошибки. Кроме того, отсутствует процедура завершения связи с вызовом shutdown.
Данный пример может рассматриваться как фрагмент кода простого сервера. В отдельной нити такого сервера выполняется цикл, состоящий из вызова accept и создания новой нити для обслуживания полученного таким образом сокета. Затем другие нити при необходимости могут давать таким нитям команды (необходимо только предусмотреть для каждой нити, обслуживающей сокет, свою копию массива SockEvents). Благодаря этому каждый клиент будет обслуживаться независимо.
К недостаткам такого сервера следует отнести его низкую устойчивость против DoS-атак, при которых к серверу подключается очень большое число клиентов. Если сервер будет создавать отдельную нить для обслуживания каждого подключения, количество нитей очень быстро станет слишком большим, и вся система окажется неработоспособной, т.к. большая часть процессорного времени будет тратиться на переключение между нитями. Более защищенным является вариант, при котором сервер заранее создает некоторое разумное количество нитей (пул нитей) и обработку запроса или выполнение команды поручает любой свободной нити из этого пула. Если ни одной свободной нити в пуле нет, задание ставится в очередь. По мере освобождения нитей задания извлекаются из очереди и выполняются. При DoS-атаках такой сервер также не справляется с поступающими заданиями, но это не приводит к краху всей системы. Но сервер с пулом нитей реализуется сложнее (обычно — через порты завершения, которые мы здесь не рассматриваем). Тем не менее простой для реализации сервер без пула нитей тоже может оказаться полезным, если вероятность DoS-атак низка (например, в изолированных технологических подсетях).
Приведенный пример может рассматриваться также как заготовка для клиента. В этом случае целесообразнее передавать в функцию ProcessSockEvents не готовый сокет, а только адрес сервера, к которому необходимо подключиться. Создание сокета и установление связи с сервером при этом выполняет сама нить перед началом цикла ожидания событий. Такой подход очень удобен для независимой работы с несколькими однотипными серверами.
2.2.8. Пример использования сокетов с событиями
К достоинствам асинхронного режима, основанного на сообщениях, относится то, что нить, обслуживающая сокет, может выходить из состояния ожидания не только при получении данных сокетом, но и по иным сигналам. Протокол обмена который мы до сих пор использовали, не позволяет продемонстрировать это достоинство в полном объеме. Поэтому, прежде чем создавать пример, мы несколько изменим протокол. Формат пакетов оставим прежним, изменим условия, при которых эти пакеты могут быть посланы. Теперь клиент имеет право посылать пакеты, не дожидаясь от сервера ответа на предыдущий пакет, а сервер имеет право посылать пакеты клиенту не только в ответ на его запросы, но и по собственной инициативе. В нашей реализации он будет посылать клиентам строку с текущим временем.
Сервер на асинхронных событиях (пример EventSelectServer на компакт-диске) имеет много общего с рассмотренным ранее многонитевым сервером (пример MultithreadedServer, см. разд. 2.1.12). В нем также есть нить для обработки подключений клиентов и по одной нити на каждого клиента, а главная нить только создает слушающий сокет и запускает обслуживающую его нить.
Еще одним важным отличием нашего сервера от всех предыдущих примеров серверов станет то, что пользователь сможет его остановить в любой момент. Подобную функциональность было бы несложно добавить и к таким серверам, как SelectServer, NonBlockingServer и AsyncSelectServer, которые работают в одной нити. Но остановить нити в многонитевом сервере можно было только одним способом: уничтожив сокеты из главной нити — в этом случае все работающие с этими сокетами нити завершились бы с ошибками. Очевидно, что это порочный подход, не позволяющий корректно завершить работу с клиентами. Режим с использованием событий позволяет предусмотреть реакцию нити на внешний сигнал об отключении. Отключаться сервер будет по нажатию кнопки Остановить.
В листинге 2.63 приведен код нити, взаимодействующей с клиентом (код методов LogMessage и DoLogMessage опущен, т.к. он идентичен приведенному в листингах 2.20 и 2.7 соответственно).
Листинг 2.63. Нить, взаимодействующая с клиентами
unit ClientThread;
{
Нить, обслуживающая одного клиента.
Выполняет цикл, выход из которого возможен по внешнему сигналу или при возникновении ошибки на сокете. Умеет отправлять клиенту сообщения по внешнему сигналу.
}
interface
uses
Windows, Classes, WinSock, Winsock2_Events, ShutdownConst, SysUtils, SyncObjs;
type
TClientThread = class(TThread)
private
// Сообщение, которое нужно добавить в лог,
// хранится в отдельном поле, т.к. метод, вызывающийся через
// Synchronize, не может иметь параметров.
FMessage: string;
// Префикс для всех сообщений лога, связанных с данным клиентом
FHeader: string;
// Сокет для взаимодействия с клиентом
FSocket: TSocket;
// События нити
// FEvents[0] используется для остановки нити
// FEvents[1] используется для отправки сообщения
// FEvents[2] связывается с событиями FD_READ, FD_WRITE и FD_CLOSE
FEvents; array[0..2] of TWSAEvent;
// Критическая секция для доступа к буферу исходящих
FSendBufSection: TCriticalSection;
// Буфер исходящих
FSendBuf: string;
// Вспомогательный метод для вызова через Synchronize
procedure DoLogMessage;
// Функция, проверяющая, завершила ли нить работу
function GetFinished: Boolean;
protected
procedure Execute; override;
// Вывод сообщения в лог главной формы
procedure LogMessage(сonst Msg: string);
// Отправка клиенту данных из буфера исходящих
function DoSendBuf: Boolean;
public
constructor Create(ClientSocket: TSocket; const ClientAddr: TSockAddr);
destructor Destroy; override;
// Добавление строки в буфер исходящих
procedure SendString(const S: string);
// Остановка нити извне
procedure StopThread;
property Finished: Boolean read GetFinished;
end;
ESocketError = class(Exception);
implementation
uses
MainServerUnit;
{ TClientThread }
// Сокет для взаимодействия с клиентом создается в главной нити,
// а сюда передается через параметр конструктора. Для формирования
// заголовка сюда же передается адрес подключившегося клиента
constructor TClientThread.Create(ClientSocket: TSocket; const ClientAddr: TSockAddr);
begin
FSocket := ClientSocket;
// заголовок содержит адрес и номер порта клиента.
// Этот заголовок будет добавляться ко всем сообщениям в лог
// от данного клиента.
FHeader :=
'Сообщение от клиента ' + inet_ntoa(ClientAddr.sin_addr) +
': ' + IntToStr(ntohs(ClientAddr.sin_port)) + ': ';
// Создаем события и привязываем первое из них к сокету
FEvents[0] := WSACreateEvent;
if FEvents[0] = WSA_INVALID_EVENT then
raise ESocketError.Create(
FHeader + 'Ошибка при создании события: ' + GetErrorString);
FEvents[1] := WSACreateEvent;
if FEvents[1] = WSA_INVALID_EVENT then
raise ESocketError.Create(
FHeader + 'Ошибка при создании события: ' + GetErrorString);
FEvents[2] := WSACreateEvent;
if FEvents[2] = WSA_INVALID_EVENT then raise
ESocketError.Create(
FHeader + 'Ошибка при создании события: ' + GetErrorString);
if WSAEventSelect(FSocket, FEvents[2], FD_READ or FD_WRITE or FD_CLOSE) =
SOCKET_ERROR then
raise ESocketError.Create(
FHeader + 'Ошибка при привязывании сокета к событию: ' + GetErrorString);
FSendBufSection := TCriticalSection.Create;
// Объект этой нити не должен удаляться сам
FreeOnTerminate := False;
inherited Create(False);
end;
destructor TClientThread.Destroy;
begin
FSendBufSection.Free;
WSACloseEvent(FEvents[0]);
WSACloseEvent(FEvents[1]);
WSACloseEvent(FEvents[2]);
inherited;
end;
// Функция добавляет строку в буфер для отправки
procedure TClientThread.SendString(const S: string);
begin
FSendBufSection.Enter;
try
FSendBuf := FSendBuf + S + #0;
finally
FSendBufSection.Leave;
end;
LogMessage('Сообщение "' + S + '" поставлено в очередь для отправки');
// Взводим событие, которое говорит, что нужно отправлять данные
WSASetEvent(FEvents[1]);
end;
// Отправка всех данных, накопленных в буфере
// Функция возвращает False, если произошла ошибка,
// и True, если все в порядке
function TClientThread.DoSendBuf: Boolean;
var
SendRes: Integer;
begin
FSendBufSection.Enter;
try
// Если отправлять нечего, выходим
if FSendBuf = '' then
begin
Result := True;
Exit;
end;
// Пытаемся отправить все, что есть в буфере
SendRes := send(FSocket, FSendBuf[1], Length(FSendBuf), 0);
if SendRes > 0 then
begin
// Удаляем из буфера ту часть, которая отправилась клиенту
Delete(FSendBuf, 1, SendRes);
Result := True;
end
else
begin
Result := WSAGetLastError = WSAEWOULDBLOCK;
if not Result then
LogMessage('Ошибка при отправке данных: ' + GetErrorString);
end;
finally
FSendBufSection.Leave;
end;
end;
procedure TClientThread.Execute;
const
// размер буфера для приема сообщении
RecvBufSize = 4096;
var
// Буфер для приема сообщений
RecvBuf: array[0..RecvBufSize - 1] of Byte;
RecvRes: Integer;
NetEvents: TWSANetworkEvents;
// Полученная строка
Str: string;
// Длина полученной строки
StrLen: Integer;
// Если ReadLength = True, идет чтение длины строки,
// если False - самой строки
ReadLength: Boolean;
// Смещение от начала приемника
Offset: Integer;
// Число байтов, оставшихся при получении длины строки или самой строки
BytesLeft: Integer;
Р: Integer;
I: Integer;
LoopExit: Boolean;
WaitRes: Cardinal;
begin
LogMessage('Соединение установлено');
ReadLength := True;
Offset := 0;
BytesLeft := SizeOf(Integer);
repeat
WaitRes := WSAWaitForMultipleEvents(3, @FEvents, False, WSA_INFINITE, False);
case WaitRes of
WSA_WAIT_EVENT_0: begin
// Закрываем соединение с клиентом и останавливаем нить
LogMessage('Получен сигнал об остановке нити');
shutdown(FSocket, SD_BOTH);
Break;
end;
WSA_WAIT_EVENT_0 + 1:
begin
// Сбрасываем событие и отправляем данные
WSAResetEvent(FEvents[1]);
if not DoSendBuf then Break;
end;
WSA_WAIT_EVENT_0 + 2: begin
// Произошло событие, связанное с сокетом.
// Проверяем, какое именно, и заодно сбрасываем его
if WSAEnumNetworkEvents(FSocket, FEvents[2], NetEvents) = SOCKET_ERROR then
begin
LogMessage('Ошибка при получении списка событий: ' + GetErrorString);
Break;
end;
if NetEvents.lNetworkEvents and FD_READ <> 0 then
begin
if NetEvents.iErrorCode[FD_READ_BIT] <> 0 then
begin
LogMessage('Ошибка в событии FD_READ: ' +
GetErrorString(NetEvents.iErrorCode[FD_READ_BIT]));
Break;
end;
// В буфере сокета есть данные.
// Копируем данные из буфера сокета в свой буфер RecvBuf
RecvRes := recv(FSocket, RecvBuf, SizeOf(RecvBuf), 0);
if RecvRes > 0 then
begin
P := 0;
// Эта переменная нужна потому, что здесь появляется
// вложенный цикл, при возникновении ошибки в котором нужно
// выйти и из внешнего цикла тоже. Так как в Delphi нет
// конструкции типа Break(2) в Аде, приходится прибегать
// к таким способам: если нужен выход из внешнего цикла,
// во внутреннем цикле выполняется LoopExit := True,
// а после выполнения внутреннего цикла проверяется
// значение этой переменной и при необходимости выполняется
// выход и из главного цикла.
LoopExit := False;
// В этом цикле мы извлекаем данные из буфера
// и раскидываем их по приёмникам - Str и StrLen.
while Р < RecvRes do
begin
// Определяем, сколько байтов нам хотелось бы скопировать
L := BytesLeft;
// Если в буфере нет такого количества,
// довольствуемся тем, что есть
if Р + L > RecvRes then L := RecvRes - P;
// Копируем в соответствующий приемник
if ReadLength then
Move(RecvBuf[P], (PChar(@StrLen) + Offset)^, L)
else Move(RecvBuf[P], Str(Offset + 1), L);
Dec(BytesLeft, L);
// Если прочитали все, что хотели,
// переходим к следующему
if BytesLeft = 0 then
begin
ReadLength := not ReadLength;
Offset := 0;
// Если закончено чтение строки, нужно вывести ее
if ReadLength then
begin
LogMessage('Получена строка: ' + Str);
BytesLeft := SizeOf(Integer);
// Формируем ответ и записываем его в буфер
Str :=
AnsiUpperCase(StringReplace(Str, #0, '#0',
[rfReplaceAll])) + '(AsyncEvent server)';
SendString(Str);
Str := '';
end
else
begin
if StrLen <= 0 then
begin
LogMessage('Неверная длина строки от клиента: ' +
IntToStr(StrLen));
LoopExit := True;
Break;
end;
BytesLeft := StrLen;
SetLength(Str, StrLen);
end;
end
else Inc(Offset, L);
Inc(P, L);
end;
// Проверяем, был ли аварийный выход из внутреннего цикла,
// и если был, выходим и из внешнего, завершая работу
// с клиентом
if LoopExit then Break;
end
else if RecvRes = 0 then
begin
LogMessage('Клиент закрыл соединение ');
Break;
end
else
begin
if WSAGetLastError <> WSAEWOULDBLOCK then
begin
LogMessage('Ошибка при получении данных от клиента: ' +
GetErrorString);
end;
end;
end;
// Сокет готов к передаче данных
if NetEvents.lNetworkEvents and FD_WRITE <> 0 then
begin
if NetEvents.iErrorCode[FD_WRITE_BIT] <> 0 then
begin
LogMessage('Ошибка в событии FD_WRITE: ' +
GetErrorString(NetEvents.iErrorCode[FD_WRITE_BIT)));
Break;
end;
// Отправляем то, что лежит в буфере
if not DoSendBuf then Break;
end;
if NetEvents.lNetworkEvents and FD_CLOSE <> 0 then
begin
// Клиент закрыл соединение
if NetEvents.iErrorCode[FD_CLOSE_BIT] <> 0 then
begin
LogMessage('Ошибка в событии FD_CLOSE: ' +
GetErrorString(NetEvents.iErrorCode[FD_CLOSE_BIT]));
Break;
end;
LogMessage('Клиент закрыл соединение');
shutdown(FSocket, SD_BOTH);
Break;
end;
end;
WSA_WAIT_FAILED: begin
LogMessage('Ошибка при ожидании сообщения: ' + GetErrorString);
Break;
end;
else begin
LogMessage(
'Внутренняя ошибка сервера — неверный результат ожидания ' +
IntToStr(WaitRes));
Break;
end;
end;
until False;
closesocket(FSocket);
LogMessage('Нить остановлена');
end;
// Функция возвращает True, если нить завершилась
function TClientThread.GetFinished: Boolean;
begin
// Ждем окончания работы нити с нулевым тайм-аутом.
// Если нить завершена, вернется WAIT_OBJECT_0.
// Если еще работает, вернется WAIT_TIMEOUT.
Result := WaitForSingleObject(Handle, 0) = WAIT_OBJECT_0;
end;
// Метод для остановки нити извне.
// Взводим соответствующее событие, а остальное сделаем
// при обработке события
procedure TClientThread.StopThread;
WSASetEvent(FEvents[0]);
end;
Модуль WinSock2_Events, появившийся в списке uses, содержит объявления констант, типов и функций из WinSock 2, которые понадобятся в программе. Модуль ShutdownConst содержит объявления констант для функции shutdown, которые отсутствуют в модуле WinSock Delphi 5 и более ранних версиях — этот модуль нам понадобился, чтобы программу можно было откомпилировать в Delphi 5.
Нить использует три события, дескрипторы которых хранятся в массиве FEvents. Событие FEvents[0] служит для уведомления нити о том, что необходимо завершиться, FEvents[1] — для уведомления о том, что нужно оправить данные, FEvents[2] связывается с событиями на сокете. Такой порядок выбран не случайно. Если взведено несколько событий, функция WSAWaitForMultipleEvents вернет результат, соответствующий событию с самым младшим из взведенных событий индексом. Соответственно, чем ближе к началу массива, тем более высокий приоритет у события. Событие, связанное с сокетом, имеет наинизший приоритет для того, чтобы повысить устойчивость сервера к DoS-атакам. Если приоритет этого события был бы выше, чем события остановки нити, то в случае закидывания сервера огромным количеством сообщений от клиента, событие FD_READ было бы всегда взведено, и сервер все время тратил бы на обработку этого события, игнорируя сигнал об остановке нити. Соответственно, сигнал об остановке должен иметь самый высокий приоритет, чтобы остановке нити ничего не могло помешать. Тем, как отправляются сообщения, сервер управляет сам. поэтому не приходится ожидать проблем, связанных с тратой излишних ресурсов на обработку сигнала отправки. Соответственно, этому событию присваивается приоритет, промежуточный между событием остановки нити и событием сокета.
Так как клиент по новому протоколу перед отправкой сообщения не обязан ждать, пока сервер ответит на предыдущее, возможны ситуации, когда ответ на следующее сообщение сервер должен готовить уже тогда, когда предыдущее еще не отправлено. Кроме того, сервер может отправить сообщение по собственной инициативе, и этот момент тоже может наступить тогда, когда предыдущее сообщение еще не отправлено. Таким образом, мы вынуждены формировать очередь сообщений в том или ином виде. Так как протокол TCP, с одной стороны, может объединять несколько пакетов в один, а с другой, не обязан отправлять отдельную строку за один раз, проще всего не делать очередь из отдельных строк, а заранее объединять их в одном буфере и затем пытаться отправить все содержимое буфера. Таким буфером в нашем случае является поле FSendBuf, метод SendString добавляет строку в этот буфер, a DoSendBuf отправляет данные из этого буфера. Если все данные отправить за один раз не удалось, отправленные данные удаляются из буфера, а оставшиеся будут отправлены при следующем вызове SendBuf. Все операции с буфером FSendBuf выполняются внутри критической секции, т.к. функция SendString может вызываться из других нитей. К каждой строке добавляется символ #0, который, согласно протоколу, является для клиента разделителем строк в потоке.
Сигналом к отправке данных является событие FEvents[1]. Метод SendString, помещая данные в буфер, взводит это событие. Если все содержимое буфера за один раз отправить не удастся, то через некоторое время возникнет событие FD_WRITE, означающее готовность сокета к приему новых данных. Это событие привязано у нас к FEvents[2], поэтому при наступлении FEvents[2] тоже возможна отправка данных.
Для приема данных здесь также используется буфер. Прямой необходимости в этом нет — можно было, как и раньше, помещать данные непосредственно в переменную, хранящую длину строки, а затем и в саму строку. Сделано это в учебных целях, чтобы показать, как можно работать с подобным буфером. Буфер имеет фиксированный размер. Сначала мы читаем из сокета в этот буфер столько, сколько сможем, а потом начинаем разбирать полученное точно так же, как и раньше, копируя данные то в целочисленную, то в строковую переменную. Когда строковая переменная полностью заполняется, строка считается принятой, для пользователя выводится ответ на нее, а в буфер для отправки добавляется ответная строка. Достоинством такого способа является то, что, с одной стороны, за время обработки одного события сервер может прочитать несколько запросов от клиента (если буфер достаточно велик), но, с другой стороны, это не приводит к зацикливанию, если сообщения поступают непрерывно. Другими словами, разработчик здесь сам определяет, какой максимальный объем данных можно получить от сокета за один раз. Иногда это бывает полезно.
Теперь рассмотрим нить, обслуживающую слушающий сокет. Код этой нити приведен в листинге 2.64.
Листинг 2.64. Код нити, обслуживающей слушающий сокет
unit ListenThread;
{
Нить, следящая за подключением клиента к слушающему сокету.
При обнаружении подключения она создает новую нить для работы с подключившимся клиентом, а сама продолжает обслуживать "слушающий" сокет.
}
interface
uses
SysUtils, Classes, WinSock, WinSock2_Events;
type
TListenThread = class(TThread)
private
// Сообщение, которое нужно добавить в лог.
// Хранится в отдельном поле, т.к. метод, вызывающийся
// через Synchronize, не может иметь параметров.
FMessage: string;
// Сокет, находящийся в режиме прослушивания
FServerSocket: TSocket;
// События нити
// FEvents[0] используется для остановки нити
// FEvents[1] связывается с событием FD_ACCEPT
FEvents: array[0..1] of TWSAEvent;
// Список нитей клиентов
FClientThreads: TList;
// Если True, сервер посылает клиенту сообщения
// по собственной инициативе
FServerMsg: Boolean;
// Вспомогательный метод для вызова через Synchronize
procedure DoLogMessage;
protected
procedure Execute; override;
// Вывод сообщения в лог главной формы
procedure LogMessage(const Msg: string);
public
constructor Create(ServerSocket: TSocket; ServerMsg: Boolean);
destructor Destroy; override;
// Вызывается извне для остановки сервера
procedure StopServer;
end;
implementation
uses
MainServerUnit, ClientThread;
{ TListenThread }
// "Слушающий" сокет создается в главной нити,
// а сюда передается через параметр конструктора
constructor TListenThread.Create(ServerSocket: TSocket; ServerMsg: Boolean);
begin
FServerSocket := ServerSocket;
FServerMsg := ServerMsg;
// Создаем события
FEvents[0] := WSACreateEvent;
if FEvents[0] = WSA_INVALID_EVENT then
raise ESocketError.Create(
'Ошибка при создании события для сервера:' + GetErrorString);
FEvents[1] := WSACreateEvent;
if FEvents[1] = WSA_INVALID_EVENT then
raise ESocketError.Create(
'Ошибка при создании события для сервера: ' + GetErrorString);
if WSAEventSelect(FServerSocket, FEvents[1], FD_ACCEPT) = SOCKET_ERROR then
raise ESocketError.Create(
'Ошибка при привязывании серверного сокета к событию: ' + GetErrorString);
FClientThreads := TList.Create;
inherited Create(False);
end;
destructor TListenThread.Destroy;
begin
// Убираем за собой
FClientThreads.Free;
WSACloseEvent(FEvents[0]);
WSACloseEvent(FEvents[1]);
inherited;
end;
procedure TListenThread.Execute;
var
// Сокет, созданный для общения с подключившимся клиентом
ClientSocket: TSocket;
// Адрес подключившегося клиента
ClientAddr: TSockAddr;
ClientAddrLen: Integer;
NetEvents: TWSANetworkEvents;
I: Integer;
WaitRes: Cardinal;
begin
LogMessage('Сервер начал работу');
// Начинаем бесконечный цикл
repeat
// Ожидание события с 15-секундным тайм-аутом
WaitRes :=
WSAWaitForMultipleEvents(2, @FEvents, False, 15000, False);
case WaitRes of
WSA_WAIT_EVENT_0:
// Событие FEvents[0] взведено - это означает, что
// сервер должен остановиться.
begin
LogMessage('Сервер получил сигнал завершения работы');
// Просто выходим из цикла, остальное сделает код после цикла
Break;
end;
WSA_WAIT_EVENT_0 + 1:
// Событие FEvents[1] взведено.
// Это должно означать наступление события FD_ACCEPT.
begin
// Проверяем, почему событие взведено,
// и заодно сбрасываем его
if WSAEnumNetworkEvents(FServerSocket, FEvents[1], NetEvents) = SOCKET_ERROR then
begin
LogMessage('Ошибка при получении списка событий: ' +
GetErrorString);
Break;
end;
// Защита от "тупой" ошибки - проверка того,
// что наступило нужное событие
if NetEvents.lNetworkEvents and FD_ACCEPT = 0 then
begin
LogMessage(
'Внутренняя ошибка сервера - неизвестное событие');
Break;
end;
// Проверка, не было ли ошибок
if NetEvents.iErrorCode[FD_ACCEPT_BIT] <> 0 then
begin
LogMessage('Ошибка при подключении клиента: ' +
GetErrorString(NetEvents.iErrorCode[FD_ACCEPT_BIT]));
Break;
end;
ClientAddrLen := SizeOf(ClientAddr);
// Проверяем наличие подключения
ClientSocket :=
accept(FServerSocket, @ClientAddr, @ClientAddrLen);
if ClientSocket = INVALID_SOCKET then
begin
// Ошибка в функции accept возникает только тогда, когда
// происходит нечто экстраординарное. Продолжать работу
// в этом случае бессмысленно. Единственное возможное
// в нашем случае исключение - ошибка WSAEWOULDBLOCK,
// которая может возникнуть, если срабатывание события
// было ложным, и подключение от клиента отсутствует
if WSAGetLastError <> WSAEWOULDBLOCK then
begin
LogMessage('Ошибка при подключении клиента: ' +
GetErrorString);
Break;
end;
end;
// Создаем новую нить для обслуживания подключившегося клиента
// и передаем ей сокет, созданный для взаимодействия с ним.
// Указатель на нить сохраняем в списке
FClientThreads.Add(
TClientThread.Create(ClientSocket, ClientAddr));
end;
WSA_WAIT_TIMEOUT:
// Ожидание завершено по тайм-ауту
begin
// Проверяем, есть ли клиентские нити, завершившие работу.
// Если есть такие нити, удаляем их из списка
// и освобождаем объекты
for I := FClientThreads.Count -1 downto 0 do
if TClientThread(FClientThreads[I]).Finished then
begin
TClientThread(FClientThreads[I]).Free;
FClientThreads.Delete(I);
end;
// Если разрешены сообщения от сервера, отправляем
// всем клиентам сообщение с текущим временем
if FServerMsg then
for I := 0 to FClientThreads.Count - 1 do
TClientThread(FClientThreads[I]).SendString(
'Время на сервере ' + TimeToStr(Now));
end;
WSA_WAIT_FAILED:
// При ожидании возникла ошибка. Это может означать
// только какой-то серьезный сбой в библиотеке сокетов.
begin
LogMessage('Ошибка при ожидании события сервера: ' +
GetErrorString);
Break;
end;
else
// Неожиданный результат при ожидании
begin
LogMessage(
'Внутренняя ошибка сервера — неожиданный результат ожидания '
+ IntToStr(WaitRes));
Break;
end;
end;
until False;
// Останавливаем и уничтожаем все нити клиентов
for I := 0 to FClientThreads.Count - 1 do
begin
TClientThread(FClientThreads[I]).StopThread;
TClientThread(FClientThreads[I]).WaitFor;
TClientThread(FClientThreads[I]).Free;
end;
closesocket(FServerSocket);
LogMessage('Сервер завершил работу');
Synchronize(ServerForm.OnStopServer);
end;
// Завершение работы сервера. Просто взводим соответствующее
// событие, а остальное делает код в методе Execute.
procedure TListenThread.StopServer;
begin
WSASetEvent(FEvents[0));
end;
end.
Нить TListenThread реализует сразу несколько функций. Во-первых, она обслуживает подключение клиентов и создает нити для их обслуживания. Во-вторых, уничтожает объекты завершившихся нитей. В-третьих, она с определённой периодичностью ставит в очередь на отправку всем клиентам сообщение с текущим временем сервера. И в-четвертых, управляет уничтожением клиентских нитей при завершении работы сервера.
Здесь следует пояснить, почему выбран такой способ управления временем жизни объектов клиентских нитей. Очевидно, что нужно иметь список всех нитей, чтобы обеспечить возможность останавливать их и ставить в очередь сообщения для отправки клиентам (этот список реализован переменной FClientThreads). Если бы объект TClientThread автоматически удалялся при завершении работы нити, в его деструкторе пришлось бы предусмотреть и удаление ссылки на объект из списка, а это значит, что к списку пришлось бы обращаться из разных нитей. Соответственно, потребовалось бы синхронизировать обращение к списку, и здесь мы бы столкнулись с одной неприятной проблемой. Когда нить TListenThread получает команду завершиться, она должна завершить все клиентские нити. Для этого она должна использовать их список для отправки сигнала и ожидания их завершения. И получилась бы взаимная блокировка, потому что нить TListenThread ждала бы завершения клиентских нитей, а они не могли бы завершиться, потому что им требовался бы список, захваченный нитью TListenThread. Избежать этого можно с помощью асинхронных сообщений, но в нашем случае реализация этого механизма затруднительна (хотя и возможна). Для простоты был выбран другой вариант: клиентские нити сами свои объекты не удаляют, а к списку имеет доступ только нить TListenThread, которая время от времени проходит по по списку и удаляет объекты всех завершившихся нитей. В этом случае клиентские нити не используют синхронизацию при завершении, и нить TListenThread может дожидаться их.
Нить TListenThread использует два события: FEvents[0] для получения сигнала о необходимости закрытия и FEvents[1] для получения уведомлений о возникновении события FD_ACCEPT на слушающем сокете (т.е. о подключении клиента). Порядок следования событий к массиве определяется теми же соображениями, что и в случае клиентской нити: сигнал остановки нити должен иметь более высокий приоритет. чтобы в случае DoS-атаки нить могла быть остановлена.
И поиск завершившихся нитей, и отправка сообщений с текущим временем клиентам осуществляется в том случае, если при ожидании события произошёл тайм-аут (который в нашем случае равен 15 c). Подключение клиента — событие достаточно редкое, поэтому такое решение выгладит вполне оправданным. Для тех экзотических случаев, когда клиенты часто подключаются и отключаются, можно предусмотреть еще одно событие у нити TListenThread, при наступлении которого она будет проверять список клиентов. Клиентская нить при своем завершении будет взводить это событие. Что же касается отправки сообщений клиентам, то в обработчик тайм-аута этот код помещён в демонстрационных целях. В реальной программе инициировать отправку сообщений клиентам будет, скорее всего, другой код, например, код главной нити по команде пользователя.
Несмотря на изменение протокола, новый сервер был бы вполне совместим со старым клиентом SimpleClient (см. разд. 2.1.11), если бы не отправлял сообщения по своей инициативе. Действительно, прочие изменения в протоколе разрешают клиенту отправлять новые сообщения до получения ответа сервера, но не обязывают его делать это. В класс TClientThread добавлено логическое поле FServerMsg. Если оно равно False, то сервер не посылает клиентам сообщений по собственной инициативе, т.е. работает в режиме совместимости со старым клиентом. Поле FServerMsg инициализируется в соответствии с параметром, переданным в конструктор, т.е. в соответствии с состоянием галочки Сообщения от сервера, расположенной на главной форме. Если перед запуском сервера она снята, сервер не будет сам посылать сообщения, и старый клиент сможет обмениваться данными с ним.
Запуск сервера практически не отличается от запуска сервера MultithreadedServer (см. листинг 2.19), только теперь объект, созданный конструктором, запоминается классом главной формы, чтобы потом можно было сервер остановить. Остановка осуществляется методом StopServer (листинг 2.65).
Листинг 2.65. Метод StopServer
// Остановка сервера
procedure TServerForm.StopServer;
begin
// Запрещаем кнопку, чтобы пользователь не мог нажать ее
// еще раз, пока сервер не остановится.
BtnStopServer.Enabled := False;
// Ожидаем завершения слушавшей нити. Так как вывод сообщений
// эта нить осуществляет через Synchronize, выполняемый главной
// нитью в петле сообщений, вызов метода WaitFor мог бы привести
// к взаимной блокировке: главная нить ждала бы, когда завершится
// нить TListenThread, а та, в свою очередь - когда главная нить
// выполнит Synchronize. Чтобы этого не происходило, организуется
// ожидание с локальной петлей сообщений.
if Assigned(FListenThread) then
begin
FListenThread.StopServer;
while Assigned(FListenThread) do
begin
Application.ProcessMessages;
Sleep(10);
end;
end;
end;
Данный метод вызывается в обработчике нажатия кнопки Остановить и при завершении приложения. Сервер можно многократно останавливать и запуска вновь, не завершая приложение.
Чтобы увидеть все возможности сервера, потребуется новый клиент. На компакт-диске он называется EventSelectClient, но "EventSelect" в данном случае означает только то, что клиент является парным к серверу EventSelectServer. Сам клиент функцию WSAEventSelect не использует, поскольку она неудобна, когда нужно работать только с одним сокетом. Поэтому клиент работает в асинхронном режиме, основанном на сообщениях, т.е. посредством функции WSAAsyncSelect.
Клиент может получать от сервера сообщения двух видов: те. которые сервер посылает в ответ на запросы клиента, и те, которые он посылает по собственной инициативе. Но различить эти сообщения клиент не может: например, если клиент отправляет запрос серверу, а потом получает от него сообщение, он не может определить, то ли это сервер ответил на его запрос, то ли именно в этот момент сервер сам отправил клиенту свое сообщение. Соответственно, сообщения обоих типов читает один и тот же код.
Примечание
В принципе, протокол мог бы быть определен таким образом, что ответы на запросы клиента и сообщения, посылаемые сервером по собственной инициативе, имели бы разный формат, по которому их можно было бы различить и читать по-разному. Но даже при этом форматы нельзя различить, пока сообщение не будет прочитано хотя бы частично, так что начало чтения будет выполняться единообразно в любом случае.
Подключение клиента к серверу выполняется точно так же, как в листинге 2.16, за исключением того, что после выполнения функции connect сокет переводится в асинхронный режим, и его события FD_READ и FD_CLOSE связываются с сообщением WM_SOCKETMESSAGE. Обработчик этого сообщения приведен в листинге 2.66.
Листинг 2.66. Получение данных клиентом
procedure TESClientForm.WMSocketMessage(var Msg: TWMSocketMessage);
const
// Размер буфера для получения данных
RecvBufSize = 4096;
var
// Буфер для получения данных
RecvBuf: array[0..RecvBufSize - 1] of Byte;
RecvRes: Integer;
P: Integer;
begin
// Защита от "тупой" ошибки
if Msg.Socket <> FSocket then
begin
MessageDlg('Внутренняя ошибка программы — неверный сокет',
mtError, [mbOK], 0);
Exit;
end;
if Msg.SockError <> 0 then
begin
MessageDlg('Ошибка при взаимодействии с сервером'#13#10 +
GetErrorString(Msg.SockError), mtError, [mbOK], 0);
OnDisconnect;
Exit;
end;
case Msg.SockEvent of
FD_READ:
// Получено сообщение от сервера
begin
// Читаем столько, сколько можем
RecvRes := recv(FSocket, RecvBuf, RecvBufSize, 0);
if RecvRes > 0 then
begin
// Увеличиваем строку на размер прочитанных данных
P := Length(FRecvStr);
SetLength(FRecvStr, P + RecvRes);
// Копируем в строку полученные данные
Move(RecvBuf, FRecvStr[Р + 1], RecvRes);
// В строке может оказаться несколько строк от сервера,
// причем последняя может прийти не целиком.
// Ищем в строке символы #0, которые, согласно протоколу,
// являются разделителями строк.
P := Pos(#0, FRecvStr));
while P > 0 do
begin
AddMessageToRecvMemo('Сообщение от сервера: ' +
Copy(FRecvStr, 1, P - 1));
// Удаляем из строкового буфера выведенную строку
Delete(FRecvStr, 1, P);
P := Pos(#0, FRecvStr);
end;
end
else if RecvRes = 0 then
begin
MessageDlg('Сервер закрыл соединение'#13#10 +
GetErrorString, mtError, [mbOK], 0);
OnDisconnect;
end
else
begin
if WSAGetLastError <> WSAEWOULDBLOCK then
begin
MessageDlg('Ошибка при получении данных от клиента'#13#10 +
GetErrorString, mtError, [mbOK], 0);
OnDisconnect;
end;
end;
end;
FD_CLOSE: begin
MessageDlg('Сервер закрыл соединение', mtError, [mbOK], 0);
shutdown(FSocket, SD_BOTH);
OnDisconnect;
end;
else begin
MessageDlg('Внутренняя ошибка программы — неизвестное событие ' +
IntToStr(Msg.SockEvent), mtError, [mbOK], 0);
OnDisconnect;
end;
end;
end;
Здесь мы используем новый способ чтения данных. Он во многом похож на тот, который применен в сервере. Функция recv вызывается один раз за один вызов обработчика значений и передаст данные в буфер фиксированного размера RecvBuf. Затем в буфере ищутся границы отдельных строк (символы #0), строки, полученные целиком, выводятся. Если строка получена частично (а такое может случиться не только из-за того, что она передана по частям, но и из-за того, что в буфере просто не хватило место для приема ее целиком), её начало следует сохранить в отдельном буфере, чтобы добавить к тому, что будет прочитано при следующем событии FD_READ. Этот буфер реализуется полем FRecvStr типа string. После чтения к содержимому этой строки добавляется содержимое буфера RecvBuf, а затем из строки выделяются все подстроки, заканчивающиеся на #0. То, что остается в строке FRecvStr после этого, — это начало строки, прочитанной частично. Оно будет учтено при обработке следующего события FD_READ.
Примечание
Описанный алгоритм разбора буфера прост, но неэффективен с точки зрения нагрузки на процессор и использования динамической памяти, особенно в тех случаях, когда в буфере RecvBuf оказывается сразу несколько строк. Это связано с тем, что при добавлении содержимого RecvBuf к FRecvStr и последующем поочередном удалении строк из FRecvStr происходит многократное перераспределение памяти, выделенной для строки. Алгоритм можно оптимизировать: все строки, которые поместились в RecvBuf целиком, выделять непосредственно из этого буфера, не помещая в FRecvStr, а помещать туда только то, что действительно нужно сохранить между обработкой разных событий FD_READ. Реализацию такого алгоритма рекомендуем выполнить в качестве самостоятельного упражнения.
При отправке данных вероятность того, что функция send не сможет быть выполнена сразу, достаточно мала. Кроме того, как мы уже говорили, блокировка клиента при отправке данных часто бывает вполне приемлема из-за редкости и непродолжительности. Таким образом, блокирующий режим из-за своей простоты наиболее удобен при отправке данных серверу клиентом. Но мы не можем перевести сокет, работающий в асинхронном режиме, в блокирующий режим на время отправки, зато можем этот режим имитировать. Занимается этим метод SendString (листинг 2.67).
Листинг 2.67. Метод SendString, имитирующий блокирующим режим отправки
// Отправка строки серверу. Функция имитирует блокирующий
// режим работы сокета: если за один раз не удается отправить
// данные, попытка отправить их продолжается до тех пор,.
// пока все данные не будут отправлены или пока не возникнет ошибка.
procedure TESClientForm.SendString(const S: string);
var
SendRes: Integer;
// Буфер, куда помещается отправляемое сообщение
SendBuf: array of Byte;
// Сколько байтов уже отправлено
BytesSent: Integer;
begin
if Length(S) > 0 then
begin
// Отправляемое сообщение состоит из длины строки и самой строки.
// Выделяем для буфера память, достаточную для хранения
// и того и другого.
SetLength(SendBuf, SizeOf(Integer) + Length(S));
// Копируем в буфер длину строки
PInteger(@SendBuf[0])^ := Length(S);
// А затем - саму строку
Move(S[1], SendBuf[SizeOf(Integer)], Length(S));
BytesSent := 0;
// повторяем попытку отправить до тех пор, пока все содержимое
// буфера не будет отправлено серверу.
while BytesSent < Length(SendBuf) do
begin
SendRes :=
send(FSocket, SendBuf[BytesSent], Length(SendBuf) - BytesSent, 0);
if SendRes > 0 then Inc(BytesSent, SendRes)
else if WSAGetLastError = WSAEWOULDBLOCK then Sleep(10)
else
begin
MessageDlg('Ошибка при отправке данных серверу'#13#10 +
GetErrorString, mtError, [mbOK], 0);
OnDisconnect;
Exit;
end;
end;
end;
end;
Имитация блокирующего режима осуществляется очень просто: если сообщение не удалось отправить сразу, после небольшой паузы производится попытка отправить то, что ещё не отправлено, и так до тех пор, пока не будет отправлено все или пока не возникнет ошибка. В программе SimpleClient мы отправляли длину строки и саму строку разными вызовами send. Теперь, из-за того, что функция send может отправить только часть переданных ей данных, это становится неудобным из-за громоздкости многочисленных проверок. Поэтому мы создаем один буфер, куда заносим и длину строки, и саму строку, и затем передаем его как единое целое.
Примечание
Далее мы познакомимся с функцией WSASend, которая позволяет отправлять данные, находящиеся не в одном, а в нескольких разных местах. Если бы мы использовали ее, можно было бы не объединять самостоятельно длину строки и саму строку в специальном буфере, а просто передать два указателя на длину и на строку.
Чтобы продемонстрировать возможности сервера по приему нескольких слившихся запросов, клиент должен отправлять ему несколько строк сразу, поэтому на главной форме клиента мы заменяем однострочное поле ввода на многострочное (т.е. TEdit на TMemo). При нажатии кнопки Отправить клиент отправляет серверу все непустые строки из этого поля ввода.
Других существенных отличий от SimpleClient программа EventSelectClient не имеет. Получившийся пример работает не только с сервером EventSelectServer, но и с любым сервером, написанным нами ранее. Действительно, ни один из этих серверов не требует, чтобы на момент получения запроса от клиента в буфере сокета ничего не было, кроме этого запроса. Поэтому то, что EventSelectClient может отправлять несколько сообщений сразу, не помешает им работать: просто, в отличие от EventSelectServer, они будут обрабатывать эти запросы строго по одному, а не получать из сокета сразу несколько штук.
2.2.9. Перекрытый ввод-вывод
Прежде чем переходить к рассмотрению перекрытого ввода-вывода, вспомним, какие модели ввода-вывода нам уже известны. Появление разных моделей связано с тем, что операции ввода-вывода не всегда могут быть выполнены немедленно.
Самая простая модель ввода-вывода — блокирующая. В блокирующем режиме, если операция не может быть выполнена немедленно, работа нити приостанавливается до тех пор, пока не возникнут условия для выполнения операции. В неблокирующей модели ввода-вывода операция, которая не может быть выполнена немедленно, завершается с ошибкой. И наконец, в асинхронной модели ввода-вывода предусмотрена система уведомлений о том что операция может быть выполнена немедленно.
При использовании перекрытого ввода-вывода операция, которая не может быть выполнена немедленно, формально завершается ошибкой — в этом заключается сходство перекрытого ввода-вывода и неблокирующего режима. Однако, в отличие от неблокирующего режима, при перекрытом вводе-выводе библиотека сокетов начинает выполнять операцию в фоновом режиме, после ее завершения начавшая операцию программа получает уведомление об успешно выполненной операции или о возникшей при ее выполнении фатальной ошибке. Несколько операций ввода-вывода могут одновременно выполняться в фоновом режиме, как бы перекрывая работу инициировавшей их нити и друг друга. Именно поэтому данная модель получила название модели перекрытого ввода-вывода.
Перекрытый ввод-вывод существовал и в спецификации WinSock 1, но реализовывался только для линии NT. Специальных функций для перекрытого ввода-вывода в WinSock 1 не было, требовались функции ReadFile и WriteFile, в которые вместо дескриптора файла подставлялся дескриптор сокета. В WinSock 2 появилась полноценная поддержка перекрытого ввода-вывода для всех версий Windows, а в спецификацию добавились новые функции для его реализации, избавившие от необходимости использования функций файлового ввода-вывода. Здесь мы будем рассматривать перекрытый ввод-вывод только в спецификации WinSock 2, т.к. старый вариант из-за своих ограничений уже не имеет практического смысла.
Существуют два варианта уведомления о завершении операции перекрытого ввода-вывода: через событие и через процедуру завершения. Кроме того, программа может не дожидаться уведомления, а проверять состояние запроса перекрытого ввода-вывода с помощью функции WSAGetOverlappedResult (ее мы рассмотрим позже).
Чтобы сокет мог использоваться в операциях перекрытого ввода-вывода, при его создании должен быть установлен флаг WSA_FLAG_OVERLAPPED (функция socket неявно устанавливает этот флаг). Для выполнения операций перекрытого ввода-вывода сокет не нужно переводить в какой-либо особый режим, достаточно обычные функции send и recv заменить на WSARecv и WSASend. Сначала мы рассмотрим функцию WSARecv, прототип которой приведен в листинге 2.68.
Листинг 2.68. Функция WSARecv
// ***** Описание на C++ *****
int WSARecv(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
// ***** Описание на Delphi *****
function WSARecv(S: TSocket; lpBuffers: PWSABuf; dwBufferCount: DWORD; var NumberOfBytesRecvd: DWORD; var Flags: DWORD; lpOverlapped: PWSAOverlapped; lpCompletionRoutine: TWSAOverlappedCompletionRoutine): Integer;
Перекрытым вводом-выводом управляют два последних параметра функции, но WSARecv обладает и другими дополнительными по сравнению с функцией recv возможностями, не связанными с перекрытым вводом-выводом. Если оба этих параметра равны nil, или сокет создан без указания флага WSA_FLAG_OVERLAPPED, функция работает в обычном блокирующем или неблокирующем режиме, который установлен для сокета. При этом ее поведение отличается от поведения функции recv только тремя незначительными аспектами: во-первых, вместо одного буфера ей можно передать несколько, заполняемых последовательно. Во-вторых, флаги передаются ей не как значение, а как параметр-переменная, и при некоторых условиях функция WSARecv может их изменять (при использовании TCP и UDP флаги никогда не меняются, поэтому мы не будем рассматривать здесь эту возможность). В-третьих, при успешном завершении функция WSARecv возвращает ноль, а не число прочитанных байтов (последнее возвращается через параметр lpNumberOfBytesRecvd).
Буферы, в которые нужно поместить данные, передаются функции WSARecv через параметр lpBuffers. Он содержит указатель на начало массива структур TWSABuf, а параметр dwBufferCount — число элементов в этом массиве. Ранее мы знакомились со структурой TWSABuf (см. листинг 2.39): она содержит указатель на начало буфера и его размер. Соответственно, массив таких структур определяет набор буферов. При чтении данных заполнение буферов начинается с первого буфера в массиве lpBuffers, затем, если в нем не хватает места, заполняется второй буфер и т.д. Функция не переходит к следующему буферу, пока не заполнит предыдущий до последнего байта. Таким образом, данные, получаемые с помощью функции WSARecv, могут быть помещены в несколько несвязных областей памяти, что иногда бывает удобно, если принимаемые сообщения имеют строго определенный формат с фиксированными размерами компонентов пакета: в этом случае можно каждый компонент поместить в свой независимый буфер.
Теперь переходим непосредственно к рассмотрению перекрытого ввода-вывода на основе событий. Для реализации этого режима при вызове функции WSARecv параметр lpCompletionRoutine должен быть равен nil, а через параметр lpOverlapped передается указатель на запись TWSAOverlapped, которая определена следующим образом (листинг 2.69).
Листинг 2 69. Тип TWSAOverlapped
//***** Описание на C++ *****
struct _WSAOVERLAPPED {
DWORD Internal;
DWORD InternalHigh;
DWORD Offset;
DWORD OffsetHigh;
WSAEVENT hEvent;
} WSAOVERLAPPED, *LPWSAOVEPLAPPED;
// ***** Описание на Delphi *****
PWSAOverlapped = ^TWSAOverlapped;
TWSAOverlapped = packed record
Internal, InternalHigh, Offer, OffsetHigh: DWORD;
hEvent: TWSAEvent;
end;
Поля Internal, InternalHigh, Offset и OffsetHigh предназначены для внутреннего использования системой, программа не должна выполнять никаких действий с ними. Поле hEvent задает событие, которое будет взведено при завершении операции перекрытого ввода-вывода. Если на момент вызова функции WSARecv данные в буфере сокета отсутствуют, она вернет значение SOCKET_ERROR, а функция WSAGetLastError — WSA_IO_PENDING (997). Это значит, что операция начала выполняться в фоновом режиме. В этом случае функция WSARecv не изменяет значения параметров NumberOfBytesRecvd и Flag. Поля структуры TWSAOverlapped при этом также модифицируются, и эта структура должна быть сохранена программой в неприкосновенности до окончания операции перекрытого ввода-вывода. После окончания операции будет взведено событие, указанное в поле hEvent параметра lpOverlapped. При необходимости программа может дождаться этого взведения с помощью функции WSAWaitForMultipleEvents.
Как только запрос будет выполнен, в буферах, переданных через параметр lpBuffers, оказываются принятые данные. Но знания одного только факта, что запрос выполнен, недостаточно, чтобы этими данными воспользоваться, потому что, во-первых, неизвестен размер этих данных, а во-вторых, неизвестно, успешно ли завершена операция перекрытого ввода-вывода. Для получения недостающей информации служит функция WSAGetOverlappedResult, прототип которой приведен в листинге 2.70.
Листинг 2.70. Функция WSAGetOverlappedResult
// ***** Описание на C++ *****
BOOL WSAGetOverlappedResult(SOCKET s, LPWSAOVERLAPРED lpOverlapped, LPDWORD lpcbTransfer, BOOL fWait, LPDWORD lpdwFlags);
// ***** Описание на Delphi *****
function WSAGetOverlappedResult(S: TSocket; lpOverlapped: PWSAOverlapped; var cbTransfer: DWORD; fWait: BOOL; var Flags: DWORD): BOOL;
Параметры S и lpOverlapped функции WSAGetOverlappedResult определяют coкет и операцию перекрытого ввода-вывода, информацию о которой требуется получить. Их значения должны совпадать со значениями соответствующих параметров, переданных функции WSARecv. Через параметр cbTransfer возвращается число полученных байтов, а через параметр Flags — флаги (напомним, что в случае TCP и UDP флаги не модифицируются, и выходное значение параметра Flags будет равно входному значению параметра Flags функции WSARecv).
Допускается вызов функции WSAGetOverlappedResult до того, как операция перекрытого ввода-вывода будет завершена. В этом случае поведение функции зависит от параметра fWait. Если он равен True, функция переводит нить в состояние ожидания до тех пор, пока операция не будет завершена. Если он равен False, функция завершается немедленно с ошибкой WSA_IO_INCOMPLETE (996).
Функция WSAGetOverlappedResult возвращает True, если операция перекрытого ввода-вывода успешно завершена, и False, если произошли какие-то ошибки. Ошибка может возникнуть в одном из трех случаев:
1. Операция перекрытого ввода-вывода еще не завершена, а параметр fWait равен False.
2. Операция перекрытого ввода-вывода завершилась с ошибкой (например, из-за разрыва связи).
3. Параметры, переданные функции WSAGetOverlappedResult, имеют некорректные значения.
Точную причину, по которой функция вернула False, можно установить стандартным образом — по коду ошибки, возвращаемому функцией WSAGetLastError.
В принципе, программа может вообще не использовать события для отслеживания завершения операции ввода-вывода, а вызывать вместо этого время от времени функцию WSAGetOverlappedResult в удобные для себя моменты. Тогда при вызове функции WSARecv можно указать нулевое значение события hEvent. Но следует иметь в виду, что при вызове функции WSAGetOverlappedResult с параметром fWait, равным True, указанное событие служит для ожидания завершения операции, и если событие не задано, возникнет ошибка. Таким образом, если событие не используется, функция WSAGetOverlappedResult не может вызываться в режиме ожидания.
Отдельно рассмотрим ситуацию, когда на момент вызова функции WSARecv с ненулевым параметром lpOverlapped во входном буфере сокета есть данные. В этом случае функция отработает так же, как и в неперекрытом режиме, т.е. изменит значения параметров NumberOfBytesRecvd и Flags и вернет ноль, свидетельствующий об успешном выполнении функции. Но при этом событие будет взведено, а в структуру lpOverlapped будет внесена вся необходимая информация. Благодаря этому последующие вызовы функций WSAWaitForMultipleEvents и WSAGetOverlappedResult будут выполняться корректно, т.е. таким образом, как если бы функция WSARecv завершилась с ошибкой WSA_IO_PENDING, и сразу после этого в буфер сокета поступили данные. Это позволяет выполнить обработку результатов операций перекрытого ввода-вывода с помощью одного и того же кода независимо от того, были ли в буфере сокета данные на момент начала операции или нет.
Новая операция перекрытого ввода-вывода может быть начата до того, как закончится предыдущая. Это удобно при работе с несколькими сокетами: можно выполнять операции с ними параллельно в фоновом режиме, получая уведомления о завершении каждой из операций.
В MSDN не написано явно, что будет, если вызвать для сокета функцию WSARecv повторно, до того как будет завершена предыдущая операция перекрытого чтения (но запрета на такие действия тоже нет). Эксперименты показывают, что в этом случае операции перекрытого чтения встают в очередь, т.е. первый полученный сокетом пакет приводит к завершению операции, начатой первой, второй пакет — к завершению операции, начатой второй, и т.д. Но поскольку это явно не документировано, лучше не полагаться на то, что такой порядок будет всегда соблюдаться.
В качестве примера реализации перекрытого ввода-вывода рассмотрим, ситуацию, когда программа начинает операцию чтения данных из сокета, время от времени проверяя статус операции (листинг 2.71). События в этом примере не используются, проверка осуществляется с помощью функции WSAGetOverlappedResult.
Листинг 2.71. Перекрытый ввод-вывод с использованием функции WSAGetOverlappedResult
var
S: TSocket;
Overlapped: TWSAOverlapped;
BufPtr: TWSABuf;
RecvBuf: array[1..100] of Char;
Cnt, Flags: Cardinal;
begin
// Инициализация WinSock, создание сокета S, привязка его к адресу
......
// Подготовка структуры, задавшей буфер
BufPtr.Buf := @RBuf;
BufPtr.Len := SizeOf(RBuf);
// Подготовка структуры TWSAOverlapped
// Поля Internal, InternalHigh, Offset, OffsetHigh программа
// не устанавливает
Overlapped.hEvent := 0;
Flags := 0;
// Начало операции перекрытого получения данных
WSARecv(S, @BufPtr, 1, Cnt, Flags, @Overlapped, nil);
while True do
begin
if WSAGetOverlappedResult(S, @Overlapped, Cnt, False, Flags) then
begin
// Данные получены, находятся в RecvBuf, обрабатываем
......
// Выходим из цикла Break;
end
else if WSAGetLastError <> WSA_IO_INCOMPLETE then
begin
// Произошла ошибка, анализируем ее
......
// Выходим из цикла
Break;
end
else
begin
// Операция чтения не завершена
// Занимаемся другими действиями
end;
end;
Теперь перейдем к рассмотрению перекрытого ввода-вывода на основе процедур завершения. Для этого при вызове функции WSARecv нужно задать указатель на процедуру завершения, описанную в программе. Процедура завершения должна иметь прототип, приведенный в листинге 2.72.
Листинг 2.72. Прототип процедуры завершения
// ***** Описание на C++ *****
void CALLBACK CompletionROUTINE(DWORD dwError, DWORD cbTransferred, LPWSAOVERLAPPED lpOverlapped, DWORD dwFlags);
// ***** Описание на Delphi *****
TWSAOverlappedCompletionRoutine =
procedure(dwError: DWORD; cbTransferred: DWORD; lpOverlapped: PWSAOverlapped; dwFlags: DWORD); stdcall;
При использовании процедур завершения в функцию WSARecv также нужно передавать указатель на запись TWSAOverlapped через параметр lpOverlapped, но значение поля hEvent этой структуры игнорируется. Вместо взведения события при завершении операции будет вызвана процедура, указанная в качестве параметра функции WSARecv. Указатель на структуру, заданный при вызове WSARecv, передается в процедуру завершения через параметр lpOverlapped. Смысл остальных параметров очевиден: dwError — это код ошибки (или ноль, если операция завершена успешно), cbTransferred — число полученных байтов (само полученное сообщение копируется в буферы, указанные при вызове функции WSARecv), a dwFlags — флаги.
Процедура завершения всегда выполняется в той нити, которая инициировала начало операции перекрытого ввода-вывода. Но система не может прерывать нить для выполнения процедуры завершения в любой удобный ей момент — нить должна перейти в состояние ожидания. В это состояние ее можно перевести, например, с помощью функции SleepEx, имеющей следующий прототип:
function SleepEx(dwMilliseconds: DWORD; bAlertable: BOOL); DWORD;
Функция SleepEx является частью стандартного API системы и импортируется модулем Windows. Она переводит нить в состояние ожидания. Параметр dwMilliseconds задает время ожидания в миллисекундах (или значение INFINITE для бесконечного ожидания). Параметр bAlertable указывает, допустимо ли прерывание состояния ожидания для выполнения процедуры завершения. Если bAlertable равен False, функция SleepEx ведет себя так же как функция Sleep, т.е. просто приостанавливает работу нити на заданное время. Если bAlertable равен True, нить может быть выведена системой из состояния ожидания раньше, чем истечет заданное время, если возникнет необходимость выполнить процедуру завершения. О причине завершения ожидания программа может судить по результату, возвращаемому функцией SleepEx: ноль в случае завершения по тайм-ауту и WAIT_IO_COMPLETION в случае завершения из-за выполнения процедуры завершения (в последнем случае сначала выполняется процедура завершения, а потом только происходит возврат из функции SleepEx). Если завершились несколько операций перекрытого ввода-вывода, в результате выполнения SleepEx будут вызваны процедуры завершения для всех этих операций.
Существует также возможность ожидать выполнения процедуры завершения одновременно с ожиданием взведения событий с помощью функции WSAWaitForMultipleEvents. Напомним, что у этой функции также есть параметр fAlertable. Если задать его равным True, то при необходимости выполнения процедуры завершения функция WSAWaitForMultipleEvents, подобно функции SleepEx, выполняет эту процедуру и возвращает WAIT_IO_COMPLETION.
Если программа выполняет одновременно несколько операций перекрытого ввода-вывода, возникает вопрос, как при вызове процедуры завершения определить, какая из них завершилась. Для каждой такой операции должен быть создан уникальный экземпляр записи TWSAOverlapped. Процедура завершения получает указатель на тот экземпляр, который использовался для начала завершившейся операции. Можно сравнил, указатель с теми, которые были заданы при запуске операций перекрытого ввода-вывода, и определить, какая из них завершилась. Это не всегда бывает удобно из-за необходимости где-то хранить список указателей, заданных при начале операций перекрытого ввода-вывода. Существуют еще два варианта решения этой проблемы. Первый заключается в создании своей процедуры завершения для каждой из выполняющихся параллельно операций. Этот способ приводит к получению громоздкого кода и может быть неудобен, если число одновременно выполняющихся операций заранее неизвестно. Он целесообразен только при одновременном выполнении разнородных операций, требующих разных алгоритмов при обработке их завершения. Другой вариант предлагается в MSDN. Так как при работе через процедуры завершения значение поля hEvent структуры TWSAOverlapped игнорируется системой, программа может записать туда любое 32-битное значение и с его помощью определить, какая из операций завершена. В строго типизированном языке, каким является Delphi, подобное смещение типа дескриптора и целого выглядит весьма непривлекательно, но, к сожалению, это лучшее из того, что нам предлагают разработчики WinSock API.
Механизм процедур завершения допускает определение статуса операции с с помощью функции WSAGetOverlappedResult, но ее параметр fWait обязательно должен быть равен False, потому что события, необходимые для выполнения ожидания, не взводятся, и попытка дождаться окончания операции может привести к блокировке работы нити.
В процедуре завершения допускается вызывать функции, начинающие новую операцию перекрытого ввода-вывода, в том числе и такую же операцию, которая только что завершена. Эта возможность используется в примере, приведенном в листинге 2.73. Пример иллюстрирует работу клиента, который подключается к серверу и получает от него данные в режиме перекрытого ввода-вывода, выполняя параллельно какие-то другие действия.
Листинг 2.73. Перекрытый ввод-вывод с использованием процедуры завершения
var
S: TSocket;
Overlapped: TWSAOverlapped;
BufPtr: TWSABuf;
RecvBuf: array[1..100] of Char;
Cnt, Flags: Cardinal;
Connected: Boolean;
procedure GetData(Err, Cnt:DWORD; OvPtr: PWSAOverlapped; Flags: DWORD): stdcall;
begin
if Err <> 0 then
begin
// Произошла ошибка. Соединение нужно устанавливать заново
closesocket(S);
Connected := False;
end;
else
begin
// Получены данные, обрабатываем
......
// Запускаем новую операцию перекрытого чтения
Flags := 0;
WSARecv(S, @BufPtr, 1, Cnt, Flags, OvPtr, GetData);
end;
end;
procedure ProcessConnection;
begin
// Устанавливаем начальное состояние - сокет не соединен
Connected := False;
// Задаем буфер
BufPtr.Buf := @RecvBuf;
BufPtr.Len := SizeOf(RecvBuf);
while True do
begin
if not Connected then
begin
Connected := True;
// Создаем и подключаем сокет
S := socket(AF_INET, SOCK_STREAM, 0);
connect(S, ...);
// Запускаем первую для данного сокета операцию чтения
Flags := 0;
WSARecv(S, @BufPtr, 1, Cnt, Flags, @Overlapped, GetData);
end;
// Позволяем системе выполнить процедуру завершения,
// если это необходимо
SleepEx(0, True);
// Выполняем какие-либо дополнительные действия
......
end;
end;
Основная процедура здесь — ProcessConnection. Эта процедура в бесконечном цикле устанавливает соединение, если оно не установлено, дает системе выполнить процедуру завершения, если это требуется, и выполняет какие-либо иные действия, не связанные с получением данных от сокета. Процедура завершения GetData получает и обрабатывает данные, а если произошла ошибка, закрывает сокет и сбрасывает флаг Connected, что служит для процедуры ProcessConnection сигналом о необходимости установить соединение заново.
Из этого примера хорошо видны достоинства и недостатки процедур заверения. Получение и обработка данных выносится в отдельную процедуру, и с одной стороны, позволяет разгрузить основную процедуру, но, с другой стороны, заставляет прибегнуть к глобальным переменным для буфера и сокета.
Для протоколов, не поддерживающих соединение, существует другая функция для перекрытого получения данных — WSARecvFrom. Из названия очевидно, что она позволяет узнать адрес отправителя. Прототип функции WSARecvFrom приведен в листинге 2.74.
Листинг 2.74. Функция WSARecvFrom
// ***** Описание на C++ *****
int WSARecvFrom(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, struct sockaddr FAR *lpFrom, LPINT lpFromlen, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine;
// ***** Описание на Delphi *****
function WSARecvFrom(S: TSocket; lpBuffers: PWSABuf; dwBufferCount: DWORD; var NumberOfBytesRecvd: DWORD; var Flags: DWORD; lpFrom: PSockAddr; lpFromLen: PInteger; lpOverlapped: FWSAOverlapped; lpCompletionRoutine: TWSAOverlappedCompletionRoutine): Integer;
Параметры lpFrom и lpFromLen этой функции, служащие для получения адреса отправителя, эквивалентны соответствующим параметрам функции recvfrom, с которой мы уже хорошо знакомы. В остальном WSARecvFrom ведет себя так же, как WSARecv, поэтому мы не будем останавливаться на ней.
Для отправки данных в режиме перекрытого ввода-вывода существуют функции WSASend и WSASendTo, имеющие следующие прототипы (листинг 2.75).
Листинг 2.75. Функции WSASend и WSASendTo
// ***** Описание на C++ *****
int WSASend(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
int WSASendTo(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount, LPDWORD lpNumberOfBytesSent, DWORD dwFlags, const struct sockaddr FAR *lpTo, int iToLen, LPWSAOVERLAPPED lpOverlapped, LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);
// ***** Описание на Delphi *****
function WSASend(S: TSocket; lpBuffers: PWSABuf; dwBufferCount: DWORD; var NumberOfBytesRecvd: DWORD; Flags: DWORD; lpOverlapped: PWSAOverlapped; lpCompletionRoutine: TWSAOverlappedCompletionRoutine): Integer;
function WSASendTo(S: TSocket; lpBuffers: PWSABuf; dwBufferCount: DWORD; var NumberOfBytesRecvd: DWORD; Flags: DWORD; var AddrTo: TSockAddr; ToLen: Integer; lpOverlapped: PWSAOverlapped; lpCompletionRoutine: TWSAOverlappedCompletionRoutine): Integer;
Если вы разобрались с функциями WSARecv, send и sendto, то смысл параметров функций WSASend и WSASendTo должен быть вам очевиден, поэтому подробно разбирать мы их не будем. Но отметим, что флаги передаются по значению, и функции не могут изменять их.
Потребность в перекрытом вводе-выводе при отправке данных возникает достаточно редко. Но функции WSASend/WSASendTo могут оказаться удобными при подготовке многокомпонентных пакетов, которые, например, имеют фиксированный заголовок и финальную часть. Для таких пакетов можно один раз подготовить буферы с заголовком и с финальной частью и, пользуясь возможностью отправки данных из несвязных буферов, при отправке каждого пакета менять только его среднюю часть.
2.2.10. Сервер, использующий перекрытый ввод-вывод
В этом разделе мы рассмотрим создание сервера на основе перекрытого ввода-вывода на основе процедур завершения (пример кода с использованием событий есть в MSDN в описании функций WSARecv — и WSASend). Перекрытый ввод-вывод лучше подходит для обмена в режиме "запрос-ответ", поэтому мы вновь вернемся к первоначальному протоколу, который не предусматривает отправку сервером сообщений по собственному усмотрению. На компакт-диске этот пример называется OverlappedServеr.
Как обычно, для каждого соединения создается экземпляр записи TConnection, которая на этот раз выглядит так, как показано в листинге 2.76.
Листинг 2.76. Тип TConnection
// Информация о соединении с клиентом:
// ClientSocket - сокет, созданный для взаимодействия с клиентом
// ClientAddr - строковое представление адреса клиента
// MsgSite - длина строки, получаемая от клиента
// Msg - строка, получаемая от клиента или отправляемая ему
// Offset - количество байтов, уже полученных от клиента
// или отправляемых ему на данном этапе
// BytesLeft - сколько байтов осталось получить от клиента
// или отправить ему на данном этапе
// Overlapped - структура для выполнения перекрытой операции
PConnection = ^TConnection;
TConnection = record
ClientSocket: TSocket;
ClientAddr: string;
MsgSize: Integer;
Msg: string;
Offset: Integer;
BytesLeft: Integer;
Overlapped: TWSAOverlapped;
end;
Основное отличие этого варианта типа TConnection от того, что применялся ранее в примерах NonBlockingServer и AsyncSelectServer (см. разд. 2.1.16 и 2.2.6, а также листинг 2.31) — это отсутствие поля Phase, которое хранит этап взаимодействия с клиентом. Разумеется, в программе OverlappedServer взаимодействие с клиентом также разбивается на три этапа, но реализуется другой способ для того, чтобы различать этапы — для каждого этапа создается своя процедура завершения.
Примечание
Использование одной процедуры завершения для всех трех этапов и распознавание в ней этапов с помощью поля Phase в случае перекрытого ввода-вывода также возможно. Рекомендуем написать такой вариант сервера в качестве самостоятельного упражнения.
Поле Overlapped содержит структуру TWSAOverlapped, которой программа непосредственно не пользуется, она только передает указатель на эту структуру в функции WSARecv и WSASend. Напомним, что одновременно может выполняться несколько операций перекрытого ввода-вывода, но у каждой из этих операций должен быть свой экземпляр TWSAOverlapped. Гак как в нашем случае с одним клиентом в каждый момент времени может выполняться не более одной операции, мы создаем по одному экземпляру TWSAOverlapped на каждого клиента.
Функция для перекрытого подключения клиентов существует — это AcceptEx, с которой мы познакомимся в разд. 2.2.12. Но она неудобна при работе совместно с WSARecv и WSASend, особенно в таком строго типизированном языке, как Delphi. Поэтому подключение клиентов мы будем отслеживать с помощью уже опробованной технологии асинхронных сокетов на сообщениях. Код запуска сервера OverlappedServer выглядит идентично коду запуска AsyncSelectServer (см. листинг 2.30): точно так же создается сокет, ставится в режим прослушивания, а затем его событие FD_ACCEPT привязывается к сообщению WM_ACCEPTMESSAGE.
Сам обработчик WM_ACCEPTMESSAGE выглядит теперь следующим образом (листинг 2.77).
Листинг 2.77. Обработчик сообщения WM_ACCEPTMESSAGE
procedure TServerForm.WMAcceptMessage(var Msg: TWMSocketMessage);
var
NewConnection: PConnection;
// Сокет, который создается для вновь подключившегося клиента
ClientSocket: TSocket;
// Адрес подключившегося клиента
ClientAddr: TSockAddr;
// Длина адреса
AddrLen: Integer;
// Аргумент для перевода сокета в неблокирующий режим
Arg: u_long;
// Буфер для операции перекрытого чтения
Buf: TWSABuf;
NumBytes, Flags: DWORD;
begin
// Страхуемся от "тупой" ошибки
if Msg.Socket <> FServerSocket then
raise ESocketError.Create(
'Внутренняя ошибка сервера - неверный серверный сокет');
// Обрабатываем ошибку на сокете, если она есть
if Msg.SockError <> 0 then
begin
MessageDlg('Ошибка при подключении клиента:'#13#10 +
GetErrorString(Msg.SockError) +
#13#10'Сервер будет ocтановлен', 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
// Новый сокет наследует свойства слушающего сокета.
// В частности, он работает в асинхронном режиме,
// и его событие FD_ACCEPT связано с сообщением WM_ACCEPTMESSAGE.
// Так как нам это совершенно не нужно, отменяем асинхронный
// режим и делаем сокет блокирующим.
if WSAAsyncSelect(ClientSocket, Handle, 0, 0) = SOCKET_ERROR then
begin
MessageDlg('Ошибка при отмене асинхронного режима ' +
'подключившегося сокета:'#13#10 + GetErrorString,
mtError, [mbOK], 0);
closesocket(ClientSocket);
Exit;
end;
Arg := 0;
if ioctlsocket(ClientSocket, FIONBIO, Arg) = 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.Offset := 0;
NewConnection.BytesLeft := SizeOf(Integer);
NewConnection.Overlapped.hEvent := 0;
// Добавляем запись нового соединения в список
FConnections.Add(NewConnection);
AddMessageToLog('Зафиксировано подключение с адреса ' +
NewConnection.ClientAddr);
// Начинаем перекрытый обмен с сокетом.
// Начинаем, естественно, с чтения длины строки,
// в качестве принимающего буфера используем NewConnection.MsgSize
Buf.Len := NewConnection.BytesLeft;
Buf.Buf := @NewConnection.MsgSize;
Flags := 0;
if WSARecv(NewConnection.ClientSocket, @Buf, 1, NumBytes, Flags,
@NewConnection.Overlapped, ReadLenCompleted) = SOCKET_ERROR then
begin
if WSAGetLastError <> WSA_IO_PENDING then
begin
AddMessageToLog('Клиент ' + NewConnection.ClientAddr +
' - ошибка при чтении длины строки: ' + GetErrorString);
RemoveConnection(NewConnection);
end;
end;
end;
end;
После того как сокет для взаимодействия с подключившимся клиентом создан, следует отменить для него асинхронный режим, унаследованный от слушающего сокета, т.к. при перекрытом вводе-выводе этот режим не нужен. Затем, после создания экземпляра TConnection и добавления его в список, запускается первая операция перекрытого чтения с помощью функции WSARecv. Об окончании этой операции будет сигнализировать вызов функции ReadLenCompleted, которая передана в WSARecv в качестве параметра.
Как мы уже говорили ранее, в программе OverlappedServer есть три разных функции завершения: ReadLenCompleted, ReadMsgCompleted и SendMsgCompleted. Последовательность работы с ними такая: сначала для чтения длины строки вызывается WSARecv, в качестве буфера передастся Connection.MsgSize, в качестве функции завершения — ReadLenCompleted (это мы уже видели в листинге 2.77). Когда вызывается ReadLenCompleted, это значит, что операция чтения уже завершена и прочитанная длина находится в Connection.MsgSize. Поэтому в функции ReadLenCompleted выделяем нужный размер для строки Connection.Msg и запускаем следующую операцию перекрытого чтения — с буфером Connection.Msg и функцией завершения ReadMsgCompleted. В этой функции полученная строка показывается пользователю, формируется ответ, и запускается следующая операция перекрытого ввода-вывода — отправка строки клиенту. В качестве буфера в функцию WSASend передаётся Connection.Msg, а в качестве функции завершения — SendMsgCompleted. В функции SendMsgCompleted вновь вызывается WSARecv с буфером Connection.MsgSize и функцией завершения ReadLenCompleted, и таким образом сервер возвращается к первому этапу взаимодействия с клиентом.
Описанную простую последовательность действий портит то, что из-за возможной отправки данных по частям можно столкнуться с ситуацией, когда функция завершения вызвана для уведомления о том, что получена или отправлена часть данных. Чтобы получить остальную их часть, необходимо вновь вызвать функцию чтения или записи с той же функцией завершения, а указатель на буфер должен при этом указывать на оставшуюся незаполненной часть переменной, в которую помещаются данные. С учетом этого, а также необходимости обработки ошибок, функции завершения выглядят так, как показано в листинге 2.78.
Листинг 2.78. Функции завершения
// Функция ReadLenCompleted используется в качестве функции завершения
// для перекрытого чтения длины строки
procedure ReadLenCompleted(dwError: DWORD; cdTransferred: DWORD; lpOverlapped: PWSAOverlapped; dwFlags: DWORD); stdcall;
var
// Указатель на соединение
Connection: PConnection;
// Указатель на буфер
Buf: TWSABuf;
// Параметры для WSARecv
NumBytes, Flags: DWORD;
begin
// Для идентификации операции в функцию передается указатель
// на запись TWSAOverlapped. Ищем по этому указателю
// подходящее соединение в списке FConnections.
Connection := ServerForm.GetConnectionByOverlapped(lpOverlapped);
if Connection = nil then
begin
ServerForm.AddMessageToLog(
'Внутренняя ошибка программы - не найдено соединение');
Exit;
end;
// Проверяем, что не было ошибки
if dwError <> 0 then
begin
ServerForm.AddMessageToLog('Клиент ' + Connection.ClientAddr +
' - ошибка при чтении длины строки: ' + GetErrorString(dwError));
ServerForm.RemoveConnection(Connection);
Exit;
end;
// Уменьшаем число оставшихся к чтению байтов
// на размер полученных данных
Dec(Connection.BytesLeft, cdTransferred);
if Connection.BytesLeft < 0 then
// Страховка от "тупой" ошибки
ServerForm.AddMessageToLog('Клиент ' + Connection.ClientAddr +
' - внутренняя ошибка программы: получено больше байтов, ' +
'чем ожидалось');
ServerForm.RemoveConnection(Connection);
end
else if Connection.BytesLeft = 0 then
begin
// Длина строки прочитана целиком
if Connection.MsgSize <= 0 then
begin
ServerForm.AddMessageToLog('Клиент ' + Connection.ClientAddr +
' — получена неверная длина строки ' +
IntToStr(Conneсtion.MsgSizе));
ServerForm.RemoveConnection(Connection);
Exit;
end;
// Делаем строку нужной длины
SetLength(Connection.Msg, Connection.MsgSize);
// Данные пока не прочитаны, поэтому смещение - ноль,
// осталось прочитать полную длину.
Connection.Offset := 0;
Connection.BytesLeft := Connection.MsgSize;
// Заносим размер буфера и указатель на него в Buf.
// Данные будут складываться в строку,
// на которую ссылается Connection.Msg.
Buf.Len := Connection.MsgSize;
Buf.Buf := Pointer(Connection.Msg);
// Вызываем WSARecv для чтения самой строки
Flags := 0;
if WSARecv(Connect ion.ClientSocket, @Buf, 1, NumBytes, Flags,
@Connection.Overlapped, ReadMsgCompleted) = SOCKET_ERROR then
begin
if WSAGetLastError <> WSA_IO_PENDING then
begin
ServerForm.AddMessageToLog('Клиент ' + Connection.ClientAddr +
' - ошибка при чтении строки: ' + GetErrorString(dwError));
ServerForm.RemoveConnection(Connection);
end;
end;
end
else
begin
// Connection.BytesLeft < 0 - длина строки
// прочитана не до конца.
// Увеличиваем смещение на число прочитанных байтов
Inc(Connection.Offset, cdTransferred);
// Формируем буфер для чтения оставшейся части длины
Buf.Len := Connection.BytesLeft;
Buf.Buf := PChar(@Connection.MsgSize) + Connection.Offset;
// вызываем WSARecv для чтения оставшейся части длины строки
Flags := 0;
if WSARecv(Connection.ClientSocket, @Buf, 1, NumBytes, Flags,
@Connection.Overlapped, ReadMsgCompleted) = SOCKET_ERROR then
begin
if WSAGetLastError <> WSA_IO_PENDING then
begin
ServerForm.AddMessageToLog('Клиент ' + Connection.ClientAddr +
' - ошибка при чтении длины строки: ' +
GetErrorString(dwError));
ServerForm.RemoveConnection(Connection);
end;
end;
end;
end;
// Функция ReadMsgCompleted используется в качестве функции завершения
// для перекрытого чтения строки.
// Она во многом аналогична функции ReadLenCompleted
procedure ReadMsgCompleted(dwError: DWORD; cdTransferred: DWORD; lpOverlapped: PWSAOverlapped; dwFlags: DWORD); stdcall;
var
Connection: PConnection;
Buf: TWSABuf;
NumBytes, Flags: DWORD;
begin
Connection := ServerForm.GetConnectionByOverlapped(lpOverlapped);
if Connection = nil then
begin
ServerForm.AddMessageToLog(
'Внутренняя ошибка программы - не найдено соединение');
Exit;
end;
if dwError <> 0 then
begin
ServerForm.AddMessageToLog('Клиент ' + Connection.ClientAddr +
' ошибка при чтении строки: ' + GetErrorString(dwError));
ServerForm.RemoveConnection(Connection);
Exit;
end;
Dec(Connection.BytesLeft, cdTransferred);
if Connection.BytesLeft < 0 then
begin
ServerForm.AddMessageToLog('Клиент ' + Connection.ClientAddr +
' - внутренняя ошибка программы: получено больше байтов, ' +
'чем ожидалось');
ServerForm.RemoveConnection(Connection);
end
else if Connection.BytesLeft = 0 then
begin
// Строка получена целиком. Выводим ее на экран.
ServerForm.AddMessageToLog('От клиента ' + Connection.ClientAddr +
' получена строка: ' + Connection.Msg);
// Формируем ответ
Connection.Msg :=
AnsiUpperCase(StringReplace(Connection.Msg, #0,
'#0', [rfReplaceAll])) + ' (Overlapped server)'#0;
// Смещение - ноль, осталось отправить полную длину
Connection.Offset := 0;
Connection.BytesLeft := Length(Connection.Msg);
// Формируем буфер из строки Connection.Msg
Buf.Len := Connection.BytesLeft;
Buf.Buf := Point(Connection.Msg);
// Отправляем строку
if WSASend(Connection.ClientSocket, @Buf, 1, NumBytes, 0,
@Connection.Overlapped, SendMsgCompleted) = SOCKET_ERROR then
begin
it WSAGetLastError <> WSA_IO_PENDING then
begin
ServerForm.AddMessageToLog('Клиент ' + Connection.ClientAddr +
' - ошибка при отправке строки: ' + GetErrorString);
ServerForm.RemoveConnection(Connection);
end;
end;
end
else
begin
// Connection.BytesLeft < 0 - строка прочитана частично
Inc(Connection.Offset, cdTransferred);
// Формируем буфер из непрочитанного остатка строки
Buf.Len := Connection.BytesLeft;
Buf.Buf := PChar(Connection.Msg) + Connection.Offset;
// Читаем остаток строки
Flags := 0;
if WSARecv(Connection.ClientSocket, @Buf, 1, NumBytes, Flags,
@Connection.Overlapped, ReadMsgCompleted) = SOCKET_ERROR then
begin
if WSAGetLastError <> WSA_IO_PENDING then
begin
ServerForm.AddMessageToLog('Клиент ' + Connection.ClientAddr +
' - ошибка при чтении строки: ' + GetErrorString);
ServerForm.RemoveConnection(Connection);
end;
end;
end;
end;
// Функция SendMsgCompleted используется в качестве функции завершения
// для перекрытой отправки строки.
// Во многом она аналогична функции ReadLenCompleted
procedure SendMsgCompleted(dwError: DWORD; cdTransferred: DWORD; lpOverlapped: PWSAOverlapped; dwFlags: DWORD); stdcall;
var
Connection: PConnection;
Buf: TWSABuf;
NumBytes, Flags: DWORD;
begin
Connection := ServerForm.GetConnectionByOverlapped(lpOverlapped);
if Connection = nil then
begin
ServerForm.AddMessageToLog(
'Внутренняя ошибка программы - не найдено соединение');
Exit;
end;
if dwError <> 0 then
begin
ServerForm.AddMessageToLog('Клиент ' + Connection.ClientAddr +
' - ошибка при отправке строки: ' + GetErrorString(dwError));
ServerForm.RemoveConnection(Connection);
Exit;
end;
Dec(Connection.BytesLeft, cdTransferred);
if Connection.BytesLeft < 0 then
begin
ServerForm.AddMessageToLog('Клиент ' + Connection.ClientAddr +
' — внутренняя ошибка программы: отправлено больше байтов, ' +
'чем ожидалось');
ServerForm.RemoveConnection(Connection);
end
else if Connection.BytesLeft = 0 then
begin
// Строка отправлена целиком. Выводим сообщение об этом.
ServerForm.AddMessageToLog('Клиенту ' + Connection.ClientAddr +
' отправлена строка: ' + Connection.Msg);
// Очищаем строку, чтобы зря не занимала память
Connection.Msg := '';
// Теперь будем снова читать длину строки
Connection.Offset := 0;
Connection.BytesLeft := SizeOf(Integer);
// Читать будем в Connection.MsgSize
Buf.Len := Connection.BytesLeft;
Buf.Buf := @Connection.MsgSize;
Flags := 0;
if WSARecv(Connection.ClientSocket, @Buf, 1, NumBytes, Flags,
@Connection.Overlapped, ReadLenCompleted) = SOCKET_ERROR then
begin
if WSAGetLastError <> WSA_IO_PENDING then
begin
ServerForm.AddMessageToLog('Клиент ' + Connection.ClientAddr +
' - ошибка при чтении длины строки: ' + GetErrorString);
ServerForm.RemoveConnection(Connection);
end;
end;
end
else
begin
// Строка отправлена не полностью
Inc(Connection.Offset, cdTransferred);
// Формируем буфер из остатка строки
Buf.Len := Connection.BytesLeft;
Buf.Buf := PChar(Connection.Msg) + Connection.Offset;
if WSASend(Connection.ClientSocket, @Buf, 1, NumBytes, 0,
@Connection.Overlapped, SendMsgCompleted) = SOCKET_ERROR then
begin
if WSAGetLastError <> WSA_IO_PENDING then
begin
ServerForm.AddMessageToLog('Клиент ' + Connection.СlientAddr +
' - ошибка при отправке строки: ' + GetErrorString);
ServerForm.RemoveConnection(Connection);
end;
end;
end;
end;
Чтобы это все заработало, остался последний штрих: нить нужно время от времени переводить в состояние ожидания. Мы будем это делать, вызывая SleepEx с нулевым тайм-аутом по сигналам от таймера. В получившемся сервере трудно увидеть все преимущества перекрытого ввода-вывода. Это и неудивительно, потому что его главное достоинство — высокая производительность при большом количестве подключений. Перекрытый ввод-вывод ориентирован на создание серверов, интенсивно взаимодействующих с многими клиентами, а на таком маленьком сервере, как OverlappedServer, он выглядит несколько тяжеловесно, хотя и позволяет получить вполне работоспособный вариант.
2.2.11. Многоадресная рассылка
При описании стека протоколов TCP/IP мы упоминали протокол IGMP - дополнение к протоколу IP, позволяющее назначать нескольким узлам групповые адреса. С помощью этого протокола можно группе сокетов назначить один IP-адрес, и тогда все пакеты, отправленные на этот адрес, будут получать все сокеты, входящие в группу. Заметим, что не следует путать группы сокетов в терминах IGMP, и группы сокетов в терминах WinSock (поддержка групп сокетов в WinSock пока отсутствует, существуют только зарезервированные для этого параметры в некоторых функциях).
Мы уже говорили, что сетевая карта получает все IP-пакеты, которые проходят через ее подсеть, но выбирает из них только те, которые соответствуют назначенному ей MAC- и IP-адресу. Существуют два режима работы сетевых карт. В первом выборка пакетов осуществляется аппаратными средствами карты, во втором — программными средствами драйвера. Аппаратная выборка осуществляется быстрее и не загружает центральный процессор, но ее возможности ограничены. В частности, у некоторых старых карт отсутствует аппаратная поддержка IGMP, поэтому они не могут получать пакеты, отправленные на групповой адрес, без переключения в режим программной выборки. Более современные сетевые карты способны запоминать несколько (обычно 16 или 32) групповых адресов, и, пока количество групповых адресов не превышает этот предел, могут осуществлять аппаратную выборку пакетов с учетом групповых адресов.
Windows 95 и NT 4 используют сетевые карты в режиме программной выборки пакетов. Windows 98 и 2000 и выше по умолчанию устанавливают сетевые карты в режим аппаратной выборки пакетов. При этом Windows 2000 может переключать карту в режим программной выборки, если число групповых адресов, с которых компьютер должен принимать пакеты, превышает ее аппаратные возможности. Windows 98 такой возможностью не обладает, поэтому программа, выполняемая в этой среде, может столкнуться с ситуацией, когда сокет не сможет присоединиться к групповому адресу из-за нехватки аппаратных ресурсов сетевой карты (программа при этом получит ошибку WSAENOBUFS).
WinSock предоставляет достаточно широкие возможности по управлению многоадресной рассылкой, но для их использования необходимо, чтобы выбранный сетевой протокол поддерживал все эти возможности. Поддержка многоадресной рассылки протоколом IP достаточно скудна по сравнению, например, с протоколами, применяющимися в сетях ATM. Здесь мы будем рассматривать только те возможности WinSock по поддержке многоадресной рассылки, которые совместимы с протоколом IP.
Протокол TCP не поддерживает многоадресную рассылку, поэтому все, что далее будет сказано, относится только к протоколу UDP. Отметим также, что при многоадресной рассылке через границы подсетей маршрутизаторы должны поддерживать передачу многоадресных пакетов. Глава "Многоадресная рассылка" в [3], к сожалению, содержит множество неточностей. Далее мы будем обращать внимание на эти неточности, чтобы облегчить чтение этой книги.
Многоадресная рассылка в IP является одноранговой и в плоскости управления, и в плоскости данных (в [3] вместо "одноранговая" употребляется слово "немаршрутизируемая" — видимо, переводчик просто перепутал слова non-rooted и non-routed). Это значит, что все сокеты, участвующие в ней, paвноправны. Каждый сокет без каких-либо ограничений может подключиться к многоадресной группе и получать все сообщения, отправленные на групповой адрес. При этом послать сообщение на групповой адрес может любой сокет, в том числе и не входящий в группу. Для групповых адресов протокол IP задействует диапазон от 224.0.0.0 до 239.255.255.255. Часть из этих адресов зарезервирована для стандартных служб, поэтому своим группам лучше назначать адреса, начиная с 225.0.0.0. Кроме того, весь диапазон от 224.0.0.0 до 224.0.0.255 зарезервирован для групповых сообщений, управляющих маршрутизаторами, поэтому сообщения, отправленные на эти адреса, никогда не передаются в соседние подсети.
Есть два варианта осуществления многоадресной рассылки с использованием IP средствами WinSock. Первый реализуется средствами WinSock 1 и жестко привязан к протоколу IP. Второй вариант подразумевает работу с WinSock 2 и осуществляется универсальными, не привязанными к конкретному протоколу средствами.
Если рассылка будет осуществляться средствами WinSock 1, то сокет, участвующий в ней, создается обычным образом — с помощью функции WSASocket со стандартным набором флагов или с помощью функции socket с обычными параметрами, задаваемыми при создании UDP-сокета. Если же используется WinSock 2, то сокет должен быть создан с указанием его роли в плоскостях управления и данных. Так как многоадресная рассылка в IP является одноранговой, все сокеты, участвующие в ней, могут быть только "листьями", поэтому сокет для рассылки должен создаваться функцией WSASocket с указанием флагов WSA_FLAG_MULTIPONT_C_LEAF (4) и WSA_FLAG_MULTIPOINT_D_LEAF (16). В [3] на странице 313 написано, что для рассылки средствами WinSock 2 можно создавать сокет функцией socket — это неверно. Впрочем, на странице 328 все-таки сказано, что указанные флаги задавать обязательно. Далее сокет, который планируется добавить в группу, привязывается к любому локальному порту обычным способом — с помощью функции bind. Этот шаг ничем не отличается от привязки к адресу обычного сокета, не использующего групповой адрес.
Затем выполняется собственно добавление сокета в группу. В WinSock 12 для этого потребуется функция setsockopt с параметром IP_ADD_MEMBERSHIP, в качестве уровня следует указать IPPROTO_IP. При этом через параметр optval передается указатель на запись ip_mreq, описанную так, как показано в листинге 2.79.
Листинг 2.79. Тип TIPMreq
// ***** Описание на C++ *****
struct ip_mreq {
struct in_addr imr_multiaddr;
struct in_addr imr_interface;
}
// ***** Описание на Delphi *****
TIPMreq = packed record
IMR_MultiAddr: TSockAddr;
IMR_Interface: TSockAddr
end;
Поле IMR_MultiAddr задает групповой адрес, к которому присоединяется сокет. У этой структуры должны быть заполнены поля sin_family (значением AF_INET) и sin_addr. Номер порта здесь указывать не нужно, значение этого поля игнорируется. Поле IMR_Interface определяет адрес сетевого интерфейса, через который будет вестись прием многоадресной рассылки. Если программу устраивает интерфейс, выбираемый системой по умолчанию, значение поля IMR_Interface.sin_addr должно быть INADDR_ANY (на компьютерах с одним сетевым интерфейсом обычно используется именно это значение). Но если у компьютера несколько сетевых интерфейсов, которые связывают его с разными сетями, интерфейс для получения групповых пакетов, выбираемый системой по умолчанию, может быть связан не с той сетью, из которой они реально ожидаются. В этом случае программа может явно указать IP-адрес того интерфейса, через который данный сокет должен принимать групповые пакеты. Как и в поле IMR_MultiAddr, в поле IMR_Interface задействованы только поля sin_familу и sin_addr, а остальные поля игнорируются.
Для прекращения членства сокета в группе служит та же функция setsockopt, но с параметром IP_DROP_MEMBERSHIP. Через параметр optval при этом также передается структура ip_mreq, значимые поля которой должны быть заполнены так же, как и при добавлении данного сокета в данную группу. Несмотря на то, что структура ip_mreq относится к WinSock 1, в модуле WinSock ее описание отсутствует. Константы IP_ADD_MEMBERSHIP и IP_DROP_MEMBERSHIP в этом модуле объявлены, но работать с ними следует с осторожностью, потому что они должны иметь разные значения в WinSock 1 и WinSock 2. В WinSock 1 они должны иметь значения 5 и 6 соответственно, а в WinSock 2 — 12 и 13. Из-за этого нужно внимательно следить, чтобы значения соответствовали той библиотеке, из которой импортируется функция setsockopt: 5 и 6 — для WSock32.dll и 12 и 13 — для WS2_32.dll.
В WinSock 2 для присоединения сокета к группе объявлена функция WSAJoinLeaf, прототип которой приведен в листинге 2.80.
Листинг 2.80. Функция WSAJoinLeaf
// ***** описание на C++ *****
SOCKET WSAJoinLeaf(SOCKET s, const struct sockaddr FAR *name, int namelen, LPWSABUF lpCallerData, LPWSABUF lpCalleeData, LPQOS lpSQOS, LPQOS lpGQOS, DWORD dwFlags);
// ***** описание на Delphi *****
function WSAJoinLeaf(S: TSocket; var Name: TSockAddr; NameLen: Integer; lpCallerData, lpCalleeData: PWSABuf; lpSQOS, lpGQOS: PQOS; dwFlags: DWORD): TSocket;
Параметры lpCallerData и lpCalleeData задают буферы, в которые помещаются данные, передаваемые и получаемые при присоединении к группе. Протокол IP не поддерживает передачу таких данных, поэтому при его использовании эти параметры должны быть равны nil. Параметры lpSQOS и lpGQOS относятся к качеству обслуживания, которое мы здесь не рассматриваем, поэтому их мы тоже полагаем равными nil.
Параметр S определяет сокет, который присоединяется к группе, Name — адрес группы, NameLen — размер буфера с адресом. Параметр dfFlags определяет, будет ли сокет служить для отправки данных (JL_SENDER_ONLY, 1), для получения данных (JL_RECEIVER_ONLY, 2) или и для отправки, и для получения (JL_BOTH, 4).
Функция возвращает сокет, который создан для взаимодействия с группой. В протоколах типа ATM подключение к группе похоже на установление связи в TCP, и функция WSAJoinLeaf, подобно функции accept, создаёт новый сокет, подключенный к группе. В случае UDP новый сокет не создается, и функция WSAJoinLeaf возвращает значение переданного ей параметра S.
Номер порта в параметре Name игнорируется. Для получения групповых сообщений используется тот интерфейс, который система назначает для этого по умолчанию.
Чтобы прекратить членство сокета в группе, в которую он был добавлен с помощью WSAJoinLeaf, нужно закрыть его посредством функции closesocket. Если сокет, для которого вызывается функция WSAJoinLeaf, находится в асинхронном режиме, то при успешном присоединении сокета к группе возникнет событие FD_CONNECT (в [3] написано, что в одноранговых плоскостях управления FD_CONNECT не возникает — это не соответствует действительности). Но в случае ненадежного протокола UDP возникновение этого события говорит лишь о том, что было отправлено IGMP-сообщение, извещающее о включении сокета в группу (это сообщение должны получить все маршрутизаторы сети, чтобы потом правильно передавать групповые сообщения в другие подсети). Однако FD_CONNECT не гарантирует, что это сообщение успешно принято всеми маршрутизаторами.
UDP-сокет, присоединившийся к многоадресной группе, не должен "подключаться" к какому-либо адресу с помощью функции connect или WSAConnect. Соответственно, для отправки данных такой сокет может использовать только sendto и WSASendTo. Сокет, присоединившийся к группе, может отправлять данные на любой адрес, но если используется поддержка качества обслуживания, она работает только при отправке данных на групповой адрес сокета. Отправка данных на групповой адрес не требует присоединения к группе, причем для сокета, отправляющего данные, нет никакой разницы между отправкой данных на обычный адрес и на групповой. И в том и в другом случае используется функция sendto или WSASendto (или sendWSASend с предварительным вызовом connect). Никаких дополнительных действий для отправки данных на групповой адрес выполнять не требуется. Порт при этом также указывается. Как мы уже видели, номер порта при добавлении сокета в группу не указывается, но сам сокет перед этим должен быть привязан к какому-либо порту. При отправке группового сообщения его получат только те сокеты, входящие в группу, чей порт привязки совпадает с портом, указанным в адресе назначения сообщения.
Если сокет, отправляющий сообщение на групповой адрес, сам является членом этой группы, он, в зависимости от настроек, может получать или не получать свое сообщение. Это определяется его параметром IP_MULTICAST_LOOP, имеющим тип BOOL. По умолчанию этот параметр равен True — это значит, что сокет будет получать свои собственные сообщения. С помощью функции setsockopt можно изменить значение этого параметра на False, и тогда сокет не будет принимать свои сообщения.
Параметром IP_MULTICAST_LOOP следует пользоваться осторожно, т.к. он не поддерживается в Windows NT 4 и требует Windows 2000 или выше. В Windows 9x/МЕ он тоже не поддерживается (хотя упоминания об этом в MSDN нет).
В разд. 2.1.4 мы говорили, что каждый IP-пакет в своем заголовке имеет целочисленный параметр TTL (Time То Live). Его значение определяет, сколько маршрутизаторов может пройти данный пакет. По умолчанию групповые пакеты имеют TTL, равный 1, т.е. могут распространяться только в пределах непосредственно примыкающих подсетей. Целочисленный параметр сокета IP_MULTICAST_TTL позволяет программе изменить это значение.
У функции WSAJoinLeaf не предусмотрены параметры для задания адреса сетевого интерфейса, через который следует получать групповые сообщения, поэтому всегда используется интерфейс, выбираемый системой для этих целей по умолчанию. Выбрать интерфейс, который система будет назначать по умолчанию, можно с помощью параметра сокета IP_MULTICAST_IF. Этот параметр имеет тип TSockAddr, причем значимыми полями структуры в данном случае являются sin_family и sin_addr, а значение поля sin_port игнорируется.
Значения констант IP_MULTICAST_IF, IP_MULTICAST_TTL и IP_MULTICAST_LOOP также зависят от версии WinSock. В WinSock 1 они должны быть равны 2, и 4, а в WinSock 2 — 9, 10 и 11 соответственно.
2.2.12. Дополнительные функции
В этом разделе мы рассмотрим некоторые функции, относящиеся в WinSock к дополнительным. В WinSock 1 эти функции вместе со всеми остальными экспортируются библиотекой WSock32.dll, а в WinSock 2 они вынесены в отдельную библиотеку MSWSock.dll (в эту же библиотеку вынесены некоторые устаревшие функции типа EnumProtocols).
Начнем мы знакомство с этими функциями с функции WSARecvEx (которая, кстати, является расширенной версией функции recv, а отнюдь не WSARecv, как это можно заключить из ее названия), имеющей следующий прототип:
function WSARecvEx(s: TSocket; var buf; len: Integer; var flags: Integer): Integer;
Видно, что она отличается от обычной функции recv только тем, что флаги передаются через параметр-переменную вместо значения. В функции WSARecvEx этот параметр не только входной, но и выходной; функция может модифицировать его. Ранее мы познакомились с функцией WSARecv, которая также может модифицировать переданные ей флаги, но условия, при которых эти две функции модифицируют флаги, различаются.
При использовании TCP (а также любого другого потокового протокола) флаги не изменяются функцией, и результат работы WSARecvEx эквивалентен результату работы recv.
Как мы уже не раз говорили, дейтаграмма UDP должна быть прочитана из буфера сокета целиком. Если в буфере, переданном функции recv или recvfrom, недостаточно места для получения дейтаграммы, эти функции завершаются с ошибкой. При этом в буфер помещается та часть дейтаграммы, которая может в нем поместиться, а оставшаяся часть дейтаграммы теряется. Функция WSARecvEx отличается от recv только тем, что в случае, когда размер буфера меньше размера дейтаграммы, она завершается без ошибки (возвращая при этом размер прочитанной части дейтаграммы, т.е. размер буфера) и добавляет флаг MSG_PARTIAL к параметру flags. Остаток дейтаграммы при этом также теряется. Таким образом, WSARecvEx дает альтернативный способ проверки того, что дейтаграмма не поместилась в буфер, и в некоторых случаях этот способ может оказаться удобным.
Если при вызове функции WSARecvEx флаг MSG_PARTIAL установлен программой, но дейтаграмма поместилась в буфер целиком, функция сбрасывает этот флаг.
В описании функции WSARecvEx в MSDN можно прочитать, что если дейтаграмма прочитана частично, то следующий вызов функции позволит прочитать оставшуюся часть дейтаграммы. Это не относится к протоколу UDP и справедливо только по отношению к протоколам типа SPX, в которых одна дейтаграмма может разбиваться на несколько сетевых пакетов и потому возможна ситуация, когда в буфере сокета окажется только часть дейтаграммы. В UDP, напомним, дейтаграмма всегда посылается одним IP-пакетом и помещается в буфер сразу целиком.
Функция WSARecvEx не позволяет программе определить, с какого адреса прислана дейтаграмма, а аналога функции recvfrom с такими же возможностями в WinSock нет.
Мы уже упоминали о том, что в WinSock 1 существует перекрытый ввод-вывод, но только для систем линии NT. Также в WinSock 1 определена функция AcceptEx, которая является более мощным эквивалентом функции accept, и позволяет принимать входящие соединения в режиме перекрытого ввода-вывода. В WinSock 1 эта функция не поддерживается в Windows 95, в WinSock 2 она доступна во всех системах. Листинг 2.81 содержит ее прототип.
Листинг 2.81. Функция AcceptEx
function AcceptEx(sListenSocket, sAcceptSocket: TSocket; lpOutputBuffer: Pointer; dwReceiveDataLength: DWORD; dwLocalAddressLength: DWORD; dwRemoteAddressLength: DWORD; var lpdwBytesReceived: DWORD; lpOverlapped: POverlapped): BOOL;
Функция AcceptEx позволяет принять новое подключение со стороны клиента и сразу же получить от него первую порцию данных. Функция работает только в режиме перекрытого ввода-вывода.
Параметр sListenSocket определяет сокет, который должен находиться в режиме ожидания подключения. Параметр sAcceptSocket — сокет, через который будет осуществляться связь с подключившимся клиентом. Напомним, что функции accept и WSAAccept сами создают новый сокет. При использовании же AcceptEx программа должна заранее создать сокет и, не привязывая его к адресу, передать в качестве параметра sAcceptSocket. Параметр lpOutputBufer задает указатель на буфер, в который будут помещены, во-первых, данные, присланные клиентом, а во-вторых, адреса подключившегося клиента и адрес, к которому привязывается сокет sAcceptSocket. Параметр dwReceiveDataLength задает число байтов в буфере, зарезервированных для данных, присланных клиентом, dwLocalAddressLength — для адреса привязки сокета sAcceptSocket, dwRemoteAddressLength — адреса подключившегося клиента. Если параметр dwReceiveDataLength равен нулю, функция не ждет, пока клиент пришлет данные, и считает операцию завершившейся сразу после подключения клиента, как функция accept. Для адресов нужно резервировать как минимум на 16 байтов больше места, чем реально требуется. Так как размер структуры TSockAddr составляет 16 байтов, на каждый из адресов требуется зарезервировать как минимум 32 байта. Параметр lpdwBytesReceived используется функцией, чтобы вернуть количество байтов, присланных клиентом.
Параметр lpOverlapped указывает на запись TOverlapped, определенную в модуле Windows следующим образом (листинг 2.82).
Листинг 2.82. Тип TOverlapped
POverlapped = TOverlapped;
_OVERLAPPED = record
Internal: DWORD;
InternalHigh: DWORD;
Offset: DWORD;
OffsetHigh: DWORD;
hEvent: THandle;
end;
TOverlapped = _OVERLAPPED;
Структура TOverlapped используется, в основном, для перекрытого ввода-вывода в файловых операциях. Видно, что она отличается от уже знакомой нам структуры TWSAOverlapped (см. листинг 2.69) только типом параметра hEvent — THandle вместо TWSAEvent. Впрочем, ранее мы уже обсуждали, что TWSAEvent — это синоним THandle, так что можно сказать, что эти структуры идентичны (но компилятор подходит к этому вопросу формально и считает их разными).
Параметр lpOverlapped функции AcceptEx не может быть равным -1, а его поле hEvent должно указывать на корректное событие. Процедуры завершения не предусмотрены. Если на момент вызова функции клиент уже подключился и прислал первую порцию данных (или место для данных в буфере не зарезервировано), AcceptEx возвращает True. Если же клиент еще не подключился, или подключился, но не прислал данные, функция AcceptEx возвращает False, а WSAGetLastError — ERROR_IO_PENDING. Параметр lpBytesReceived в этом случае остается без изменений.
Проконтролировать состояние операции можно с помощью функции GetOverlappedResult, которая является аналогом известной нам функции WSAGetOverlappedResult, за исключением того, что использует запись TOverlapped вместо TWSAOverlapped и не предусматривает передачу флагов. С ее помощью можно узнать, завершилась ли операция, а также дождаться ее завершения и узнать, сколько байтов прислано клиентом (функция AcceptEx не ждет, пока клиент заполнит весь буфер, предназначенный для него — для завершения операции подключения достаточно первого пакета).
Если к серверу подключаются некорректно работающие клиенты, которые не присылают данные после подключения, операция может не завершаться очень долго, что будет мешать подключению новых клиентов. MSDN рекомендует при ожидании время от времени с помощью функции getsockopt для сокета sAcceptSocket узнавать значение целочисленного параметра SO_CONNECT_TIME уровня SOL_SOCKET. Этот параметр показывает время в секундах, прошедшее с момента подключения клиента к данному сокету (или -1, если подключения не было). Если подключившийся клиент слишком долго не присылает данных, сокет sAcceptSocket следует закрыть, что приведет к завершению операции, начатой AcceptEx, с ошибкой. После этого можно снова вызывать AcceptEx для приема новых клиентов.
Функция AcceptEx реализована таким образом, чтобы обеспечивать максимальную скорость подключения. Ранее мы говорили, что сокеты, созданные функциями accept и WSAAccept, наследуют параметры слушающего сокета (например, свойства асинхронного режима). Для повышения производительности сокет sAcceptSocket по умолчанию не получает свойств сокета sListenSocket. Но он может унаследовать их после завершения операции с помощью следующей установки параметра сокета SO_UPDATE_ACCEPT_CONTEXT:
setsockopt(sAcceptSocket, SOL_SOCKET, SO_UPDATE_ACCEPT_CONTEXT, PChar(@sListenSocket), SizeOf(sListenSocket));
Ha сокет sAcceptedSocket после его подключения к клиенту накладываются ограничения: он может использоваться не во всех функциях WinSock, а только в следующих: send, WSASend, recv, WSARecv, ReadFile, WriteFile, TransmitFile, closesocket и setsockopt, причем в последней — только для установки параметра SO_UPDATE_ACCEPT_CONTEXT.
В WinSock не документируется, в какую именно часть буфера помещаются адрес клиента и принявшего его сокета. Вместо этого предоставляется функция GetAcceptExSockAddrs, прототип которой приведен в листинге 2.83.
Листинг 2.83. Функция GetAcceptExSockAddrs
procedure GetAcceptExSockAddrs(lpOutputBuffer: Pointer; dwReceiveDataLength: DWORD; dwLocalAddressLength: DWORD; dwRemoteAddressLength: DWORD; var LocalSockaddr: PSockAddr; var LocalSockaddrLength: Integer; var RemoteSockaddr: PSockAddr; var RemoteSockaddrLength: Integer);
Примечание
В Delphi до 7-й версии включительно модуль WinSock содержит ошибку — параметры LocalSockaddr и RemoteSockaddr функции GetAcceptExSockAddrs имеют в нем тип TSockAddr вместо PSockAddr. Из-за этой ошибки функцию GetAcceptExSockAddrs в этих версиях Delphi необходимо самостоятельно импортировать. Следует заметить, что во многих модулях для WinSock 2 от независимых разработчиков объявление этой функции скопировано из стандартного модуля вместе с ошибкой.
Первые четыре параметра функции GetAcceptExSockAddrs определяют буфер, в котором в результате вызова AcceptEx оказались данные от клиента и адреса, и размеры частей буфера, зарезервированных для данных и для адресов. Значения этих параметров должны совпадать со значениями аналогичных параметров в соответствующем вызове AcceptEx. Через параметр LocalSockaddrs возвращается указатель на то место в буфере, в котором хранится адрес привязки сокета sAcceptSocket, а через параметр LocalSockaddrsLength — длина адреса (16 в случае TCP). Адрес клиента и его длина возвращаются через параметры RemoteSockaddrs и RemoteSockaddrsLength. Следует особенно подчеркнуть, что указатели LocalSockaddrs и RemoteSockaddrs указывают именно на соответствующие части буфера: память для них специально не выделяется и, следовательно, не должна освобождаться, а свою актуальность они теряют при освобождении буфера.
Последняя из дополнительных функций, TransmitFile, служит для передачи файлов по сети. Ее прототип приведен в листинге 2.84.
Листинг 2.84. Функция TransmitFile
function TransmitFile(hSocket: TSocket; hFile: THandle; nNumberOfBytesToWrite, nNumberOfBytesPerSend: DWORD; lpOverlapped: POverlapped; lpTransmitBuffers: PTransmitFileBuffers; dwReserved: DWORD): BOOL;
Функция TransmitFile отправляет содержимое указанного файла через указанный сокет. При этом допускаются только протоколы, поддерживающие соединение, т.е. использовать данную функцию с UDP-сокетом нельзя. Сокет задается параметром hSocket, файл — параметром hFile. Дескриптор файла обычно получается с помощью функции стандартного API CreateFile. Файл рекомендуется открывать с флагом FILE_FLAG_SEQUENTIAL_SCAN, т.к. это повышает производительность.
Параметр nNumberOfBytesToWrite определяет, сколько байтов должно быть передано (позволяя, тем самым, передавать не весь файл, а только его часть). Если этот параметр равен нулю, передается весь файл.
Функция TransmitFile кладет данные из файла в буфер сокета по частям. Параметр nNumberOfBytesPerSend определяет размер одной порции данных. Он может быть равен нулю — в этом случае система сама определяет размер порции. Этот параметр критичен только в случае дейтаграммных протоколов, потому что при этом размер порции определяет размер дейтаграммы. Для TCP данные, хранящиеся в буфере, передаются в сеть целиком или по частям в зависимости от загрузки сети, готовности принимающей стороны и т.п., а какими порциями они попали в буфер, на размер пакета почти не влияет. Поэтому для TCP-сокета параметр nNumberOfBytesPerSend лучше установить равным нулю.
Параметр lpOverlapped указывает на запись TOverlapped, использующуюся для перекрытого ввода-вывода. Эту структуру мы обсуждали при описании функции AcceptEx. В отличие от AcceptEx, в TransmitFile этот параметр добыть равным nil, и тогда операция передачи файла не будет перекрытой.
Если параметр lpOverlapped равен nil, передача файла начинается с той позиции, на которую указывает файловый указатель (для только что открытого файла этот указатель указывает на его начало, а переместить его можно, например, с помощью функции SetFilePointer; также он перемещается при чтении файла с помощью ReadFile). Если же параметр lpOverlapped задан, то передача файла начинается с позиции, заданной значениями полей Offset и OffsetHigh, которые должны содержать соответственно младшую и старшую часть 64-битного смещения стартовой позиции от начала файла.
Параметр lpTransmitBuffers является указателем на запись TTransmitFileBuffers, объявленную так, как показано в листинге 2.85.
Листинг 2.85. Тип TTransmitFileBuffers
PTransmitFileBuffers = ^TTransmitFileBuffers;
_TRANSMIT_FILE_BUFFERS = record
Head: Pointer;
HeadLength: DWORD;
Tail: Pointer;
TailLength: DWORD;
end;
TTransmitFileBuffers = _TRANSMIT_FILE_BUFFERS;
С ее помощью можно указывать буферы, содержащие данные, которые должны быть отправлены перед передачей самого файла и после него. Поле Head содержит указатель на буфер, содержащий данные, предназначенные для отправки перед файлом, HeadLength — размер этих данных. Аналогично Tail и TailLength определяют начало и длину буфера с данными, которые передаются после передачи файла. Если передача дополнительных данных не нужна, параметр lpTransmitBuffer может быть равен nil.
Допускается и обратная ситуация: параметр hFile может быть равен нулю, тогда передаются только данные, определяемые параметром lpTransmitBuffer.
Последний параметр функции TransmitFile в модуле WinSock имеет имя Reserved. В WinSock 1 он и в самом деле был зарезервирован и не имел смысла, но в WinSock 2 через него передаются флаги, управляющие операцией передачи файла. Мы не будем приводить здесь полный список возможных флагов (он есть в MSDN), а ограничимся лишь самыми важными. Указание флага TF_USE_DEFAULT_WORKER или TF_USE_SYSTEM_THREAD позволяет повысить производительность при передаче больших файлов, a TF_USE_KERNEL_APC — при передаче маленьких файлов. Вообще, при работе с функцией TransmitFile чтение файла и передачу данных в сеть осуществляет ядро операционной системы, что приводит к повышению быстродействия по сравнению с использованием ReadFile и send самой программой.
Функция TransmitFile реализована по-разному в серверных версиях Windows NT/2000 и в остальных системах: в серверных версиях она оптимизирована по быстродействию, а в остальных — по количеству необходимых ресурсов.
Данные, переданные функцией TransmitFile, удаленная сторона должна принимать обычным образом, с помощью функций recv/WSARecv.