Чтение онлайн

ЖАНРЫ

О чём не пишут в книгах по Delphi

Григорьев Антон Борисович

Шрифт:

function accept(s: TSocket; addr: PSockAddr; addrlen: PInteger) : TSocket;

Параметр

s
задает сокет, который находится в режиме ожидания соединения и из очереди которого извлекается информация о соединении. Выходной параметр
addr
позволяет получить адрес клиента, установившего соединение. Здесь должен быть передан указатель на буфер, в который этот адрес будет помещен. Параметр
addrlen
содержит указатель на переменную, в которой хранится длина этого буфера: до вызова функции эта переменная должна содержать фактическую длину буфера, задаваемого параметром
addr
, после вызова — количество байтов буфера, реально понадобившихся для хранения адреса клиента. Очевидно, что в случае TCP и входное, и выходное значение этой переменной должно быть равно
SizeOf(TSockAddr)
. Эти параметры передаются как указатели, а не как параметры-переменные,
что было бы более естественно для Delphi, потому что библиотека сокетов допускает для этих указателей нулевые значения, если сервер не интересует адрес клиента. В данном случае разработчики модуля WinSock сохранили полную функциональность, предоставляемую библиотекой.

В случае ошибки функция

accept
возвращает значение
INVALID_SOCKET
. При успешном завершении возвращается дескриптор сокета. созданного библиотекой сокетов и предназначенного для обслуживания данного соединения. Этот сокет уже привязан к адресу и соединен с сокетом клиента, установившего соединение, и его можно использовать в функциях
recv
и
send
без предварительного вызова каких-либо других функций. Уничтожается этот сокет обычным образом, с помощью
closesocket
.

Исходный сокет, определяемый параметром

s
, остается в режиме прослушивания. Если сервер поддерживает одновременное соединение с несколькими клиентами, то функция
accept
может быть вызвана многократно. Каждый раз при этом будет создаваться новый сокет, обслуживающий одно конкретное соединение: протокол TCP и библиотека сокетов гарантируют, что данные, посланные клиентами, попадут в буферы соответствующих сокетов и не будут перемешаны.

Для получения целостной картины кратко повторим все сказанное. Для установления соединения сервер должен, во-первых, создать сокет с помощью функции

socket
, а во-вторых, привязать его к адресу с помощью функции
bind
. Далее сокет должен быть переведен в режим ожидания с помощью
listen
, а потом с помощью функции
accept
создается новый сокет, обслуживающий соединение, установленное клиентом. После этого сервер может обмениваться данными с клиентом. Клиент же должен создать сокет, при необходимости привязки к конкретному порту вызвать
bind
, и затем вызвать
connect
для установления соединения. После успешного завершения этой функции клиент может обмениваться данными с сервером. Это иллюстрируют листинги 2.11 и 2.12.

Листинг 2.11. Код сервера

var

 S, AcceptedSock: TSocket;

 Addr: TSockAddr;

 Data: TWSAData;

 Len: Integer;

begin

 WSAStartup($101, Data);

 S := socket(AF_INET, SOCK_SТREAМ, 0);

 Addr.sin_family := FF_INET;

 Addr.sin_port := htons(3030);

 Addr.sin_addr.S_addr := INADDR_ANY;

 FillChar(Addr.sin_zero, SizeOf(Addr.sin_zero), 0);

 bind(S, Addr, SizeOf(TSockAddr));

 listen(S, SOMAXCONN);

 Len := SizeOf(TSockAddr);

 AcceptedSock := accept(S, @Addr, @Len);

 {

Теперь Addr содержит адрес клиента, с которым установлено соединение, а AcceptedSock - дескриптор, обслуживающий это соединение. Допустимы следующие действия:

send(AcceptedSock, ...) - отправить данные клиенту

recv(AcceptedSock, ...) - получить данные от клиента

accept(...) - установить соединение с новым клиентом

 }

Здесь сокет сервера привязывается к порту с номером 3030. В общем случае разработчик сервера сам должен выбрать порт из диапазона 1024–65 535.

Листинг 2.12. Код клиента

var

 S: TSocket;

 Addr: TSockAddr;

 Data: TWSAData;

begin

 WSAStartup($101, Data);

 S := socket(AF_INET, SOCK_STREAM, 0);

 Addr.sin_family := AF_INET;

 Addr.sin_port := htons(3030);

 Addr.sin_addr.S_addr := inet_addr(...);

 FillChar(Addr.sin_zero, SizeOf(Addr.sin_zero), 0);

 connect(S, Addr, SizeOf(TSockAddr));

 {

Теперь соединение установлено. Допустимы следующие действия:

send(S, ...) - отправить данные серверу

recv(S, ...) — получить данные от сервера

 }

В приведенном коде для краткости опущены проверки результатов функций с целью обнаружения ошибок. При написании серьезных программ этим пренебрегать нельзя. Блок-схема действии клиента и сервера приведена на рис. 2.3.

Если на момент вызова функции

accept
очередь соединений пуста, то нить, вызвавшая ее, блокируется до тех пор, пока какой-либо клиент не подключится к серверу. С одной стороны, это удобно: сервер может не вызывать функцию
accept
в цикле до тех пор, пока она не завершится успехом, а вызвать ее один раз и ждать, когда подключится клиент. С другой стороны, это создает проблемы тем серверам, которые должны взаимодействовать с несколькими клиентами. Действительно, пусть функция
accept
успешно завершилась и в распоряжении программы оказались два сокета: находящийся в режиме ожидания новых подключений и созданный для обслуживания уже существующего подключения. Если вызвать accept, то программа не сможет продолжить работу до тех пор, пока не подключится еще один клиент, а это может произойти через очень длительный промежуток времени или вообще никогда не случится. Из-за этого программа не сможет обрабатывать вызовы уже подключившегося клиента. С другой стороны, если функцию
acсept
не вызывать, сервер не сможет обнаружить подключение новых клиентов. Средства для решения этой проблемы есть как у стандартных сокетов, так и у сокетов Windows, и далее мы их рассмотрим. Но существует довольно популярный способ ее решения средствами не библиотеки сокетов, а операционной системы. Он заключается в использовании отдельной нити для обслуживания каждого из клиентов. Каждый раз, когда клиент подключается, функция
accept
передает управление программе, возвращая новый сокет. Здесь сервер может породить новую нить, которая предназначена исключительно для обмена данными с новым клиентом. Старая нить после этого снова вызывает accept для старого сокета, а новая — функции
recv
и
send
для нового сокета. Такой метод решает заодно и проблемы, связанные с тем, что функции
send
и
recv
также могут блокировать работу программы и помешать обмену данными с другими клиентами. В данном случае будет блокирована только одна нить, обменивающаяся данными с одним из клиентов, а остальные нити продолжат свою работу. Далее мы рассмотрим пример сервера, работающего по такой схеме.

Рис. 2.3. Последовательность действий клиента и сервера при использовании TCP

То, что функция

recv
может возвратить только часть ожидаемого пакета, обычно вызывает трудности, поэтому здесь мы рассмотрим один из вариантов написания функции (назовем ее
ReadFromSocket
), которая эти проблемы решает (листинг 2.13). Суть этой функции в том, что она вызывает recv до тех пор, пока не будет получено требуемое количество байтов или пока не возникнет ошибка. Тот код, который получает и анализирует приходящие данные, использует уже не
recv
, a
ReadFromSocket
, которая гарантирует, что будет возвращено столько байтов, сколько требуется.

Листинг 2.13. Функция
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

// На каждой итерации цикла нам нужно прочитать

Поделиться с друзьями: