А. Григорьев - О чём не пишут в книгах по Delphi
- Название:О чём не пишут в книгах по Delphi
- Автор:
- Жанр:
- Издательство:БХВ-Петербург
- Год:2008
- Город:СПб
- ISBN:978-5-9775-019003
- Рейтинг:
- Избранное:Добавить в избранное
-
Отзывы:
-
Ваша оценка:
А. Григорьев - О чём не пишут в книгах по Delphi краткое содержание
Рассмотрены малоосвещённые вопросы программирования в Delphi. Описаны методы интеграции VCL и API. Показаны внутренние механизмы VCL и приведены примеры вмешательства в эти механизмы. Рассмотрено использование сокетов в Delphi: различные механизмы их работы, особенности для протоколов TCP и UDP и др. Большое внимание уделено разбору ситуаций возникновения ошибок и получения неверных результатов в "простом и правильном" коде. Отдельно рассмотрены особенности работы с целыми, вещественными и строковыми типами данных, а также приведены примеры неверных результатов, связанных с ошибками компилятора, VCL и др. Для каждой из таких ситуаций предложены методы решения проблемы. Подробно рассмотрен синтаксический анализ в Delphi на примере арифметических выражений. Многочисленные примеры составлены с учётом различных версий: от Delphi 3 до Delphi 2007. Прилагаемый компакт-диск содержит примеры из книги.
Для программистов
О чём не пишут в книгах по Delphi - читать онлайн бесплатно ознакомительный отрывок
Интервал:
Закладка:
Третий шаг выполняется только после функции select
, т.е. тогда, когда хотя бы один из сокетов находится в состоянии готовности. На этом шаге мы проверяем сокеты, созданные для взаимодействия с клиентами на предыдущих итерациях цикла с помощью функции accept
. Эти сокеты располагаются в массиве сокетов, начиная с элемента с индексом 1. Программа в цикле просматривает все сокеты и, если они находятся в состоянии готовности, выполняет операцию чтения.
На первый взгляд может показаться странным, почему для перебора элементов массива выбран цикл while
, а не for
. Но в дальнейшем мы увидим, что размер массива во время выполнения цикла может изменяться. Особенность же цикла for
заключается в том, что его границы вычисляются один раз и запоминаются в отдельных ячейках памяти, и дальнейшее изменение значений выражений, задающих эти границы, не изменяет эти границы. В нашем примере это приведет к тому, что в случае уменьшения массива цикл for
не остановится на реальной уменьшившейся длине, а продолжит выполнение по уже не существующим элементам, что приведет к трудно предсказуемым последствиям. Поэтому в данном случае предпочтительнее цикл while
, в котором условие продолжения цикла заново вычисляется при каждой его итерации.
Напомним, что функция select
модифицирует переданные ей множества таким образом, что в них остаются лишь сокеты, находящиеся в состоянии готовности. Поэтому чтобы проверить, готов ли конкретный сокет, достаточно с помощью функции FD_ISSET
проверить, входит ли он в множество FDSet
. Если входит, то вызываем для него функцию recv
. Если эта функция возвращает положительное значение, значит, данные в буфере есть, программа их читает и отвечает. Если функция возвращает 0 или -1 ( SOCKET_ERROR
) значит, соединение закрыто или разорвано, и данный сокет больше не может быть использован. Поэтому мы должны освободить связанные с ним ресурсы ( closesocket
) и убрать его из массива сокетов (как раз на этом шаге размер массива уменьшается). При удалении оставшиеся сокеты смещаются на одну позицию влево, поэтому переменную цикла необходимо уменьшить на единицу, иначе следующий сокет будет пропущен.
И наконец, на четвертом шаге мы проверяем состояние готовности исходного сокета, который хранится в нулевом элементе массива. Так как этот сокет находится в режиме ожидания соединения, для него состояние готовности означает, что в очереди соединений появились клиенты, и необходимо вызвать функцию accept
, чтобы создать сокеты для взаимодействия с этими клиентами.
Хотя приведенный пример вполне работоспособен, следует отметить, что это только один из возможных вариантов организации сервера. Так что лучше не относиться к нему как к догме, потому что именно в вашем случае может оказаться предпочтительнее какой-либо другой вариант. Ценность этого примера заключается в том, что он иллюстрирует работу функции select
, а не в том, что он дает готовое решение на все случаи жизни.
2.1.14. Примеры использования функции select
Рассмотрим два практических примера использования функции select
для получения информации о готовности сокета. Оба примера станут развитием рассмотренных ранее.
Сначала модифицируем UDP-чат (см. разд. 2.1.10) таким образом, чтобы он использовал один сокет и для отправки, и для получения сообщений (пример SelectChat на компакт-диске). Вторая нить нам теперь не понадобится, всё будет делать главная форма. Процедуры создания сокета и отправки сообщений изменений не претерпели, главное дополнение — это то, что на форме появился таймер, в обработчике события OnTimer
которого мы будем проверять с помощью select
, пришло ли сообщение для сокета (листинг 2.24). С помощью таких простейших модификаций мы получили чат, который работает без распараллеливания и использует всего один сокет. Работать с таким чатом стало намного проще, потому что теперь ответ нужно посылать на тот же порт, с которого пришло сообщение, а не запоминать, какой порт для отправки какому из экземпляров чата соответствует.
Несмотря на эти изменения, новая версия UDP-чата может обмениваться сообщениями со старой, т.к. протокол обмена остался неизменным.
// Реакция на таймер. С периодичностью, заданной таймером,
// проверяем, не пришли ли сообщения, и если пришли,
// получаем их.
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-атаки, когда злоумышленник посылает большой поток сообщений, чтобы парализовать работу чата. Работа в этом случае, конечно же, будет парализована независимо от того, будет ли в обработчике события таймера извлекаться одно сообщение или все сразу — все равно чат будет замусорен бессмысленными сообщениями. Но в первом случае между показом сообщений будут интервалы, и пользователь хотя бы сможет корректно закрыть программу. Во втором же случае, если злоумышленник посылает сообщения достаточно быстро, цикл может оказаться бесконечным, обработка других оконных сообщений прекратится, и пользователь вынужден будет снять задачу средствами системы. Таким образом, извлечение только одного сообщения за один раз снижает ущерб от атаки. (Разумеется, вряд ли кто-то всерьез захочет атаковать наш учебный пример, но эту возможность следует учитывать при разработке более серьезных приложений.)
Интервал:
Закладка: