Добрый день.
Понадобился редкий и небольшой обмен данными по локальной сети между двумя программами. Почитал книгу Антона Григорьева и решил пойти "легким путем", используя TUdpSocket с вкладки "Internet". Для проверки использовал localhost (как "localhost" и как 127.0.0.1 при отключенной сети). Компонент успешно передает пакеты, снифер их получает, но порт приема (LocalHost:LocalPort) остается свободным. Пробовал передавать и принимать на один порт, запускать две копии программы, отправлять UDP-пакеты своей программе снифером - безуспешно, ничего получить не удается т.к. порт не прослушивается (к нему можно подключить снифер).
С теми же настройками в любой комбинации Indy TIdUDPServer работает сразу (при первом же запуске встроенный в виндовс фаервол запросил разрешения), но он использует только блокирующие сокеты. Использую D7, W7x32.
Подскажите, как настроить TUdpSocket для прослушивания заданного порта. Что я делаю неправильно?
Уважаемые авторы вопросов! Большая просьба сообщить о результатах решения проблемы на этой странице. Иначе, следящие за обсуждением, возможно имеющие аналогичные проблемы, не получают ясного представления об их решении. А авторы ответов не получают обратной связи. Что можно расценивать, как проявление неуважения к отвечающим от автора вопроса.
21-12-2024 02:29 | Комментарий к предыдущим ответам
для Python
>>>На мой взгляд, должно быть верно следующее правило: ...найти компонент, который решает мою задачу
Применение компонентов - это сплошные компромиссы. Между теми задачами, которые мы решаем в прикладном ПО и тем инструментарием (в том числе компонентами), которым пользуемся. Между использованием готового и конструированием нового. Между выбираемыми нами подходами: использованием, доработкой, переработкой, созданием подобного но специализированного или созданием нового. Всё зависит от "постановки задачи" и масштабов её решения (области охвата, отведённых ресурсов, штучности использования и т.д.).
Здесь же, в этой ветке обсуждений, мы пытаемся ответить автору, на частный, вполне определённый и практический вопрос: "как использовать компонент UdpSocket". Уточню. Именно определённый компонент, а не вообще сетевые компоненты. Именно подход КОП, а не преимущества КОП vs ООП. Именно использовать и доработать, а не заменить другим или отказаться от его использования. Кроме того, автор "Почитал книгу Антона Григорьева". Поэтому его, видимо интересует, не только "что использовать" для организации сетевого взаимодействия, но, и как использовать, и "сам механизм" такого взаимодействия.
Долго ли, коротко ли, в этой ветке (поскольку не нашёл способа, как опубликовать статью на этом сайте), мной приводится некоторое конструктивное, частное решение вопроса, заданного автором. Ну и попутно, поясняю некоторые типовые задачи и удивляюсь "конформизму" разработчиков компонента UdpSocket.
17-12-2024 06:27 | Комментарий к предыдущим ответам
На мой взгляд, должно быть верно следующее правило: "Если компонент не выполняет мою задачу, то он должен быть выброшен, а затем переход к п.1 (найти компонент, который решает мою задачу)".
5.6. UDP сервер и дополнительные потоки.
———————————————————————————
Если посмотреть на результаты нашей работы, то можно сказать, что к этому моменту, у нас уже есть исправленный, вполне работоспособный компонент, который может принимать и отправлять сообщения. С помощью компонентов UdpSocketExt мы можем организовать между ними по сети обмен сообщениями. Но можно ли назвать один из компонентов "сервером", а другой "клиентом", если оба компонента одинаковые? Для ответа на этот вопрос, попробуем прояснить что мы называем сервером, а что клиентом.
1) Вообще термином "сервер" принято называть тот сетевой элемент, который постоянно ожидает запросы и затем отвечает на них. А "клиентом" того, кто инициирует запросы. Применительно к сетевому устройству или программе можно сказать, что "сервер" или «клиент» — это та роль, которую они играют, по отношению к другим таким же, постоянно или время от времени. Причём определить такую роль, можно только в контексте взаимодействия.
При использовании протокола UDP, сказать является ли сокет клиентом или сервером, можно лишь условно. В любом из этих случаев, и создание, и "привязка к адресу" сокета происходит одинаково, с помощью одних и тех же функций. Поскольку постоянного канала соединения не устанавливается, при использовании в работе, возможны случаи, когда оба попеременно, как отсылают, так и получают сообщения.
В отношении использования компонентов, ситуация та же. Можно сказать, что если в настоящий момент компонент прослушивает сокет, то это сервер, а если нет - то клиент. Другими словами, для компонента, работающего с протоколом UDP, сервер и клиент, это лишь разные режимы его обычной работы, или разные роли, в которых он выступает. При использовании "блокирующего режима", про разделение по ролям, можно сказать более определённо. В "блокирующем режиме", при прослушивании порта, происходит блокировка той же нити, в которой находится сокет. И сокет, в это время, уже не может использоваться ни в каких других операциях предачи данных. Поэтому, для этого случая можно сказать, что "сервер" это тот, который постоянно прослушивает сокет.
С помощью компонента UdpSocketExt, в прикладной программе, вполне можно организовать такой сервер. Но для этого потребуется разработать и создать отдельный класс потока, в котором работал бы компонент. Например, можно создать класс производный от TThread, создать в нём или передать ему экземпляр UdpSocketExt, а в методе TThread.Execute организовать цикл прослушивания сокета с помощью метода ReceiveFrom. Но такой сервер, можно назвать "программой сервером". А поскольку мы разрабатываем компонент, то нам нужно сделать тоже самое, но на уровне компонентов.
Поэтому поставим перед собой задачу. Разработать (или доработать) компонент UdpSocketExt так, чтобы тот мог считаться сервером. Другими словами, добавить в компонент "дополнительный поток", в котором бы постоянно прослушивался сокет.
2) На этапе анализа, попробуем сэкономить и облегчить нашу работу. В модуле Sockets уже есть компонент TcpServer, по описанию похожий на разрабатываемый нами, использующий много-поточность, но ориентированный на протокол TCP. Возможно нам удастся использовать, либо базовый класс, либо схему взаимодействия классов, либо дополнительные потоковые классы, производные от TThread.
Упрощённая схема работы компонента TcpServer следующая:
-свойство TcpServer.BlockMode устанавливает режим работы сокета. Таких режимов, оказалось не два ("блокирующий" и "не блокирующий"), а три. Добавился режим "блокирующий потоковый";
-в методе TcpServer.Open, если установлен режим "блокирующий потоковый", создаётся "серверный поток" TServerSocketThread(TThread). В него передаётся указатель на TcpServer;
-при создании, "серверный поток" создаёт список кэшируемых "клиентских потоков";
-"серверный поток" в методе Execute, в цикле:
-дожидается входного соединения сокета, с помощью select();
-выбирает из списка кэшируемых подходящий или создаётся новый "клиентский поток" TClientSocketThread(TThread). В него передаётся указатель на "серверный поток". Если соединений больше чем размер кэша, то соединение отвергается;
-"клиентский поток" запускается на исполнение;
-"клиентский поток" в методе Execute:
-создаёт новый TcpClient;
-вызывает метод TcpServer.Accept, по указателю, переданному ему при создании, а в его параметрах передаёт созданный TcpClient;
-в методе TcpServer.Accept:
-создаётся новый сокет с помощью функции WinSock.accept();
-сокет и значения копируются в переданный TcpClient напрямую, в обход свойств;
-вызывается обработчик события TcpServer.onAccept, в который передаётся TcpClient. Это событие происходит в контексте "клиентского потока". Предполагается, что вся обработка полученных данных произойдёт в этом обработчике;
-новый TcpClient уничтожается, новый сокет закрывается;
-поток приостанавливается.
-при вызове TcpServer.Close, "серверный поток" завершается.
В компоненте TcpServer используется классы: TTcpServer, TCustomTcpServer, TCustomTcpClient, TServerSocketThread TClientSocketThread. Но для нашего компонента, мы не сможем использовать ни один из них. Поскольку все они ориентированы на схему подключения, принятую при использовании TCP протокола и ориентированную на соединение. Для нашего случая, использующего протокол UDP, такая схема чересчур сложна. Поскольку наш протокол не ориентирован на соединение, то и отдельного сокета на каждое соединение не потребуется (не используется accept()). Каждое сообщение, всегда передаётся как обособленное, не связанное с предыдущими. Поэтому, нам достаточно всего одного сокета, который будет прослушивать порт, принимать сообщения и возможно, сразу же отвечать на них.
Единственное, что мы можем использовать для нового компонента, это схему создания "серверного потока". В ней, при создании экземпляра "серверного потока", в него передаётся сам TcpServer, а не создаётся новый.
Такой подход здорово нам поможет и упростит новый компонент. Он будет состоять всего из одного UdpSocketExt или его наследника, который при вызове метода Open, будет передаваться в "дополнительный поток". Настройка компонента, производится до вызова метода Open в основном потоке (т.е. до запуска "дополнительного потока"). А получение и отправка сообщений - в обработчике соответствующего события, но уже в "дополнительном потоке". Такой подход и специализация режима работы, как раз соответствуют термину "сервер". А если нам потребуется отправить сообщение из основного потока, то мы всегда можем использовать другой, не прослушивающий сокет, UdpSocketExt.
Для указания, когда предполагается "прослушивать порт", т.е. когда запускать "дополнительный поток", а когда не запускать, можно использовать свойство-признак.
3) Проектирование, начнём с создания нового компонента UdpServerExt, унаследованного от UdpSocketExt. Его отличия от предка будут незначительны. Зато выделение его в обособленный класс позволит сузить область нашей работы, увеличить её выразительность и упростит отладку. Поскольку мы сможем править только ту задачу, которую разрабатываем.
Для удобства отладки создадим, отдельные и производные от TCustomUdpSocketExt, новые классы для компонента UdpServerExt: TCustomUdpServerExt(TCustomUdpSocketExt) и TUdpServerExt(TCustomUdpServerExt). Для "дополнительного потока" создадим класс TUdpServerExtThread(TThread).
В качестве свойства-признака, указывающего, когда "прослушивать порт" и запускать "дополнительный поток", можно использовать свойство Listen логического типа. У нас, в компоненте, принята схема, по которой он запускается в работу методом Open и останавливается методом Close. Поэтому значение свойства Listen будет отображать лишь тот режим, который мы хотим получить после начала работы компонента. Для отражения того "прослушивается ли порт" на самом деле, нам потребуется ещё одно свойство Listening логического типа, но уже "только для чтения".
Данные, полученные в сообщении, будем возвращать через событие OnReceiveListen и метод диспетчеризации DoReceiveListen.
"Дополнительный поток", прослушивающий порт UDP, организуем посредством класса TUdpSocketExtThread, унаследованного от TThread. Поток будет создаваться в методе TCustomUdpServerExt.Open, если только установлено свойство Listen и установлен "блокирующий режим", а завершаться - в Close. Этот потоковый класс содержит всего пару методов: Create и Execute. Метод Execute содержит цикл ожидания данных, выполняемое с помощью функции recvfrom().
unit SocketsExt;
interface
...
type
TCustomUdpServerExt = class;
//"Дополнительный поток", прослушивающий порт UDP, для TCustomUdpServerExt
TUdpSocketExtThread = class(TThread)
private
FServerSocket: TCustomUdpServerExt;
protected
public
constructor Create(aUdpServer: TCustomUdpServerExt);
procedure Execute; override;
property ServerSocket: TCustomUdpServerExt read FServerSocket;
end;
TUdpServerExt = class(TCustomUdpServerExt)
published
...
property Listen;
property OnReceiveListen;
end;
const
cUdpPacketMax = 65506; // 0..$FFE2; наибольший размер пакета данных UDP
type
TUdpPacketByte = array[0..cUdpPacketMax] of Byte;
implementation
...
{ TCustomUdpServerExt }
procedure TCustomUdpServerExt.Close;
begin
if Active and FListening
then begin
FListening := False;
if Assigned(FServerThread)
then FServerThread.Terminate;
FServerThread := nil;
end;
inherited Close;
end;
procedure TCustomUdpServerExt.DoReceiveListen(Buf: PChar; DataLen: integer;
const aHost, aPort: string);
begin
if Assigned(FOnReceiveListen)
then OnReceiveListen(Self, Buf, DataLen, aHost, aPort);
end;
procedure TCustomUdpServerExt.Open;
begin
inherited Open;
if Connected and FListen and (BlockMode = bmBlocking)
then begin
FServerThread := TUdpSocketExtThread.Create(Self);
FListening := True;
FServerThread.Resume;
end;
end;
procedure TUdpSocketExtThread.Execute;
var
nRecvRes, nBufferSize, nAddrLen :integer;
wHost, wPort :string;
wBuffer :TUdpPacketByte;
wAddr : TSockAddr;
begin
wHost := ''; wPort := '';
nBufferSize := SizeOf(wBuffer);
FillChar(wAddr.sin_zero, SizeOf(wAddr.sin_zero), 0);
while not Terminated and Assigned(FServerSocket) do
begin
nAddrLen := SizeOf(wAddr);
nRecvRes := WinSock.recvfrom(FServerSocket.Handle, wBuffer, nBufferSize,
0, wAddr, nAddrLen);
if not Terminated and Assigned(FServerSocket)
then begin
FServerSocket.ErrorCheck(nRecvRes);
if nRecvRes <> SOCKET_ERROR
then begin
FServerSocket.DoReceive(PChar(@wBuffer), nRecvRes); //увеличит BytesReceived
SocketAddrToNames(wAddr, wHost, wPort);
FServerSocket.DoReceiveListen(PChar(@wBuffer), nRecvRes, wHost, wPort);
end;
end;
end;
end;
end.
4) Для испытания компонента, используем такую же, как и в предыдущем разделе, программу. Отличием, будет лишь замена компонента UdpSocketExt на UdpServerExt, добавление переключателя CheckBox для установки значения свойства Listen и протоколирование события onReceiveListen. Кроме того, нужно учесть, что это событие будет возникать в контексте "дополнительного потока". Поэтому, для передачи полученного сообщения в "основной поток", создадим ещё один поток. И уже с помощью него и его метода Synchronize, передадим данные.
Действительно, разработав и испытав новый компонент UdpServerExt, можно отметить, что при значительном внутреннем отличии, единственным существенным внешним отличием от предка UdpSocketExt, является свойство Listen. Более, того, как мы и говорили в начале этого раздела, новый компонент UdpServerExt, лишь добавляет к UdpSocketExt новый, удобный в использовании, режим работы. Но по сути, и тот, и другой компонент, могут выступать, и в роли сервера, и в роли клиента. И как упоминалось ранее, новый компонент, был создан лишь для удобства разработки. Этим мы выделили и обособили частную задачу из общих задач, решаемых компонентом, в тех границах, в которых производили разработку. Но разработка задачи "создания дополнительного потока" завершена. Поэтому, для удобства использования, мы сольём компоненты UdpServerExt и UdpSocketExt в едино. И дальше будем полагать, что у нас есть только один, общий компонент UdpSocketExt.
Вообще, здесь мы столкнулись с принципиальным недостатком ООП. В ООП невозможно связать или обозначить отдельную частную задачу (одну из всех решаемых классом) и перечень части функциональности класса (т.е. методов и свойств), которые она использует. Или наоборот, обозначить какие методы и свойства, к какой частной задаче относятся. Поэтому классы получаются достаточно сложными, а их декомпозиция на группы методов и свойств, для объяснения из каких же задач они (классы) состоят - невнятной.
В противоположность предыдущему разделу, проведённая здесь работа кажется настолько важной, что её хочется назвать итогом. И можно было бы на этом завершить наш рассказ. Но по сути, мы выполнили лишь один очередной шаг. Ведь здесь мы добавили лишь одно свойство и решили только одну частную задачу, упрощающую использование компонента.
Так например, до сих пор мы старательно обходили стороной "не блокирующий" режим работы сокета. Кроме того, событие OnReceiveListen происходит в контексте "дополнительного потока", а мы пока толком не объяснили, как передать полученные в нём данные сообщения в основной поток.
5.5. Попытка пятая. Добавляем недостающие методы передачи данных.
——————————————————————————————————————-
Поскольку мы разрабатываем простейший, но удобный в практическом применении компонент, то некоторое внимание нужно уделить разработке методов компонента, предназначенных для передачи данных. Они также должны быть просты и удобны в использовании.
Анализировать, то какие существуют методы передачи сообщений и какие из них мы будем использовать, нам не потребуется. Ведь мы только дорабатываем и улучшаем существующий компонент UdoSocket. Поэтому, посмотрим какие методы, относящиеся к передаче данных в нём есть и доработаем их для нового режима работы компонента UdoSocketExt.
Для передачи сообщений с помощью сокетов предназначены всего несколько функций библиотеки сокетов: send, recv, sendto и recvfrom. Из них первые две, используются только в том случае, когда включён "фильтр адресатов". В качестве аргументов, они используют указатель на нетипизированный буфер. А вторые - когда "фильтр адресатов" выключен. Поэтому в их параметрах обязательно указание адресата в виде структуры TSockAddr.
В исходном компоненте UdpSocket, есть несколько методов, являющихся обёртками этих функций библиотеки сокетов (ReceiveBuf, SendBuf, ReceiveFrom, SendTo). Эти методы передают сообщение в виде "нетипизированного буфера" в одном сетевом пакете (датаграмме) Udp. Кроме того, в UdpSocket есть несколько методов упрощающих передачу данных:
-SendLn, ReceiveLn - передают сообщение типа строка, заканчивающееся определённой подстрокой. По умолчанию, завершающая подстрока состоит из символов завершения строки (#13#10). Если сообщение крупное, то оно может передаваться в нескольких сетевых пакетах;
-SendStream - передаёт сообщение типа поток данных. Если сообщение крупное, то оно может передаваться в нескольких сетевых пакетах.
В новом компоненте UdpSocketExt, сохраняться все методы передачи данных доставшиеся от UdpSocket. Они будут работать при включённом "фильтре адресатов" (ConnectMode=True). А для режима работы при выключенном "фильтре адресатов", нам необходимо создать новы методы, похожие на существующие. В отличии от старых, в новых методах, в качестве параметра, должен быть указан "адресат". Значение "адресата" будем задавать двумя строками Host и Port. Названия новых методов сделаем похожи на существующие:
-SendLnTo, ReceiveLnFrom - передают сообщение типа строка, заканчивающееся определённой подстрокой. По умолчанию, завершающая подстрока состоит из символов завершения строки (#13#10). Если сообщение крупное, то оно может передаваться в нескольких сетевых пакетах. От предшествующих методов отличаются указанием в параметрах "адресата";
-SendStreamTo, ReceiveStreamFrom - передаёт сообщение типа поток данных. Если сообщение крупное, то оно может передаваться в нескольких сетевых пакетах. От предшествующих, эти методы отличаются указанием в параметрах "адресата" и размера потока. Предполагается, что размер "потока данных" будет передан отдельно;
-SendBufTo, ReceiveBufFrom - методы аналогичные старым ReceiveBuf, SendBuf. От предшествующих, отличаются тем, что значение "адресата" передаётся двумя строками Host и Port.
Новые методы, по возможности, будут разработаны так, чтобы, и по описанию, и по формату передаваемых сообщений были совместимы с унаследованными. Например, сообщение, отправленное SendStreamTo или SendLnTo, может быть принято не только ReceiveStreamFrom, но и ReceiveLn, ReceiveLnFrom и ReceiveBufFrom. Размер сетевого пакета, для передачи крупного сообщения по частям, устанавливается равным 512 байт (как и в модуле Sockets). Поскольку мы разрабатываем простой компонент и используем протокол UDP, то нужно учесть, что методы, использующие для передачи несколько сетевых пакетов (XxxLnXxx, XxxStreamXxx), будут работать лишь номинально. Это обусловлено тем, что протокол UDP не гарантирует, ни доставку датаграммы, ни очерёдность их следования. Кроме того, пакеты от разных адресатов могут следовать вперемешку. Тем не менее, в локальной сети, при одиночной передаче сообщений, эти методы будут вполне работоспособны.
В компоненте UdpSocket, свойства BytesReceived и BytesSent отображают сколько данных получено и отослано с начала работы сокета. Установка значения BytesReceived происходит в обработчике события DoReceive, а BytesSent в методе SendBuf. При получении и отправке сообщений используются события onReceived и OnSend.
В новом компоненте UdpSocketExt, нам также придётся использовать события при получении и отправке сообщений. Во-первых, это единственный способ изменить значения свойств BytesReceived и BytesSent в новых методах передачи сообщений. Во-вторых, при получении сообщения, нужно как-то возвращать значение адресата. В-третьих, поскольку некоторые методы передачи сообщений передают данные несколькими пакетами, то нужно отследить передачу, и отдельного пакета (с этим вполне справляются унаследованные события onReceive и OnSend), и всей передачи в целом. События назовём по аналогии с такими же в унаследованном компоненте. Событие OnReceiveFrom будет возникать при получении сообщений, а OnSendTo - при отправке, с указанием адресата.
В старом компоненте UdpSocket, в методах передачи данных, обнаружены небольшие капканчики, которые перекочевали в новый. Их рекомендуется обходить стороной. Метод SendStream - в качестве результата всегда возвращает 0. SendLn - посылает строку в одном пакете, размер которого возможен более 512 байт, а ReceiveLn принимает сообщение в несколько пакетов, но в буфер 512 байт. Метод ReceiveFrom не возвращает адрес отправителя.
После добавления методов передачи данных, модуль SocketsExt будет выглядеть так:
TCustomUdpSocketExt = class(TIpSocket)
private
...
FOnReceiveFrom: TSocketDataAddrEvent;
FOnSendTo: TSocketDataAddrEvent;
protected
...
procedure DoReceiveFrom( Buf: PChar; DataLen: integer;
const aHost, aPort :string); virtual;
procedure DoSendTo( Buf: PChar; DataLen: integer;
const aHost, aPort :string); virtual;
public
...
//—Функции для передачи данных, с указанием адресата Host, Port :string
function SendBufTo(var aBuf; aBufSize: integer;
const aToHost, aToPort :string; aFlags: integer = 0): integer;
function SendLnTo(aText :string;
const aToHost, aToPort :string; aFlags: integer = 0;
const aEol: string = CRLF): integer;
function SendStreamTo(aStream: TStream;
const aToHost, aToPort :string; aFlags: integer = 0): integer;
function PeekBufFrom(var aBuf; aBufSize: integer;
var aFromAddr: TSockAddr; var aFromAddrLen :integer) :integer;
function ReceiveBufFrom(var aBuf; var aBufSize: integer;
var aFromHost, aFromPort :string;
aFlags: integer = 0): integer;
function ReceiveLnFrom( var aFromHost, aFromPort :string;
const aEol: string = CRLF): string;
function ReceiveStreamFrom(aStream :TStream; aStreamLen :integer;
var aFromHost, aFromPort :string): integer;
//событие возникает при приёме сообщения функциями ReceiveXXXFrom
property OnReceiveFrom: TSocketDataAddrEvent read FOnReceiveFrom write FOnReceiveFrom;
//событие возникает при отправке сообщения функциями SendXXXTo
property OnSendTo: TSocketDataAddrEvent read FOnSendTo write FOnSendTo;
end;
TUdpSocketExt = class(TCustomUdpSocketExt)
published
...
property OnReceiveFrom;
property OnSendTo;
end;
Содержания методов передачи данных, в основном, аналогичны таковым в модуле Sockets. Чтобы не загромождать текст, исходный код этих методов, приведём позже, в одной из следующих разделов.
Тестовая программа, которая проверит работу добавленных методов передачи данных, наверное, будет здесь самая сложная. Она состоит из формы, на которую поместим кнопки, каждая из которых будет отвечать за отдельный метод передачи данных. Результаты работы удобнее всего выводить в текстовом виде в Memo. Задачей у испытательной программы несколько. Во-первых, проверить совместную работу каждой соответствующей пары методов передачи данных. Например, SendLnTo и ReceiveFromLn. Во-вторых, проверить совместную работу всех методов отправки, со всеми методами получения сообщений. Сделать это удобно с помощью небольшого строкового сообщения (которое проверит элементарную работоспособность) и текстового файла, размером в пару килобайт (сообщение с содержанием которого проверит работу методов, использующих несколько пакетов). В-третьих, к компоненту нужно подключить все доступные ему обработчики событий. В них можно вставить лишь добавление строки в Memo, информирующей о событии. Это позволит проследить динамику (последовательность выполнения операций) работы компонента. Хотя, разработанный нами компонент и позволяет встроить его в "дополнительный поток", прослушивающий порт, но для наглядности его работы, программу оставим однопоточной.
Для удобства использования компонента UdpSocketExt можно создать "пакет" для установки в IDE Delphi. Для этого в Delphi создаём новый "пакет" (File->New->Other...->Package). Устанавливаем тип пакета "Designtime and runtime", название "Internet Components Ext" и сохраняем с именем dclInetExt.dpk. В содержимое пакета добавляем файл компонента SocketsExt.pas и файл регистрации в IDE SocketsExtReg.pas. В файл SocketsExt.res можно добавить ресурс иконки компонента (с именем TUDPSOCKETEXT и типом BITMAP). В файл SocketsExtReg.pas добавляем следующее содержание.
unit SocketsExtReg;
interface
procedure Register;
implementation
{$R SocketsExt.res}
uses
Classes,
SocketsExt;
// Процедура регистрации компонента в IDE Delphi на вкладке Internet
procedure Register;
begin
RegisterComponents('Internet', [TUdpSocketExt]);
end;
end.
Работа, проделанная нами в этом разделе, в общем-то кажется не такой важной, по сравнению с работой по проектированию и созданию компонента, проделанной в предыдущих разделах. И на первый взгляд, покажется очевидным, что её можно пропустить. На самом деле, мы выполнили один из важнейших этапов, цикла разработки. Мы на практическом опыте выяснили и наглядно показали, как работает компонент (с помощью протоколирования событий). Опробовали несколько сценариев передачи данных, которые позволили нам уточнить зачем нужен компонент, в каких случаях он может быть использован, а в каких нет. Другими словами, мы получили практически опыт, на основании которого можно сделать выводы о том, как проводить дальнейшую разработку компонента, и о рекомендациях по его практическому использованиию.
Кроме того, с помощью испытательной программы и пакета компонента мы попытались выявить полезность нашей работы. Т.е. попытались показать и объяснить для чего именно, как и что использовать. Ведь полезность компонента определяется той пользой, которую он приносит, не просто фактом своего существования, а решая те или иные задачи в прикладной программе.
5.4. Попытка четвёртая. Исправляем работу компонента.
————————————————————————————-
В предыдущем разделе мы разработали вполне работоспособный компонент UdpSocketExt. В нём можно указать определённый номер порта, к которому будет привязан сокет. Правда, у нового компонента работает только половина методов по передаче сообщений. Зато в исходном компоненте UdpSocket, нельзя указать номер порта "привязки адреса", но у него работают все методы по передаче сообщений. Хотя с некоторыми странностями и несуразностями.
Попытаемся добавить в наш компонент то, что исправит его работу.
То, что часть методов по передаче сообщений в новом компоненте перестала работать, обусловлено тем, каким способом мы выполняем "привязку адреса" сокета. Она выполняется в методе Open. Часть методов работают при использовании функции connect(), а часть - при bind(). Можно сказать, что компонент UdpSocket работает в "режиме клиента", а UdpSocketExt - в "режиме сервера". Очевидно, что для того, чтобы в новом компоненте совместить два режима работы, нужно найти способ как-то переключать эти режимы. Это можно сделать множеством способов.
Во-первых, можно сделать два отдельных компонента, унаследованных от TIpSocket. Назвать их UdpClient и UdpServer. А в методе Open, каждого из компонент реализовать свой способ "привязки адреса", определяющий режим работы. Но в этом случае, у нас ничего нового не получится и работу компонента мы не исправим. Да и компоненты, в основном, будут копировать реализацию методов друг друга. Потому что, один из компонентов - копия старого UdpSocket, а второй - копия нового UdpSocketExt. Видимо, ровно по этому пути шли разработчики компонента из Borland. А поскольку, второй из компонентов явно не соответствует своему наименованию "сервер", то они решили оставить лишь один из них, но вот с таким, неопределённым названием.
Во-вторых, можно сделать два отдельных компонента, но с одним предком UdpCustomSocketExt. В него можно добавить признак ServerMode, отвечающий за то, в каком режиме тот будет работать. Значение этого признака будем устанавливать до начала работы сокета компонента. А выбор метода "привязки адреса" поместим в метод Open. Копируя стиль разработки компонента UdpSocketExt, значение признака ServerMode, можно устанавливать в конструкторе UdpClient и UdpServer.
В-третьих, можно просто добавить в компонент UdpSocketExt признак ServerMode, отвечающий за то, в каком режиме тот будет работать. Выбор метода "привязки адреса" можно разместить в методе Open. И не создавать дополнительных классов, как в первом и втором случае.
Во-четвёртых, можно определять в каком режиме будет работать компонент, по заполнению значениями свойств LocalHost, LocalPort, RemoteHost и RemotePort. Например, если значениями заполнены RemoteHost и RemotePort, то в режиме "клиент", а если нет - то "сервер". Способ привлекательный, но перспектива перебирать все возможные комбинации заполнения указанных свойств настораживает.
Из всех способ, самым простым, но в то же время, выполняющий наши задачи, оказался третий. Его и возьмём за основу, при разработке компонента.
constructor TCustomUdpClient.Create(AOwner: TComponent);
begin
...
FServerMode := False;
end;
procedure TCustomUdpClient.Open;
var
addr: TSockAddr;
wHost, wPort :string;
begin
inherited Open;
if Active and not FConnected
then begin
if FServerMode
then begin
FConnected := Bind; //использует FLocalHost, FLocalPort
if FConnected
then begin
SocketToAddrStr (Handle, wHost, wPort);
FBindHost := wHost; FBindPort := wPort;
end;
end
else begin
addr := GetSocketAddr(RemoteHost, RemotePort);
FConnected := ErrorCheck(WinSock.connect(Handle, addr, sizeof(addr))) = 0;
if FConnected
then begin
SocketToAddrStr (Handle, wHost, wPort);
FBindHost := wHost; FBindPort := wPort;
end;
end;
if FConnected
then DoConnect;
end;
end;
Добавив изменения в файл модуля SocketsExt.pas и испытав компонент в программе, аналогичной той, которую мы использовали в разделе 5.3., можно удостовериться, что всё работает так, как и предполагалось.
Но при работе тестового примера выяснилось, что если установить значение свойства LocalPort=0, то вне зависимости от того какой режим работы (ServerMode) установлен, сокет привязывается к произвольному порту, выбранному системой. Какой же это "серверный режим". Получается, что название режима работы при ServerMode=True выбрано неудачно. Оно скорее сбивает с толку, чем поясняет алгоритм работы компонента.
Так получилось потому, что в нашей разработке, мы забежали слишком далеко вперёд и не проведя "анализа", сразу перешли к "проектированию". И уже на основе результатов "проектирования" сделали вывод характерный для этапа "анализа". Другими словами, мы подменили результат размышлений и опытов, устоявшейся практикой.
В повседневных, рутинных делах это оптимальный и единственно правильный подход. Да же при проектировании, иногда, в интуитивно понятных, в простейших случаях, это оказывается полезным и сокращает время разработки. Но в основном, он оказывается очень даже вредным. Из-за этого, даже в простых вещах упускаются важные аспекты и заменяются примитивными шаблонами, а сложные вещи становятся ещё сложнее и запутаннее.
2) "Проанализируем", что у нас есть, что нам нужно сделать, что нам для этого потребуется.
Основу работы компонента составляет библиотека работы с сетевыми сокетами. И нам как раз и нужно создать свойство-признак, которое переключало бы все возможные режимы работы сокета (привязку адреса) для протокола UDP. Перечислим их:
-bind(), LocalPort = 0 - привязка к произвольному порту;
-bind(), LocalPort > 0 - привязка к определённому порту;
-connect(), RemoteHost, RemotePort - привязка к произвольному порту и включение фильтра адресатов.
Получается, что выбор того, будет ли "привязан адрес" сокета к произвольному или определённому порту вполне может быть определено одной функцией bind(). А функция connect() выполняет только настройку "фильтра адресатов" для функции send(). Более того, функцию connect() можно вызывать один раз и после вызова функции bind(). В этом случае, "привязка адреса" осуществляется bind(), а connect() уже использует "привязанный адрес".
Можно сказать что функции bind() и connect(), в случае протокола UDP - это два разных этапа настройки адресов сокета. Один отвечает за "привязку адреса" сокета. И это этап обязательный. А другой - за установку "фильтра адресатов". И это опциональный этап. Видимо эту этапность и нужно реализовать в компоненте.
3) Приступим к доработке компонента. В нём, этапы настройки адресов сокета, также должны быть разделены.
Первый этап "привязки адрес" обязателен. Он должен быть выполнен всегда при включении сокета компонента. В общем-то, его настройка у нас уже есть и происходит она автоматически. При установке значения свойства LocalPort=0 происходит привязка к произвольному порту, а при LocalPort > 0 - привязка к определённому порту.
Второй этап настройку "фильтра адресатов" - опционален. Его можно включить, а можно выключить. Для него добавим свойство-признак ConnectMode. При установке признака, в методе Open, после вызова bind() будет вызвана connect(). В качестве параметров, функция connect() будет использовать значения свойств RemoteHost и RemoteHost.
unit SocketsExt;
interface
type
TCustomUdpSocketExt = class(TIpSocket)
private
FConnectMode :boolean;
procedure SetConnectMode(const aValue: boolean);
...
public
constructor Create(AOwner: TComponent); override;
procedure Open; override;
...
//Свойство устанавливает "режим работы", при котором используется "фильтр адресатов"
property ConnectMode: boolean read FConnectMode write SetConnectMode;
end;
TUdpSocketExt = class(TCustomUdpSocketExt)
published
property ConnectMode;
...
end;
implementation
constructor TCustomUdpSocketExt.Create(AOwner: TComponent);
begin
...
FConnectMode := False;
end;
procedure TCustomUdpSocketExt.Open;
var
wAddr: TSockAddr;
wHost, wPort :string;
begin
inherited Open;
if Active and not FConnected
then begin
FConnected := Bind; //использует FLocalHost, FLocalPort
if FConnected and SocketToAddrNames (Handle, wHost, wPort)
then begin
FBindHost := wHost; FBindPort := wPort;
end;
if FConnectMode and FConnected
then begin //режим "фильтрации адресатов"
wAddr := GetSocketAddr(RemoteHost, RemotePort);
FillChar(wAddr.sin_zero, SizeOf(wAddr.sin_zero), 0);
FConnected := ErrorCheck(WinSock.connect(Handle, wAddr, sizeof(wAddr))) = 0;
end;
if FConnected
then DoConnect;
end;
end;
procedure TCustomUdpSocketExt.SetConnectMode(const aValue: boolean);
begin
if aValue <> FConnectMode
then begin
if not (csLoading in ComponentState) and not (csDesigning in ComponentState)
then Close;
FConnectMode := aValue;
end;
end;
4) Для испытания компонента UdpSocketExt, также как, и во "второй" (5.2.), и в "третьей попытке" (5.3.), создадим в Delphi такую же программу. Добавим изменения в файл модуля SocketsExt.pas.
Работа компонента в программе стала проста, понятна и предсказуема.
С помощь этого компонента уже можно написать программу обмена сообщениями "чат". Например, такую как Delphi7\Demos\Internet\NetChat, но использующую протокол UDP. Или же такую, как во второй главе книги Григорьева А. Б. - UDPChat.
Но как всегда, и в этот раз, без недочётов дело не обошлось.
Первоначальный компонент UdpSocket был ориентирован на работу исключительно при включённом "фильтре адресатов". Поэтому в нём, использовались методы передачи сообщений без указания адресата в параметрах. Например, SendBuf, Sendln, SendStream. Поскольку этот режим, в новом компоненте, включается только иногда, то "только иногда" работают методы SendBuf, SendLn, SendStream. Безусловно, их всегда можно заменить на метод SendTo, но уж очень удобно использовать "паскалевские типы", вместо не типизированных указателей.
Для работы тестовых программ, мы использовали компонент, сокет которого работает в "блокирующем режиме". С одной стороны, такой подход прост и нагляден. Но для практического использования требуется разрабатывать и создавать отдельный класс потока, в котором работал бы компонент. Для таких целей подошёл бы компонент со встроенным "дополнительным потоком", прослушивающий порт сокета. Поскольку в базовом модуле Sockets.pas есть компонент TcpServer использующий многопоточность, то на его примере, можно было бы собрать аналогичный компонент, но использующий протокол Udp.
5.3. Попытка третья. Разрабатываем компонент.
До этого момента, мы занимались, либо описанием, либо практическим применением того, что было предложено компонентом UdpSocket и модулем Socets, согласно инструкции по использованию. Тем самым, мы повторили путь, которым проходят большинство программистов, впервые встретившись с этим компонентом и модулем.
Столкнувшись с тем, что, и работа компонента, и инструкции по использованию, несколько отличается от того, что мы хотим получить на практике от UdpSocket, попытаемся изменить и то и другое. Но лишь в отношении того, что нас не устроило. Попробуем разработать компонент, основанный на одном из предков UdpSocket.
Такой подход, заключающийся в разработке нового компонента, кажется сложнее метода "дополнительных функций", (который мы применили в попытка 5.2.). Но как окажется, всё усложнение обуславливается структурой самого метода. К тому же, нас-то больше заботит простота применения, при практическом использовании. А в итоге так и получится. Поэтому, несколько усложнив структуру решения нашей задачи, мы упростим вид полученного решения.
Собственно, сам процесс разработки компонента, представляет из себя несколько последовательных этапов изучения старого и создания нового:
1) Определим отправную точку и условия задачи.
Как и прежде, основой для нашей работы послужит модуль Sockets. Мы постараемся взять из него всё что нам подойдёт. И при этом постараемся, не только не нарушить правил работы других компонентов этого модуля, но и сохранить их в работе нового. Задачи перед нами остались те же, которые мы сформулировали в начале 5 раздела.
Выберем имя компонента, не сильно отличающимся от старого. Например, UdpSocketExt. Расчистим место для работы. Создадим новый модуль, с именем, не сильно отличающимся от старого. Например, SocketsExt. И сделаем заготовку для бедующего компонента. В модуле SocketsExt создадим класс TUdpSocketExt.
2) Теперь "проанализируем", что у нас есть, что нам нужно сделать, что нам для этого потребуется.
В предыдущих разделах мы выяснили, что:
-у нас есть компонент UdpSocket, работу которого необходимо подкорректировать;
-простоту его работы в виде последовательности действий: "открыл", "переслал сообщение", "закрыл", "получил сообщение", "закрыл", хотелось бы сохранить;
-основной причиной несуразной работы UdpSocket является метод Open;
-в этом методе используется вызов последовательности функций WinSock socket() и connect(), который приводит к созданию сокета и "привязке его" к произвольно выбранному системой порту;
-ближайшим предком TUdpSocket, в методе Open которого нет такой связки, является TIpSocket;
-ближайшим аналогом TUdpSocket является TTcpClient. Более того, по существу это один и тот же класс (TCustomIpClient), но с разными настройками, произведёнными в методе Create;
-практическая работа компонента TcpClient, в отличии от UdpSocket, нареканий не вызывает.
Мы поставили перед собой задачу сделать простой, как в использовании, так и в разработке компонент. Кроме того, желательно использовать уже содержащееся в модуле Sockets. Поэтому, из выясненных фактов и задач, сделаем предположение, что:
-раз TUdpSocket и TcpClient, основаны на одном классе (TCustomIpClient), но работа одного из них нас не устраивает, то нужно выделить их в отдельные классы. Вернее сказать, выделить "новый класс", а TTcpClient и TCustomIpClient оставить как есть;
-"новый класс" необходимо сделать максимально похожим на старый. В простейшем случае, можно просто скопировать всё содержимое старого класса в новый;
-в "новом классе" оставить настройки и всё что нас устраивает от "старого класса", остальное можно удалить;
-в "новом классе" изменить только то, что нас устраивает или не используется;
Компонент TcpClient, который мы выбрали для образца, разделён на два класса: TTcpClient и TCustomIpClient. Так сделано потому, что "базовый класс" TCustomIpClient содержит всю функциональность. Он используется при разработке и от него рекомендуется наследовать производные классы. А класс TTcpClient используется в качестве компонента (долгоживущего объекта, опубликованные свойства которого могут сохранять значения). Он может быть зарегистрирован в IDE Delphi, а его "опубликованные" свойства будут отображаться в Object Inspector. Собственно, этими "опубликованными" свойствами и отличается TTcpClient от TCustomIpClient.
Мы, с нашим новым компонентом UdpSocketExt, поступим по аналогии с TTcpClient. Всю функциональность мы поместим в класс TCustomUdpSocketExt, а "опубликованные" свойства создадим только для TUdpSocketExt.
В "старом классе" TCustomIpClient, нас не устраивает только метод Open, который мы перепишем в "новом классе". Последовательность функций WinSock: socket(), connect(), мы заменим на последовательность - socket(), bind(). В первом случае, сокет создаётся и привязывается к произвольному порту. А во втором случае, сокет, после создания, привязывается к указанному порту. Для этих целей нам подойдёт свойство LocalPort. Причём, если будет указано положительное число, и система сочтёт, что номер порта может быть использован, то сокет будет привязан к этому порту. А если указать значение 0 или "пустое значение", то сокет будет привязан к порту, произвольно выбранным системой.
В "старом классе" TCustomIpClient, конструктор Create, устанавливает значения некоторых служебных свойств компонента. Но значения, влияющие на тип протокола создаваемого сокета, остаются неизменными (по умолчанию). Зато такие значения меняются в конструкторе TUdpSocket. В конструкторе "нового класса" мы скопируем код, и из конструкторов TCustomIpClient, и из TUdpSocket.
Функция GetThreadObject, в "старом классе" использовалась для обеспечения "фонового потока", который мы (пока) не предполагаем использовать. Поэтому, в "новый класс", эту функцию не добавляем.
В UdpSocket, отсутствовали свойства, которые бы позволяли узнать, к какому адресу был привязан сокет. Например, можно добавить свойства BindHost и BindPort, по аналогии с LocalHost и LocalPort.
У нас получился целый список. "Список того, что нам нужно сделать" на следующем этапе. Такой "список хотелок" обычно называют "требованиями" к разрабатываемому компоненту.
3) А теперь, приступим к "разработке" самого компонента.
У нас уже есть модуль SocketsExt и заготовка класса TUdpSocketExt. Будем действовать по намеченному на предыдущем этапе плану:
-очистим модуль SocketsExt от всех классов, содержащихся в нём;
-скопируем в модуль SocketsExt классы TUdpSocket, TTcpClient, TCustomIpClient из модуля SocketsExt;
- пререименуем классы TTcpClient, TCustomIpClient в модуле SocketsExt в классы в TUdpSocket и TCustomUdpSocket, соответственно;
- изменим эти классы, в соответствии со списком требований (так как описано выше, в этапе 2);
- удалим класс TUdpSocket (пока что только из модуля SocketsExt).
В результате у нас получится модуль со следующим кодом:
unit SocketsExt;
interface
uses
Classes, Sockets, WinSock;
type
TCustomUdpSocketExt = class(TIpSocket)
private
FConnected: Boolean;
FOnDisconnect: TSocketNotifyEvent;
FOnConnect: TSocketNotifyEvent;
FBindHost :string;
FBindPort :string;
protected
procedure DoConnect; virtual;
procedure DoDisconnect; virtual;
public
constructor Create(AOwner: TComponent); override;
procedure Close; override;
procedure Open; override;
//Отображает адрес и порт на котором открыт сокет
property BindHost: string read FBindHost;
property BindPort: string read FBindPort;
property Connected: Boolean read FConnected;
property OnConnect: TSocketNotifyEvent read FOnConnect write FOnConnect;
property OnDisconnect: TSocketNotifyEvent read FOnDisconnect write FOnDisConnect;
end;
//Функция, извлекающая строковое значение адреса и порта из TSocket
function SocketToAddrStr(aSocket :TSocket; var aHost, aPort :string) :boolean;
var
nAddrLen :integer;
wAddr :TSockAddr;
begin
Result := False;
aHost := ''; aPort := '';
if aSocket = INVALID_SOCKET
then Exit;
nAddrLen := SizeOf(wAddr);
if getsockname(aSocket, wAddr, nAddrLen) = SOCKET_ERROR
then Exit;
aPort := IntToStr(ntohs(wAddr.sin_port));
aHost := Format( '%d.%d.%d.%d',
[Ord(wAddr.sin_addr.S_un_b.s_b1),
Ord(wAddr.sin_addr.S_un_b.s_b2),
Ord(wAddr.sin_addr.S_un_b.s_b3),
Ord(wAddr.sin_addr.S_un_b.s_b4)]);
Result := True;
end;
{ TCustomUdpSocketExt }
procedure TCustomUdpSocketExt.Close;
begin
if FConnected
then begin
ErrorCheck(shutdown(Handle, SD_BOTH));
FConnected := False;
DoDisconnect;
end;
FBindHost := '';
FBindPort := '';
inherited Close;
end;
procedure TCustomUdpSocketExt.Open;
var
wAddr: TSockAddr;
wHost, wPort :string;
begin
inherited Open;
if Active and not FConnected
then begin
FConnected := Bind; //использует FLocalHost, FLocalPort
if FConnected and SocketToAddrStr (Handle, wHost, wPort)
then begin
FBindHost := wHost; FBindPort := wPort;
end;
if FConnected
then DoConnect;
end;
end;
...
//Содержание остальных методов (DoConnect, DoDisconnect),
//полностью повторяет содержание соответствующих методов класса TCustomIpClient
4) Используем результаты нашей работы на практике.
Для испытания компонента UdpSocketExt, также как во "второй попытке" (5.2.), создадим в Delphi программу. В прошлом примере, мы испытывали компонент FIpSocket :TIpSocket. В этом примере, заменим его на разработанный нами FUdpSocket :TUdpSocketExt. Оставим те же поля редактирования TEdit и метки TLabel. События onClick кнопок TButton, будем использовать для вызова методов FUdpSocket: Open, Close, ReciveLn, RciveFrom SendLn, SendTo. Отдельную кнопку создадим для отображения значений свойств компонента FUdpSocket. Экземпляр класса FUdpSocket :TUdpSocketExt будем создавать в событии главной формы onShow, а уничтожать - в событии onCloseQuery.
Запустим программу на выполнение. Оказалось, что всё работает, как и предполагалось. При значении свойства LocalPort="12345", сокет создаётся на этом порту, а при значении LocalPort="0" - на порту, произвольно выбранном системой. Сообщения передаются с помощью методов ReciveFrom, ReciveLn и SendTo. А вот метод SendLn и аналогичные ему SendBuf SendStream, работать не будут, поскольку в них нет параметров, указывающих адресат. Да и метод ReciveLn будет принимать сообщения неизвестно откуда.
Так же отметим, что метод ReciveFrom, в одном из параметров должен возвращать адрес отправителя. Но, из-за ошибки в описании TIpSocket.ReceiveFrom(), этого не происходит.
Сделаем вывод, что наш список требований к разрабатываемому компоненту нужно дополнить. Сам компонент нужндается в доработке, а его работа - в исправлении. Поэтому, нам придётся снова вернуться к этапу 2) и проанализировать полученные результаты.
5.1. Попытка первая.
Попытаемся оставить в компоненте UdpSocket всё как есть.
Основным препятствием, к тому, чтобы в UdpSocket указать значение номера порта, который будет прослушивать сокет является метод Open. В нём вызываются функции socket() и connect(). Нам же, в этом методе, требуется последовательность функции socket() и bind().
Внешними, по отношению к компоненту действиями, мы не можем изменить содержание метода Open. Да и передать в компонент, созданный "на стороне" сокет не получится. Поэтому, всё-таки что-то да придётся поменять.
5.2. Попытка вторая.
Попробуем создать экземпляр класса одного из предков UdpSocket. А всё что нам нужно, сделаем с помощью дополнительного вызова функций WinSock.
Ближайшим предком, у которого нет связки функции, которую нам нужно заменить, является класс TIpSocket. И у этого класса даже есть подходящий метод Bind, но он оказался protected. Для примера, создадим в Delphi программу. И дополним её следующим кодом:
//Создать экземпляр компонента
procedure TfvMain.btnSocketCreateClick(Sender: TObject);
begin
if not Assigned(FIpSocket)
then begin
FIpSocket := TIpSocket.Create(Self);
FIpSocket.SockType := stDgram;
FIpSocket.Protocol := IPPROTO_UDP;
FIpSocketBind := False;
end;
end;
//Освободить экземпляр компонента
procedure TfvMain.btnSocketFreeClick(Sender: TObject);
begin
if Assigned(FIpSocket)
then begin
if FIpSocketBind
then shutdown(FIpSocket.Handle, SD_BOTH);
FreeAndNil(FIpSocket);
end;
end;
//Открыть сокет и привязать его к указанному адресу
procedure TfvMain.btnOpenBindClick(Sender: TObject);
var
wAddr :TSockAddr;
nBindRes :integer;
begin
if Assigned(FIpSocket)
then with FIpSocket do
begin
BlockMode := bmBlocking;
LocalHost := edLocalHost.Text;
LocalPort := edLocalPort.Text;
RemoteHost := edRemoteHost.Text;
RemotePort := edRemotePort.Text;
Open;
if Active and not FIpSocketBind
then begin
wAddr := GetSocketAddr(LocalHost, LocalPort);
nBindRes := WinSock.bind(Handle, wAddr, SizeOf(wAddr));
FIpSocketBind := nBindRes <> SOCKET_ERROR;
end;
end;
end;
//Открыть сокет и привязать его к произвольному адресу, с установкой фильтра адресатов
procedure TfvMain.btnOpenConnectClick(Sender: TObject);
var
wAddr :TSockAddr;
nCnctRes :integer;
begin
if Assigned(FIpSocket)
then with FIpSocket do
begin
BlockMode := bmBlocking;
LocalHost := edLocalHost.Text;
LocalPort := edLocalPort.Text;
RemoteHost := edRemoteHost.Text;
RemotePort := edRemotePort.Text;
Open;
if Active and not FIpSocketBind
then begin
wAddr := GetSocketAddr(RemoteHost, RemotePort);
FillChar(wAddr.sin_zero, SizeOf(wAddr.sin_zero), 0);
nCnctRes := WinSock.connect(Handle, wAddr, SizeOf(wAddr));
FIpSocketBind := nCnctRes <> SOCKET_ERROR;
end;
end;
end;
//Закрыть сокет
procedure TfvMain.btnCloseClick(Sender: TObject);
begin
if Assigned(FIpSocket)
then begin
if Active and FIpSocketBind
then shutdown(FIpSocket.Handle, SD_BOTH);
FIpSocket.Close;
end;
end;
//Отправить текстовое сообщение с помощью SendTo
procedure TfvMain.btnSendClick(Sender: TObject);
var
wAddr :TSockAddr;
wStr :string;
begin
if Assigned(FIpSocket) and FIpSocket.Active and FIpSocketBind
then begin
wStr := edMessage.Text; //извлекаем текст из TEdit
wAddr := FIpSocket.GetSocketAddr(edRemoteHost.Text, edRemotePort.Text);
FillChar(wAddr.sin_zero, SizeOf(wAddr.sin_zero), 0);
FIpSocket.SendTo(wStr[1], Length(wStr), wAddr);
end;
end;
//Отправить текстовое сообщение с помощью SendLn (только, если был вызван connect)
procedure TfvMain.btnSendLnClick(Sender: TObject);
begin
if Assigned(FIpSocket) and FIpSocket.Active and FIpSocketBind
then FIpSocket.SendLn(edMessage.Text);
end;
//Получить текстовое сообщение (с блокировкой)
procedure TfvMain.btnReciveClick(Sender: TObject);
var
wStr :string;
wAddr :TSockAddr; //адрес отправителя
wBuffer :array[0..511] of char;
nBufferLen, nAddrLen, nRecvRes :integer;
begin
if Assigned(FIpSocket) and FIpSocket.Active and FIpSocketBind
then begin
wStr := '';
nBufferLen := SizeOf(wBuffer);
nAddrLen := SizeOf(wAddr);
FillChar(wAddr.sin_zero, SizeOf(wAddr.sin_zero), 0);
nRecvRes := FIpSocket.ReceiveFrom(wBuffer, nBufferLen, wAddr, nAddrLen);
if nRecvRes <> SOCKET_ERROR
then begin
wStr := wStr + copy(wBuffer, 0, nRecvRes);
mmLog.Lines.Add(' пришло сообщение: ' + wStr);
end
else begin
mmLog.Lines.Add(' ошибка : nRecvRes = ' + IntToStr(nRecvRes) + ', ' +
BoolToStr(nRecvRes <> SOCKET_ERROR, True));
end;
end;
end;
Получилась вполне работоспособная программка. Если в свойстве LocalPort указать значение "0", то сокет создаётся на произвольном порту, выбранном системой. А если в нём указать значение номера порта (например, "12345"), то сокет будет связан с этим номером порта. После запуска двух программок, создания (кнопка btnSocketCreate) и открытия (кнопка btnOpenBind) в них сокетов, можно между ними передавать сообщения.
Сокет создаётся, а "связывание адреса" происходит с указанным нами портом. Но после этого, для передачи данных мы можем использовать только методы ReceiveFrom и SendTo. Для того чтобы использовать упрощённые методы ReceiveBuf, Receiveln, SendBuf, Sendln, SendStream нам нужно, открыть сокет и "связать адрес" так же, как это делалось в UdpSocket (кнопка btnOpenConnect).
Но в число наших задач, входила возможность, не только "связь двух компонентов подобных UdpSocket и передачи между ними данных", но и добиться простоты работы с ними. В этом же примере, всё не столь просто как предполагалось. Но мы сами выбрали такой подход, требующий ручной настройки и вызова дополнительных функций.
Ну что же. Продолжим наши эксперименты. И в следующей попытке, чтобы упростить, мы немного усложним.
5. Так можно ли сделать UDP сервер на основе UdpSocket?
—————————————————————————————————————————————-
Как мы уже рассказывали, с помощью только этого компонента и его штатных средств, не получится сделать "сервер" UDP.
Но если нельзя, а очень хочется, то ... . Нет нельзя. Поэтому поищем обходные пути. Конечно, проще всего можно было бы использовать компоненты UdpClient и UdpServer из сторонних библиотек. Например, из Indy. Но, во-первых, там уже всё и без нас работает, а мы не ищем лёгких путей. Во-вторых, интересно же разобраться как там всё устроено. В-третьих, в нас просыпается перфекционист, который подталкивает нас к исправлению несуразного компонента. К тому же, можно предположить, что раз компонент состоит из двух строчек полезного кода, то и исправления не займут много времени. Поставим перед собой задачу, так доработать компонент UdpSocket, чтобы сокеты создавались на указанном UDP порту, и между двумя компонентами можно было бы пересылать текстовые сообщения посредством очень простых функций (например, SendLn, ReciveLn). Не будем строить грандиозных планов, по написанию замены библиотеки Indy. Нам нужен простейший компонент, с простейшей функциональностью, но удобный в практическом применении. При этом желательно учесть "во-вторых" и "в-третьих" из предыдущего абзаца. Да и с чистого листа, начинать то же не хочется. Поэтому заглянем в модуль Sockets и посмотрим, что из него может нам пригодиться.
В модуле Sockets, кроме компонента UdpSocket, располагается ещё пара, вполне работоспособных компонент: TTcpClient и TTcpServer. Для этой пары есть пример в поставке Delphi, демонстрирующий их совместную работу (Delphi7\Demos\Internet\NetChat). Конечно мы не будем, при решении нашей задачи, полностью повторять их работу, но основные черты обоих позаимствуем.
Иерархия наследования интересующих нас классов, в модуле Sockets, такая:
TUdpSocket
TCustomIpClient -в методе Open - вызов inherited Open, connect()
TIpSocket -свойства Host, Port
TBaseSocket (TComponent) -в методе Open - вызов socket()
TTcpClient
TCustomIpClient
TIpSocket
TTcpServer
TCustomTcpServer
TIpSocket
Отметим, что TUdpSocket, это практически TTcpClient, но с другими настройкаи. Классы TUdpSocket, TTcpClient, TTcpServer отличаются от своих предков с префиксом TCustomXXX, лишь тем что в них "опубликованы" некоторые свойства, которые потом будут отображаться в Object Inspector.
Ну а теперь самое время, начать поиски обходных путей и попытки решить поставленную задачу.
4. Особенности
——————————
У компонента доступно достаточно много свойств, методов и событий. Такое их количество, с одной стороны вселяет надежду о богатую функциональность компонента, а с другой стороны сбивают с толку разнообразием. Тем более, что некоторый из них, компонентом не используются или дублируются.
Но компонент и его функционал очень просты. Настройка компонента производится установкой значений всего в трёх свойствах: BlockMode, RemoteHost, RemotePort. Другие свойства, либо уже настроены, либо, как например LocalHost и LocalPort, не используются. Причём изменение значения, практически любого свойства, приведёт к неявному вызову метода Close и остановке работы компонента.
Свойства RemoteHost и RemotePort, вообще можно оставить пустыми. Об этом случае, мы поговорим чуть позднее.
При вызове метода Open происходит следующее:
- создаётся сокет с помощью вызова функции socket();
- происходит привязка сокета к адресу с помощью вызова функции connect().
Свойство компонента Active, как раз и показывает создан сокет или нет. А свойство Connected - результат привязки сокета к адресу.
Вызов функции connect(), в качестве параметров, использует значения свойств RemoteHost и RemotePort. При этом, конечно никакого соединения не устанавливается (как в TCP). Но устанавливается фильтр для адресов отправления и получения данных для методов Send и Recv. Все адреса, не подходящие под этот фильтр отбрасаваются.
При вызове метода Close, сокет закрывается с помощью последовательного вызова функций shutdown() и closesocket().
Самая важная особенность. Особенность компонента UdpSocket состоит в том, что функции "создания сокета" и "привязки его к адресу" спрятаны внутри одного метода Open. И после вызова этого метода, мы получаем готовый к работе сокет с уже привязанный адресом. Через него можно, либо передавать данные, либо его закрыть. Но номер порта, на котором создаётся сокет всегда произвольно определяется системой. Мы не можем заранее задать значение этого порта. И не можем изменить его значения, ни до, ни после открытия сокета. Поэтому-то и нельзя создать UDP-сервер наподобие "чата" штатными средствами этого компонента.
Особенности пересылки данных.
Поскольку компонент является лишь обёрткой функций библиотеки WinSock, то и функции передачи и получения данных носят похожие названия. Назначения функций легко угадываются из их названий:
ReceiveBuf, Receiveln, SendBuf, Sendln, SendStream.
Пожалуй, новшеством, являются только функции: Receiveln, Sendln, SendStream. Receiveln - принимает дейтаграммы, содержащие текстовые строки, до тех пор, пока не будет получена пустая строка. Sendln - отсылает одну текстовую строку. SendStream отсылает данные потока TStream порциями по 512 байт.
Как уже было сказано выше, "локальный адрес" сокета определяется системой. В нём, адрес хоста, мы можем считать известным и неизменным. А вот номер порта определяется в момент вызова метода Open. Значения этих параметров никак не отображаются в компоненте. Но их можно извлечь с помощью функций библиотеки winsocket и зная дескриптор сокета.
"Адрес назначения", который используется в этих функциях, определяется значениями свойств RemoteHost, RemotePort. Если указаны значения RemoteHost, RemotePort, то функции ReceiveXXX будут принимать, сообщения только от этого адреса. То же справедливо и для функций SendXXX. Если же значения свойств RemoteHost, RemotePort пусты, то сообщения принимаются (или отсылаются для) от любого адреса и порта.
Если нам требуется связаться с произвольно указанным адресом, при установленных значениях свойств RemoteHost, RemotePort, то следует использовать функции: ReceiveFrom, SendTo.
Другие особенности.
Из других особенностей можно отметить следующие:
Методы ReceiveXXX предназначены только для блокирующего режима работы сокета.
Классы TClientSocketThread и TServerSocketThread, расположенные в модуле Sockets, относятся только к созданию TCP-сервера на основе компонентов TCPClient и TCPServer.
Событие OnGetThread, хоть и отображается в UdpSocket, но в нём не используется и относится к компоненту TCPServer.
В модуле Sockets, кроме компонентов TCPServer, TCPClient и UDPSocket, можно найти и RAWSocket. Компонент RAWSocket, разработан по той же "двух-строчечной методике", что и UDPSocket. Видимо, с помощь этого компонента предполагалась работать с "простым сокетом".
Свойства LocalHost и LocalPort компонента UdpSocket никак не используются (вообще никак). В свойствах LocalHost и RemoteHost можно указывать, как адрес ip (например, 127.0.0.1), так и строку адреса (например, localhost). В свойствах LocalPort и RemotePort можно указывать, как числовой номер порта (например, 123), так и название сервиса (например, tftp).
Зато, можно использовать очень удобные методы SendBuf, SendLn, SendStream, ReceiveBuf, Receiveln. В их параметрах указываются только передаваемые данные: строка, поток, или не типизированный буфер. Но воспользоваться ими, можно только, если заполнены значениями свойства RemoteHost и RemotePort.
05-11-2024 11:39 | Комментарий к предыдущим ответам
>>>Но т.к. задача очень простая, как раз по совету в книге решил обойтись самым простым компонентом. Но теперь понял...
Прочитав книгу Григорьева, вы вполне в состоянии сами написать эту статью вместо меня. И даже более того. Чуть дополнив компонент UdpSockets, переписать с помощью него пример из той книги UDPChat.
А странно работающий компонент - это лишь полигон для проверки, того стало ли прочитанное для вас практическим знанием, или же осталось просто наблюдением.
>>>Попробуйте найти ответ на этот вопрос в моём посте "2. Предназначение"
Именно из прочитанного там я и сделал такой вывод. И очень удивился этому выводу.
>>>При этом, старательно избегая сути описываемого.
Так я предварительно читал книгу Антона Григорьева, он саму суть и описывает. И примеры из книги пробовал. Но т.к. задача очень простая, как раз по совету в книге решил обойтись самым простым компонентом. Но теперь понял что проще было взять самый простой и понятный пример из книги в качестве шаблона (ещё не поздно). И подозревать не мог о такой засаде на ровном месте.
05-11-2024 06:50 | Комментарий к предыдущим ответам
>>>История печальная, но очень интересная
История длинная и драматическая. Но не печальная. ;-)
Полагаю, что в информационных технологиях есть несколько разделов, которые, вроде бы и банальны на первый поверхностный взгляд. Да в общем-то оно и просты по сути. Но почему-то они всегда трудны в освоении. К таким разделами относятся: формальные языки и написание трансляторов с них, описание основ вычислительного процесса и сетевые технологии.
Видимо, дело в том, что излагают их в виде методического руководства, либо по использованию современных готовых решений (продуктов), либо в виде перечня видов решений типовых задач и примеров. При этом, старательно избегая сути описываемого.
Вот это и приводит к драматическим сюжетам, а иногда и к трагическим последствиям.
05-11-2024 06:43 | Комментарий к предыдущим ответам
>>>В смысле посмотреть на компе-сервере номер сокета, выделенного системой, потом пойти и забить его на компе-клиенте? И так при каждой передаче?
Попробуйте найти ответ на этот вопрос в моём посте "2. Предназначение".
И не отчаивайтесь. Возможно, вы найдёте ответы и на другие вопросы в продолжении истории.
Вот не ожидал что в ответ получу такую развернутую статью. Огромное спасибо! Это единственная ценная информация об этом компоненте во всем интернете. Искал долго - ни справки, ни примеров, ничего, все с ходу предлагают Indy. Эту статью надо в docwiki embarcadero!
История печальная, но очень интересная. Только немного смущает один момент:
3. На сервере получить адрес и номер порта открытого сокета (с помощью getsockname);
4. На клиенте заполнить свойства RemoteHost, RemotePort теми значениями, которые были получены на сервере;
В смысле посмотреть на компе-сервере номер сокета, выделенного системой, потом пойти и забить его на компе-клиенте? И так при каждой передаче? Или не правильно понял?
——-
Путем проб и снифера я догадался что данные в нужном направлении он посылает, но как принимать было загадкой. Из вашей статьи следует что использовать компонент можно, но с одним ограничением - только клиентом в блокирующем режиме. Завтра попробую по вашей инструкции.
——-
Кстати, ответы на вопрос заходил проверять каждый день. Вчера ответов было ноль. Сегодня сайт днем выдавал ошибку соединения, а вечером неожиданно появилось сразу четыре ответа с первого по четвертое ноября. Как-то так.
3. Как связать два компонента
——————————————-
У многих, кто пробовал работать с этим компонентом возникает вопрос: как связать два компонента для передачи между ними данных.
Ответ прост - штатных средств компонента для этого недостаточно. Более того, разработчики постарались как смогли, сделать наш путь, к настройке компонента, тернистым. Да ещё и оставили капканы в "интуитивно понятных местах".
Но начнём рассказывать о них по порядку.
Для того, чтобы одному компоненту (например, из программы "клиент"), связаться с другим компонентом (из программы "сервер"), нужно знать адрес получателя. И здесь в UdpSocket, нас поджидает первый капкан. С помощью штатных средств компонента нельзя узнать номер порта, который прослушивает сокет. Для это придётся использовать функцию getsockname библиотеки WinSock.
Второй капкан, нас поджидает рядышком. Он заключаются в том, что номер порта сервера, мы никак не можем узнать до вызова метода Open. Другими словами, мы никак не можем заранее задать значения номера порта, который будет прослушивать компонент на "сервере".
Третий капкан - самый большой, зато расположен в "интуитивно понятном месте". Он заключаются в том, что у компонента есть свойства устанавливающие значения "локальных" и "удалённых" адресов. И согласно документации, их нужно заполнить. Обычно предполагается, что на "сервере" нужно указать адрес куда будет отослано сообщение (RemoteHost, RemotePort) и адрес с которого будет отослано сообщение (LocalHost, LocalPort). А на "клиенте", так же, предполагается установить адрес куда будет отослано сообщение (RemoteHost, RemotePort) и адрес с которого будет отослано сообщение (LocalHost, LocalPort). Интуитивно понятно, что локальные адреса "клиента" должны соответствовать удалённым адресам «сервера" и наоборот. Но адрес "сервера", который нужен для старта "клиента", может быть получен только после получения адреса "клиента", который нужен для старта "сервера". Как мы теперь понимаем, такое сделать в принципе невозможно. А в случае заполнения свойств RemoteHost, RemotePort любыми значениями, и на сервере, и на клиенте, сделает передачу сообщений невозможной. Но как оказалось, делать такого и не нужно было.
Следующий капан, самый простой, но тоже "интуитивно понятный". Очевидно же, что для указания порта, который будет прослушивать сокет, его нужно указать в свойстве LocalPort. Но это свойство LocalPort, в компоненте просто отключено.
Пятый капкан поджидает нас при попытке передачи данных. Методы SendLn, ReceiveLn (и т.п.), работают только в "блокирующем режиме" сокета и при заполненных свойствах RemoteHost, RemotePort.
Но все же находится тропинка, идя по которой можно, все капканы обойти стороной, а компоненты связать между собой. Для связи двух компонентов UdpSocket и передачи между ними данных нужно создать программку, содержащую компонент UdpSocket и запустить два её экземпляра. Один из них назовём "сервером", а другой "клиентом". Будем полагать, что оба компонента будут работать в блокирующем режиме. Затем выполним действий в следующей последовательности:
1. На сервере у компонента UdpSocket свойства RemoteHost, RemotePort оставить незаполненными;
2. На сервере выполнить Open;
3. На сервере получить адрес и номер порта открытого сокета (с помощью getsockname);
4. На клиенте заполнить свойства RemoteHost, RemotePort теми значениями, которые были получены на сервере;
5. На клиенте выполнить Open;
6. На сервере начать ожидать строку сообщения, выполнив RecvLn (с блокировкой);
7. На клиенте передать строку сообщения, выполнив SendLn;
8. На сервере, после прихода сообщения, автоматически снимается блокировка;
9. На клиенте и сервере выполнить Close.
2. Предназначение
—————————
UdpSocket - это компонент, представляющий упрощённую обёртку над функциями библиотеки WinSocket, относящимися к работе с сокетами протоколов UDP/IP.
С помощью него можно отослать и принять одно или несколько сообщений через сокет UDP. В нём есть методы, где под сообщение не нужно создавать буфер. Просто отсылаем строку или поток. Достоинство компонента именно в простоте. Но это и его неустранимый недостаток.
С помощью компонента UdpSocket и его штатных средств, не получится сделать сервер UDPServer (на подобии TCPServer) или даже UDP-чат. Причина в том, что у компонента нет средств, задающих значение номера порта, который будет прослушивать сокет. Номер порта всегда произвольно выделяется системой.
По задумке, если такая и была, компонент должен быть простым и интуитивно понятным. Что может быть проще последовательности действий: указать адрес назначения, открыть; отослать сообщение; возможно, иногда получить ответ; закрыть. Поэтому и основных методов здесь всего ничего: Open, SendLn, RecvLn, Close. Но как часто бывает на практике, что-то пошло совсем не так, как задумывалось.
1. Интрига
—————
UdpSocket - это один из самых загадочных компонентов Delphi. В документации (начиная с Delphi 7), он крайне редко упоминается. А про описание его работы, разработчики, знатоки и корифеи скромно умалчивают.
Он появился во времена Delphi 6, как "много платформенная" замена компонентов ClientSocet и ServerSocket. И видимо, с тех пор о нём все просто забыли. Но тем не менее, исправно включают его в поставку очередных версий Delphi. При этом, сохраняя исходный код модуля абсолютно неизменным.
В файле модуля, с многозначительным названием Sockets, вместе с UdpSocket, находятся компоненты TcpClient и TcpServer. Вот на них-то можно найти, и документацию, и описание работы, и пример составления программы NetChat. И если на основе TcpClient и TcpServer можно построить полноценную систему обмена сообщениями, то компонент UdpSocket для этих целей не подойдёт. Поэтому складывается такое впечатление, что UdpSocket был добавлен, как вынужденное дополнение к этим двум компонентам. И действительно, для компонента UdpSocket используется та же основа (TCustomIpClient), что и для TcpClient и TcpServer. А если заглянуть в файл Sockets.pas, то окажется, что UdpSocket состоит ровно из 2-х (двух) строчек полезного кода.
Если вы заметили орфографическую ошибку на этой странице, просто выделите ошибку мышью и нажмите Ctrl+Enter. Функция может не работать в некоторых версиях броузеров.