А. Григорьев - О чём не пишут в книгах по Delphi
// режим и делаем сокет блокирующим.
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