// но вероятность блокирования на долгое время мы оцениваем как
// крайне низкую, т.к. оставшаяся часть сообщения, скорее всего,
// придет достаточно быстро, и поэтому идем на такой риск.
for I := FD_SETSIZE * J to Min(FD_SETSIZE * (J + 1) - 1, Connections.Count - 1) do
if FD_ISSET(PConnection(Connections[I])^.ClientSocket, SockSet) then
ProcessSocketMessage(PConnection(Connections[I])^);
end;
// Проверяем поле Deleted у всех соединений. Те, у которых
// оно равно True, закрываем: закрываем сокет, освобождаем память,
// удаляем указатель из списка. Цикл идет с конца списка к началу,
// потому что в ходе работы цикла верхняя граница списка
// может меняться, и цикл for снизу вверх мог бы привести
// к появлению индексов вне диапазона.
for I := Connections.Count - 1 downto 0 do
if PConnection(Connections[I])^.Deleted then
begin
closesocket(PConnection(Connections[I])^.ClientSocket);
Dispose(PConnection(Connections[I]));
Connections.Delete(I);
end;
Sleep(100);
until False;
Функции Ceil
и Min
, которые встречаются здесь, можно было бы заменить одноимёнными функциями из модуля Math
. Но этот модуль входит не во все варианты поставки Delphi, и чтобы пример можно было откомпилировать в любом варианте поставки Delphi, мы описали их здесь самостоятельно (листинг 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. Неблокирующий режим