* При перепечатке материалов ссылка на www.SeoLiga.ru обязательна! RSS



Определение готовности сокета
19 марта 2009

Так как многие функции библиотеки сокетов блокируют вызвавшую их нить, если соответствующая операция не может быть выполнена немедленно, часто бывает полезно заранее знать, готов ли сокет к немедленному (без блокирования) выполнению той или иной операции. Основным средством определения этого в библиотеке сокетов служит функция Select :
function Select(NFds:Integer;ReadFds,WriteFds,ExceptFds:PFDSet;
Timeout:PTimeVal):LongInt;
Первый параметр этой функции оставлен только для совместимости со старыми версиями библиотеки сокетов; в существующих версиях он игнорируется. Три следующих параметра содержат указатели на множества сокетов, состояние которых должно проверяться. В данном случае понятие множества не имеет ничего общего с типом множество в Delphi. В оригинальной версии библиотеки сокетов, написанной на С, определены макросы, позволяющие очищать такие множества, добавлять и удалять сокеты и определять, входит ли тот или иной сокет в множество. В модуле WinSock эти макросы заменены одноимёнными процедурами и функциями:
//Удаляет сокет 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);
При создании переменной типа FDSet в той области памяти, которую она занимает, могут находиться произвольные данные, являющиеся, по сути дела, мусором. Из-за этого мусора функции 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 - значение 500000. Параметр Timeout функции Select должен содержать указатель на заполненную подобным образом структуру, определяющую, сколько времени функция будет ожидать, пока хотя бы один из сокетов не будет готов к требуемой операции. Если этот указатель равен nil, ожидание будет бесконечным.
Мы потратили достаточно много времени, выясняя структуру параметров функции Select. Теперь, наконец-то, мы можем перейти к описанию того, зачем она нужна и какой смысл несёт каждый из её параметров.
Функция Select позволяет дождаться, когда хотя бы один из сокетов, переданный хотя бы в одном из множеств, будет готов к выполнению той или иной операции. Какой именно операции, определяется тем, в какое из трёх множеств входит сокет. Для сокетов, входящих в множество ReadFds, готовность означает, что функции Recv или RecvFrom будут выполнены без задержки. При использовании UDP это означает, что во входном буфере сокета есть данные, которые можно прочитать. При использовании TCP функции Recv и RecvFrom могут быть выполнены без задержки ещё в двух случаях: когда партнёр закрыл соединение (в этом случае функции вернут 0), и когда соединение некорректно разорвано (в этом случае функции вернут Socket_Error). Кроме того, если сокет, включённый в множество ReadFds, находится в состоянии ожидания соединения (в которое он переведён с помощью функции Listen), то для него состояние готовности означает, что очередь соединений не пуста и функция Accept будет выполнена без задержек.
Для сокетов, входящих в множество WriteFds, готовность означает, что сокет соединён, а в его выходном буфере есть свободное место. (До сих пор мы обсуждали только блокирующие сокеты, для которых успешное завершение функции Connect автоматически означает, что сокет соединён. Далее мы познакомимся с неблокирующими сокетами, для которых нужно использовать Select, чтобы понять, установлено ли соединение.) Наличие свободного места в буфере не гарантирует, что функции Send или SendTo не будут блокировать вызвавшую их нить, т.к. программа может попытаться передать больший объём информации, чем размер свободного места в буфере на момент вызова функции. В этом случае функции Send и SendTo вернут управление вызвавшей их нити только после того, как часть данных будет отправлена, и в буфере сокета освободится достаточно места.
Следует отметить, что большинство протоколов обмена устроено таким образом, что при их реализации проблема переполнения выходного буфера практически никогда не возникает. Чаще всего клиент и сервер обмениваются небольшими пакетами, причём сервер посылает клиенту только ответы на его запросы, а клиент не посылает новый запрос до тех пор, пока не получит ответ на предыдущий. В этом случае гарантируется, что пакеты будут уходить из выходного буфера быстрее (или, по крайней мере, не медленнее), чем программа будет их туда помещать. Поэтому заботиться о том, чтобы в выходном буфере было место, приходится достаточно редко.
И, наконец, последнее множество, ExceptFds. Для сокетов, входящих в это множество, состояние готовности означает, что либо не удалась попытка соединения для неблокирующего сокета, либо получены высокоприоритетные данные (out-of-band data). В этой статье мы не будем детально рассматривать отправку и получение высокоприоритетных данных. Те, кому это понадобится, легко разберутся с этим по MSDN'у.
Функция Select возвращает общее количество сокетов, находящихся в состоянии готовности. Если функция завершила работу по таймауту, возвращается 0. Множества ReadFds, WriteFds и ExceptFds модифицируются функцией: в них остаются только те сокеты, которые находятся в состоянии готовности. При вызове функции любые два из этих трёх указателей могут быть равны nil, если программу не интересует готовность сокетов по соответствующим критериям. Один и тот же сокет может входить в несколько множеств.
Ниже приведён пример кода TCP-сервера, взаимодействующего с несколькими клиентами в рамках одной нити и работающего по простой схеме запрос-ответ:
var Sockets:array of TSocket;
Addr:TSockAddr;
Data:TWSAData;
Len,I,J:Integer;
FDSet:TFDSet;
begin
WSAStartup($101,Data);
SetLength(Sockets,1);
Sockets[0]:=Socket(AF_Inet,Sock_Stream,0);
Addr.sin_family:=AF_Inet;
Addr.sin_port:=HToNS(5514);
Addr.sin_addr.S_addr:=InAddr_Any;
FillChar(Addr.Sin_Zero,SizeOf(Addr.Sin_Zero),0);
Bind(Sockets[0],Addr,SizeOf(TSockAddr));
Listen(Sockets[0],SoMaxConn);
while True do
begin
// 1. Формирование множества сокетов
FD_Zero(FDSet);
for I:=0 to High(Sockets) do
FD_Set(Sockets[I],FDSet);
// 2. Проверка готовности сокетов
Select(0,@FDSet,nil,nil,nil);
// 3. Чтение запросов клиентов тех сокетов, которые готовы к этому
I:=1;
while I<=High(Sockets) do
begin
if FD_IsSet(Sockets[I],FDSet) then
if Recv(Sockets[I],…)<=0 then
begin
// Связь разорвана, надо закрыть сокет
// и удалить его из массива
CloseSocket(Sockets[I]);
for J:=I to High(Sockets)-1 do
Sockets[J]:=Sockets[J+1];
Dec(I);
SetLength(Sockets,Length(Sockets)-1)
end
else
begin
// Поучены данные от клиента, надо ответить
Send(Sockets[I],…)
end;
Inc(I)
end;
// 4. Проверка подключения нового клиента
if FD_IsSet(Sockets[0],FDSet) then
begin
// Подключился новый клиент
SetLength(Sockets,Length(Sockets)+1);
Len:=SizeOf(TSockAddr);
Sockets[High(Sockets)]:=Accept(Sockets[0],@Addr,@Len)
end
end;
Как и предыдущих примерах, код для краткости не содержит проверок успешности завершения функций. Ещё раз напоминаю, что в реальном коде такие проверки необходимы.
Теперь разберём программу по шагам. Создание сокета, привязка к адресу и перевод в режим ожидания подключений вам уже знакомы, поэтому мы на них останавливаться не будем. Отмечу только, что вместо переменной типа TSocket мы используем динамический массив этого типа, длина которого сначала устанавливается равной одному элементу, и этот единственный элемент и содержит дескриптор созданного сокета. В дальнейшем мы будем добавлять в этот массив сокеты, создающиеся в результате выполнения функции Accept. После перевода сокета в режим ожидания подключения начинается бесконечный цикл, состоящий их четырёх шагов.
На первом шаге цикла создаётся множество сокетов, в которое добавляются все сокеты, содержащиеся в массиве. В этом месте в примере пропущена важная проверка того, что сокетов в массиве не больше 64-ёх. Если их будет больше, то попытки добавить лишние сокеты в множество будут проигнорированы функцией FD_Set и, соответственно, эти сокеты выпадут из дальнейшего рассмотрения, т.е. даже если клиент что-то пришлёт, сервер этого не увидит. Решить проблему можно тремя способами. Самый простой - это отказывать в подключении лишним клиентам. Для этого сразу после вызова Accept надо вызывать для нового сокета CloseSocket. Второй способ - это увеличение количества сокетов в множестве, как это описано выше. В этом случае всё равно остаётся та же проблема, хотя если сделать число сокетов в множестве достаточно большим, она практически исчезает. И, наконец, можно разделить сокеты на несколько порций, для каждой из которых вызывать Select отдельно. Это потребует усложнения примера, потому что сейчас в функции Select мы используем бесконечное ожидание. При разбиении сокетов на порции это может привести к тому, что из-за отсутствия готовых сокетов в первой порции программа не сможет перейти к проверке второй порции, в которой готовые сокеты, может быть, есть.
Для создания множества оно сначала очищается, а потом в него в цикле добавляются сокеты. Для любителей красивых решений могу предложить существенно более быстрый способ формирования множества, при котором не нужно использовать ни циклов, ни FD_Zero, ни FD_Set:
Move((PChar(Sockets)-4)^,FDSet,
Length(Sockets)*SizeOf(TSocket)+SizeOf(Integer));
Почему такая конструкция будет работать, предлагаю разобраться самостоятельно, изучив по справке Delphi, как хранятся в памяти динамические массивы, а по MSDN'у - структуру типа FDSET. Тем же, кто по каким-то причинам не захочет разбираться, настоятельно рекомендую никогда и ни при каких обстоятельствах не использовать такую конструкцию, потому что в неумелых руках она превращается в мину замедленного действия, из-за которой ошибки могут появиться в самых неожиданных местах программы.
Второй шаг - это собственно выполнение ожидания готовности сокетов с помощью функции Select. Готовность к записи и к чтению высокоприоритетной информации нас в данном случае не интересует, поэтому мы ограничиваемся заданием множества ReadFds. В нашем простом примере не должно выполняться никаких действий, если ни один сокет не готов, поэтому последний параметр тоже равен nil, что означает ожидание, не ограниченное таймаутом.
Третий шаг выполняется только после функции Select, т.е. тогда, когда хотя бы один из сокетов находится в состоянии готовности. На этом шаге мы проверяем сокеты, созданные для взаимодействия с клиентами на предыдущих итерациях цикла с помощью функции Accept. Эти сокеты располагаются в массиве сокетов, начиная с элемента с индексом 1. Программа в цикле просматривает все сокеты и, если они находятся в состоянии готовности, выполняет операцию чтения.
На первый взгляд может показаться странным, почему для перебора элементов массива используется цикл while, а не for. Но в дальнейшем мы увидим, что размер массива во время выполнения цикла может изменяться. Особенность же цикла for заключается в том, что его границы вычисляются один раз и запоминаются в отдельных ячейках памяти, и дальнейшее изменение значений выражений, задающих эти границы, не изменяет эти границы. В нашем примере это приведёт к тому, что в случае уменьшения массива цикл for не остановится на реальной уменьшившейся длине, а продолжит цикл по уже не существующим элементам, что приведёт к трудно предсказуемым последствиям. Поэтому в данном случае лучше использовать цикл while, в котором условие продолжения цикла полностью вычисляется при каждой его итерации.
Напомню, что функция Select модифицирует переданные ей множества таким образом, что в них остаются лишь сокеты, находящиеся в состоянии готовности. Поэтому чтобы проверить, готов ли конкретный сокет, достаточно с помощью функции FD_IsSet проверить, входит ли он в множество FDSet. Если входит, то вызываем для него функцию Recv. Если эта функция возвращает положительное значение, значит, данные в буфере есть, программа их читает и отвечает. Если функция возвращает 0 или -1 (Socket_Error), значит, соединение закрыто или разорвано, и данный сокет больше не может быть использован. Поэтому мы должны освободить связанные с ним ресурсы (CloseSocket) и убрать его из массива сокетов (как раз на этом шаге размер массива уменьшается). При удалении оставшиеся сокеты смещаются на одну позицию влево, поэтому переменную цикла необходимо уменьшить на единицу, иначе следующий сокет будет пропущен.
И, наконец, на четвёртом шаге мы проверяем состояние готовности исходного сокета, который хранится в нулевом элементе массива. Так как этот сокет находится в режиме ожидания соединения, для него состояние готовности означает, что в очереди соединений появились клиенты, и надо вызвать функцию Accept, чтобы создать сокеты для взаимодействия с этими клиентами.
Хотя приведённый пример вполне работоспособен, хочу отметить, что это только один из возможных вариантов организации сервера. Рекомендую не относиться к нему как к догме, потому что именно в вашем случае может оказаться предпочтительнее какой-либо другой вариант. Ценность этого примера заключается в том, что он иллюстрирует работу функции Select, а не в том, что он даёт готовое решение на все случаи жизни.


Теги: asus socket, IP, borland delphi, User Datagram Protocol, Unix, SQL-сервер Borland Delphi

Статьи по теме:

Панель View
Офисные ППП
Свойство PaperSize
FilePath
Создание групп
Компоненты
Свойство Designer
Событие OnEndPage
Свойство ShowProgress
Панель Align
Разрешение и запрет
Свойство ForceNewColumn
Свойство PixelsPerY
Модель СОМ
Характеристика и назначение информационных технологий обработки данных
| Borland Delphi | Alex |
 


Пн Вт Ср Чт Пт Сб Вс
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31


     



Rambler's Top100

Данный сайт или домен продается ICQ: 403-353-727

© 2009 Seoliga.ru | Borland Delphi | Определение готовности сокета. Регион сайта: Москва и Санкт-Петербург