Если на момент вызова функции accept
очередь соединений пуста, то нить, вызвавшая ее, блокируется до тех пор, пока какой-либо клиент не подключится к серверу. С одной стороны, это удобно: сервер может не вызывать функцию accept
в цикле до тех пор, пока она не завершится успехом, а вызвать ее один раз и ждать, когда подключится клиент. С другой стороны, это создает проблемы тем серверам, которые должны взаимодействовать с несколькими клиентами. Действительно, пусть функция accept
успешно завершилась и в распоряжении программы оказались два сокета: находящийся в режиме ожидания новых подключений и созданный для обслуживания уже существующего подключения. Если вызвать accept, то программа не сможет продолжить работу до тех пор, пока не подключится еще один клиент, а это может произойти через очень длительный промежуток времени или вообще никогда не случится. Из-за этого программа не сможет обрабатывать вызовы уже подключившегося клиента. С другой стороны, если функцию acсept
не вызывать, сервер не сможет обнаружить подключение новых клиентов. Средства для решения этой проблемы есть как у стандартных сокетов, так и у сокетов Windows, и далее мы их рассмотрим. Но существует довольно популярный способ ее решения средствами не библиотеки сокетов, а операционной системы. Он заключается в использовании отдельной нити для обслуживания каждого из клиентов. Каждый раз, когда клиент подключается, функция accept
передает управление программе, возвращая новый сокет. Здесь сервер может породить новую нить, которая предназначена исключительно для обмена данными с новым клиентом. Старая нить после этого снова вызывает accept для старого сокета, а новая — функции recv
и send
для нового сокета. Такой метод решает заодно и проблемы, связанные с тем, что функции send
и recv
также могут блокировать работу программы и помешать обмену данными с другими клиентами. В данном случае будет блокирована только одна нить, обменивающаяся данными с одним из клиентов, а остальные нити продолжат свою работу. Далее мы рассмотрим пример сервера, работающего по такой схеме.
Рис. 2.3. Последовательность действий клиента и сервера при использовании TCP
То, что функция recv
может возвратить только часть ожидаемого пакета, обычно вызывает трудности, поэтому здесь мы рассмотрим один из вариантов написания функции (назовем ее ReadFromSocket
), которая эти проблемы решает (листинг 2.13). Суть этой функции в том, что она вызывает recv до тех пор, пока не будет получено требуемое количество байтов или пока не возникнет ошибка. Тот код, который получает и анализирует приходящие данные, использует уже не recv
, a ReadFromSocket
, которая гарантирует, что будет возвращено столько байтов, сколько требуется.
ReadFromSocket
, читающая из буфера сокета заданное количество байтов// Функция читает Cnt байтов в буфер Buffer из сокета S
// Учитывается, что может потребоваться несколько операций чтения,
// прежде чем будет прочитано нужное число байтов.
// Возвращает:
// 1 — в случае успешного чтения
// 0 — в случае корректного закрытия соединения удаленной стороной
// -1 — в случае ошибки чтения
function ReadFromSocket(S: TSocket; var Buffer; Cnt: Integer): Integer;
var
Res, Total: Integer;
begin
// Total содержит количество принятых байтов
Total:= 0;
// Читаем байты в цикле до тех пор, пока не будет прочитано Cnt байтов
repeat
// На каждой итерации цикла нам нужно прочитать
// не более чем Cnt — Total байтов, т. е. не более
// чем нужное количество минус то, что уже прочитано
// на предыдущих итерациях. Очередную порцию данных
// помещаем в буфер со смещением Total.
Res:= recv(S, (PChar(@Buffer) + Total)^, Cnt — Total, 0);
if Res = 0 then
begin
// Соединение закрыто удаленной стороной
Result:= 0;
Exit;
end;
if Res < 0 then
begin
// Произошла ошибка при чтении
Result:= -1;
Exit;
end;
Inc(Total, Res);
until Total >= Cnt;
Result:= 1;
end;