, которые встречаются здесь, можно было бы заменить одноимёнными функциями из модуля
Math
. Но этот модуль входит не во все варианты поставки Delphi, и чтобы пример можно было откомпилировать в любом варианте поставки Delphi, мы описали их здесь самостоятельно (листинг 2.27).
Листинг 2.27. Функции
Ceil
и
Min
// Функция Ceil возвращает наименьшее целое число X, удовлетворяющее
// неравенству X >= А / В
function Ceil(A, B: Integer): Integer;
begin
Result := A div B;
if A mod В <> 0 then Inc(Result);
end;
// Функция Min возвращает меньшее из двух чисел
function Min(А, В: Integer): Integer;
begin
if A < В then Result := A
else Result := B;
end;
Получившийся сервер более устойчив к DoS-атакам, чем написанный ранее многонитевой сервер. Так как он обходится одной нитью, планировщик задач не перегружается при большом числе подключившихся клиентов. DoS-атака заставляет расходовать только ресурсы библиотеки сокетов и процессорное время, причем вредный эффект последнего легко уменьшить, установив процессу сервера низкий приоритет.
Однако сервер имеет другую уязвимость, связанную с возможным отступлением от протокола обмена клиентом (случайным или злонамеренным). Если клиент, например, пришлет всего один байт и на этом остановится, не разрывая связь с сервером, то при попытке получить сообщение от такого клиента сервер окажется заблокированным, т.к. будет ожидать как минимум четырех байтов (длина строки). Это полностью парализует работу сервера, потому что его единственная нить окажется заблокированной, и обрабатывать сообщения от других клиентов он не сможет.
Примечание
Многонитевой сервер в этом отношении надежнее: некорректное сообщение клиента заблокирует только ту нить, которая
взаимодействует с этим клиентом, никак не влияя на остальные нити, работающие с другими клиентами.
Сделать сервер более устойчивым к некорректным действиям клиента можно, если каждый раз читать ровно столько байтов, сколько пришло. Это усложнит сервер, т.к. придется между "сеансами связи с клиентом" помнить сколько байтов было прочитано в прошлый раз. Однако это поможет полностью избежать блокировок при операциях чтения, что существенно повысит надежность сервера. В этом разделе мы не будем рассматривать соответствующий пример, а реализуем эту возможность в следующем сервере, использующем неблокирующие сокеты. В сервере на основе
select
это делается совершенно аналогично.
2.1.15. Неблокирующий режим
Ранее мы столкнулись с функциями, которые могут надолго приостановить работу вызвавшей их нити, если действие не может быть выполнено немедленно. Это функции
accept
,
recv
,
recvfrom
,
send
,
sendto
и
connect
(в дальнейшем в этом разделе мы не будем упоминать функции
recvfrom
и
sendto
, потому что они в смысле блокирования эквивалентны функциям
recv
и
send
соответственно, и все, что будет здесь сказано о
recv
и
send
, применимо к
recvfrom
и
sendto
). Такое поведение не всегда удобно вызывающей программе, поэтому в библиотеке сокетов предусмотрен особый режим работы сокетов — неблокирующий. Этот режим может быть установлен или отменен дм каждого сокета индивидуально с помощью функции
ioctlsocket
, имеющей следующий прототип:
function ioctlsocket(s: TSocket; cmd: DWORD; var arg: u_long): Integer;
Данная функция предназначена для выполнения нескольких логически мало связанных между собой действий. Возможно, у разработчиков первых версий библиотеки сокетов были причины экономить на количестве функций, потому что мы и дальше увидим, что иногда непохожие операции выполняются одной функцией. Но вернемся к
ioctlsocket
. Ее параметр
cmd
определяет действие, которое выполняет функция, а также смысл параметра
arg
. Допустимы три значения параметра
cmd
:
SIOCATMARK
,
FIONREAD
и
FIONBIO
. При задании
SIOCATMARK
параметр
arg
рассматривается как выходной: в нем возвращается ноль, если во входном буфере сокета имеются высокоприоритетные данные, и ненулевое значение, если таких данных нет (как уже было оговорено, мы в этой книге не будем касаться передачи высокоприоритетных данных).
При
cmd
, равном
FIONREAD
, в параметре
arg
возвращается размер данных, находящихся во входном буфере сокета, в байтах. При использовании TCP это число равно максимальному количеству информации, которое можно получить на данный момент за один вызов
recv
. Для UDP это значение равно суммарному размеру всех находящихся в буфере дейтаграмм (напомним, что прочитать несколько дейтаграмм за один вызов
recv
нельзя). Функция
ioctlsocket
с параметром
FIONREAD
может использоваться для проверки наличия данных с целью избежать вызова recv тогда, когда это может привести к блокированию, или для организации вызова recv в цикле до тех пор, пока из буфера не будет извлечена вся информация.