с предыдущими примерами: к каждому сообщению он добавляет адрес клиента, чтобы пользователь мог видеть, с каким именно из одновременно подключившихся клиентов связано сообщение. Что же касается кода
Execute
, то видно, что он практически не отличается от кода внутреннего цикла простейшего сервера (см. листинг 2.15). Это неудивительно — сообщение здесь читается и обрабатывается единым образом. Вся разница только в том, что теперь у нас одновременно могут работать несколько таких нитей, обеспечивая одновременную работу сервера с несколькими клиентами.
Этот сервер уже можно использовать как образец для подражания. Нужно только помнить, что он тратит на каждого клиента относительно много ресурсов, и поэтому не подходит там, где могут подключаться сотни и более клиентов одновременно. Кроме того, этот сервер очень уязвим по отношению к DoS-атакам, поэтому подобный сервер целесообразен там. где число клиентов относительно невелико, а вероятность DoS-атак низка.
Примечание
DoS-атака (Denied of Service) — способ помешать функционированию сервера, заключающийся в загрузке его бесполезной работой. В простейшем случае — это просто одновременное подключение большого числа клиентов. У нас даже простое подключение большого числа клиентов приводит к большому расходу системных ресурсов, поэтому DoS-атакой можно добиться неработоспособности не только самого сервера, но и системы в целом. Полностью защититься от DoS-атаки невозможно, но можно снизить урон, наносимый ею. Об этом мы поговорим далее.
2.1.13. Определение готовности сокета
Так как многие функции библиотеки сокетов блокируют вызвавшую их нить, если соответствующая операция не может быть выполнена немедленно, часто бывает полезно заранее знать, готов ли сокет к немедленному (без блокирования) выполнению той или иной операции. Основным средством определения этого в библиотеке сокетов служит функция
select
:
function select(nfds: Integer; readfds, writefds, exceptfds: PFDSet; timeout: PTimeVal): LongInt;
Первый параметр этой функции оставлен только для совместимости со старыми версиями библиотеки сокетов: в существующих версиях он игнорируется. Три следующих параметра содержат указатели на множества сокетов (эти множества описываются типом
TFDSet
), состояние которых должно проверяться. В данном случае понятие множества не имеет ничего общего с типом множество в Delphi. В оригинальной версии библиотеки сокетов, написанной на C, определены макросы, позволяющие очищать такие множества, добавлять и удалять сокеты и определять, входит ли тот или иной сокет в множество. В модуле WinSock эти макросы заменены одноименными процедурами и функциями (листинг 2.22).
Листинг 2.22. Функции для работы с типом
TFDSet
// Удаляет сокет Socket из множества FDSet.
procedure FD_CLR(Socket: TSocket; var FDSet: TFDSet);
// Определяет, входит ли сокет Socket в множество FDSet.
function FD_ISSET(Socket: TSocket; var FDSet: TFDSet): Boolean;
// Добавляет сокет Socket в множество FDSet.
procedure FD_SET(Socket: TSocket; var FDSet: TFDSet);
// Инициализирует множество FDSet.
procedure FD_ZERO(var FDSet: TFDSet);
При создании
переменной типа
TFDSet
в той области памяти, которую она занимает, могут находиться произвольные данные, являющиеся, по сути дела, "мусором". Из-за этого мусора функции
FD_CLR
,
FD_ISSET
, и
FD_SET
не смогут работать корректно. Процедура
FD_ZERO
очищает мусор, создавая пустое множество. Вызов остальных функций
FD_XXX
до вызова
FD_ZERO
приведёт к непредсказуемым результатам.
Мы намеренно не приводим здесь описание внутренней структуры типа
TFDSet
. С помощью функций
FD_XXX
можно выполнить все необходимые операции с множеством, не зная этой структуры. Отметим, что в Windows и в Unix внутреннее устройство этого типа существенно различается, но благодаря использованию этих функций код остается переносимым.
В Windows максимальное количество сокетов, которое может содержать в себе множество
TFDSet
, определяется значением константы
FD_SETSIZE
. По умолчанию ее значение равно 64. В C/C++ отсутствует раздельная компиляция модулей в том смысле, в котором она существует в Delphi, поэтому модуль в этих языках может поменять значение константы
FD_SETSIZE
перед включением заголовочного файла библиотеки сокетов, и это изменение приведёт к изменению внутренней структуры типа
TFDSet
(точнее, типа
FDSet
— в C/C++ он называется так). К счастью, в Delphi модули надежно защищены от подобного влияния друг на друга, поэтому как бы мы ни переопределяли константу FD
_SETSIZE
в своем модуле, на модуле WinSock это никак не отразится. В Delphi приходится прибегать к другому способу изменения количества сокетов в множестве: для этого следует определить свой тип, эквивалентный по структуре
TFDSet
, но резервирующий иное количество памяти для хранения сокетов (структуру
TFDSet
можно узнать из исходного кода модуля WinSock). В функцию
select
можно передавать указатели на структуры нового типа, необходимо только приведение типов указателей. А вот существующие функции
FD_XXX
, к сожалению, не смогут работать с новой структурой, потому что компилятор требует строгого соответствия типов для параметров-переменных. Но, опять же, при необходимости очень легко создать аналоги этих функций для своей структуры.
Примечание
На первый взгляд может показаться, что Delphi в данном случае хуже, чем C/C++. Но достаточно хотя бы раз столкнуться с ошибкой, вызванной взаимным влиянием макроопределений в модулях C/C++, чтобы понять, что уж лучше написать несколько лишних строк кода, лишь бы никогда больше не иметь таких проблем.
Последний параметр функции
select
содержит указатель на структуру
TTimeVal
, которая описывается следующим образом:
TTimeVal = record
tv_sec: LongInt;
tv_usec: LongInt;
end;
Эта структура служит для задания времени ожидания. Поле
tv_sec
содержит число полных секунд в этом интервале, поле
tv_usec
— число микросекунд. Так, чтобы задать интервал ожидания, равный 1,5 с, нужно присвоить полю
tv_sec
значение 1, а полю
tv_usec
— значение 500 000. Параметр
timeout
функции
select
должен содержать указатель на заполненную подобным образом структуру, определяющую, сколько времени функция будет ожидать, пока хотя бы один из сокетов не будет готов к требуемой операции. Если этот указатель равен