Примечание
Несмотря на эти изменения, новая версия UDP-чата может обмениваться сообщениями со старой, т.к. протокол обмена остался неизменным.
Листинг 2.24. Проверка готовности сокетов при обработке сообщения от таймера
// Реакция на таймер. С периодичностью, заданной таймером,
// проверяем, не пришли ли сообщения, и если пришли,
// получаем их.
procedure TChatForm.TimerChatTimer(Sender: TObject);
var
// Множество сокетов для функции select.
// Будет содержать только один сокет FSocket.
SocketSet: TFDSet;
// Тайм-аут для функции select
Timeout: TTimeVal;
// Буфер для получения сообщения.
// Размер равен максимальному размеру UDP-дейтаграммы
Buffer: array[0..65506] of Byte;
Msg: string;
// Адрес, с которого пришло сообщение
RecvAddr: TSockAddr;
RecvLen, AddrLen: Integer;
begin
// Инициализируем множество сокетов,
// т.е. очищаем его от случайного мусора
FD_ZERO(SocketSet);
// Добавляем в это множество сокет FSocket
FD_SET(FSocket, SocketSet);
// Устанавливаем тайм-аут равным нулю, чтобы
// функция select ничего не ждала, а возвращала
// готовность сокетов на момент вызова.
Timeout.tv_sec := 0;
Timeout.tv_usec := 0;
// Проверяем готовность сокета для чтения
if select(0, @SocketSet, nil, nil, @Timout) = SOCKET_ERROR then
begin
AddMessageToLog('Ошибка при проверке готовности сокета: ' + GetErrorString);
Exit;
end;
// Проверяем, оставила ли функция select сокет в множестве.
//Если оставила, значит, во входном буфере сокета есть данные.
if FD_ISSET(FSocket, SocketSet) then
begin
AddrLen := SizeOf(RecvAddr); // Получаем дейтаграмму
RecvLen :=
recvfrom(FSocket, Buffer, SizeOf(Buffer), 0, RecvAddr, AddrLen);
// Так как UDP не поддерживает соединение, ошибку при вызове recvfrom
// мы можем получить, только если случилось что-то совсем
// экстраординарное.
if RecvLen < 0 then
begin
AddMessageToLog('Ошибка при получении сообщения: ' +
GetErrorString);
Exit;
end;
// Устанавливаем нужный размер строки
SetLength(Msg, RecvLen);
// и копируем в неё дейтаграммы из буфера
if RecvLen > 0 then Move(Buffer, Msg[1], RecvLen);
AddMessageToLog('Сообщение с адреса ' + inet_ntoa(RecvAddr.sin_port) +
':' + IntToStr(ntohs(RecvAddr.sin_port)) + ': ' + Msg);
end;
end;
Обратите внимание, что в обработчике события от таймера читается только одно сообщение, хотя за время, прошедшее с предыдущего вызова этого обработчика, в принципе, могло прийти несколько сообщений. Если запустить два экземпляра чата на одном компьютере, и с одного из них послать несколько сообщений подряд другому (добиться этого можно, несколько раз быстро нажав на кнопку Отправить), то адресат получит сообщения последовательно, с полусекундной задержкой между ними. Было бы достаточно просто организовать в обработчике сообщения таймера цикл до тех пор, пока функция select
не покажет, что сокет не готов к чтению, и извлечь за один раз сразу все сообщения, которые накопились в буфере сокета. Этого не сделано, чтобы уменьшить уязвимость чата по отношению к действиям потенциального злоумышленника. Имеется в виду та разновидность DoS-атаки, когда злоумышленник посылает большой поток сообщений, чтобы парализовать работу чата. Работа в этом случае, конечно же, будет парализована независимо от того, будет ли в обработчике события таймера извлекаться одно сообщение или все сразу — все равно чат будет замусорен бессмысленными сообщениями. Но в первом случае между показом сообщений будут интервалы, и пользователь хотя бы сможет корректно закрыть программу. Во втором же случае, если злоумышленник посылает сообщения достаточно быстро, цикл может оказаться бесконечным, обработка других оконных сообщений прекратится, и пользователь вынужден будет снять задачу средствами системы. Таким образом, извлечение только одного сообщения за один раз снижает ущерб от атаки. (Разумеется, вряд ли кто-то всерьез захочет атаковать наш учебный пример, но эту возможность следует учитывать при разработке более серьезных приложений.)
Перейдем к следующему примеру использования select
— TCP-серверу, который может работать одновременно с неограниченным числом клиентов (пример находится на компакт-диске в папке SelectServer). Этот сервер будет усовершенствованной версией нашего простейшего сервера (см. разд. 2.1.12) и тоже будет консольным приложением (функция select
, как мы видели на примере UDP-чата, позволяет создавать приложения с графическим интерфейсом пользователя, так что реализация сервера в качестве консольного приложения — это не необходимость, а свободный выбор для иллюстрации различных способов применения функции select
).
Читать дальше
Конец ознакомительного отрывка
Купить книгу