Компоненты Indy, применяемые в Delphi 6. Часть 2
Построение собственного сетевого протокола
Применение многопоточности
ля уменьшения времени отклика и создания дружественного интерфейса в клиентских приложениях может быть использована многопоточность. Приложение, к рассмотрению которого мы переходим, демонстрирует технику применения многопоточности для построения клиента.
Для начала строится модуль с классом-наследником TThread, обычно применяющимся в Delphi для построения многопоточных приложений: основная функциональность нашего приложения обеспечивается экземпляром класса TIdTCPClient:
unit Unit2; interface uses Classes, Math, SysUtils, IdBaseComponent, IdComponent, IdTCPConnection, IdTCPClient; type TTCPClientThread = class(TThread) private FPort: Integer; FHostID: String; FCustomerInfo: String; { Private declarations } protected IdTCPClient: TIdTCPClient; procedure Execute; override; procedure UpdateList; public destructor Destroy; override; property HostID: String read FHostID write FHostID; property Port: Integer read FPort write FPort; end; implementation uses Unit1; procedure TTCPClientThread.UpdateList; begin Form1.Memo1.Lines.Add(FCustomerInfo); end; procedure TTCPClientThread.Execute; begin try IdTCPClient := TIdTCPClient.Create(nil); IdTCPClient.Host := FHostID; IdTCPClient.Port := FPort; while True do begin with IdTCPClient do begin if Terminated then Exit; try Connect; try WriteLn(IntToStr(RandomRange(1001, 1015))); if Terminated then Exit; FCustomerInfo := ReadLn; Synchronize(UpdateList); finally Disconnect; end; // try finally except end; if Terminated then Exit; sleep(RandomRange(1,Random(100))); end; //with end; //while True except //Something went wrong. Terminate the thread Exit; end; end; destructor TTCPClientThread.Destroy; begin if IdTCPClient <> nil then IdTCPClient.Free; inherited; end; initialization Randomize; end.
Метод UpDataList применяется для синхронизации с VCL-компонентами, содержащимися на главной форме приложения. Для имитации случайных обращений клиентов к серверу используется встроенный в Pascal генератор псевдослучайных чисел.
Для того чтобы использовать данный модуль, необходимо в модуле главной формы создать несколько экземпляров класса TTCPClientThread в виде, например, динамического массива:
var ThreadArray: array of TTCPClientThread;
Далее следует ввести код для метода, отвечающего за обработку события, сигнализирующего о нажатии кнопки:
procedure TForm1.Button1Click(Sender: TObject); var i: Integer; begin if Button1.Caption = 'Stop' then begin for i := low(ThreadArray) to High(ThreadArray) do ThreadArray[i].Terminate; Button1.Caption := 'Start'; end else begin Button1.Caption := 'Stop'; SetLength(ThreadArray, SpinEdit1.Value); for i:= 0 to Pred(SpinEdit1.Value) do begin ThreadArray[i] := TTCPClientThread.Create(True); with ThreadArray[i] do begin FreeOnTerminate := True; HostID := Edit1.Text; Port := StrToInt(Edit2.Text); Resume; sleep(200); //wait briefly before //creating next thread end; //with end;// for end; //else end;
После нажатия кнопки в соответствии со значением, установленным в SpinEdit1, порождаются экземпляры клиентов, независимо обращающиеся к базе данных с запросами.
Перейдем к более сложным примерам клиентских приложений. Создадим клиентское приложение, при помощи которого можно отправлять электронную почту с помощью протокола SMTP. Приложение также будет использовать два дополнительных класса из компонентов Indy: TIdMessage и TIdThread.
Чтобы послать сообщение посредством TIdSMTP, для начала нужно создать это сообщение, используя класс TIdMessage со страницы Indy Misc в качестве предка. Этот класс поддерживает формат сообщения, принятый в Internet. Классы TIdSMTP, TIdPOP3 и TIDNNTP используют класс TIdMessage, чтобы отправлять и принимать сообщения.
Создание и заполнение экземпляра класса TIdMessage во время выполнения демонстрирует следующий код:
var LMsg: TIdMessage; … LMsg := TIdMessage.Create(nil); try with LMsg do begin From.Address := 'Me@MyDomain.com'; Recipients.Add.Address := 'You@YourDomain.com'; Subject := 'Test Subject'; Body.Text := FSubject; end; finally LMsg.Free; end;
Экземпляр этого класса может также создаваться на этапе проектирования приложения — для этого достаточно просто поместить соответствующий компонент на форму.
Для создания сообщения, которое затем будет отправлено по электронной почте, как минимум должны быть заполнены свойства From и Recipients класса TidMessage (адрес отправителя и получателя). Свойство From имеет тип TIdEmailAddressItem. Класс TidEmailAddressItem выполняет обработку адресов электронной почты. В элементарном случае пользователю достаточно просто присвоить свойству Address этого класса свой электронный адрес. Свойство Recipients имеет тип TIdEmailAddressList, который является коллекцией классов TIdEmailAddressItem, так как рассылка может производиться по многим адресам одновременно (в нашем случае — по одному адресу). Заполнение свойства Subject типа String является необязательным — оно задает тему сообщения. Свойство Body типа TStringList содержит само сообщение, которое при помощи своего свойства Text может быть разбито на строки.
Использование экземпляра класса TIdSMTP тоже очень просто. Как минимум достаточно присвоить свойству Host этого класса IP-адрес или имя SMTP-сервера, который мы собираемся использовать для отправки сообщений. Кроме того, обязательно используются методы этого класса: Connect — для установления соединения (как обычно, с блокировкой); Send с одним параметром типа TIdMessage — для фактической передачи сообщения; фигурируют также методы Disconnect и Free, назначение которых очевидно. Вышесказанное можно проиллюстрировать следующим кодом:
with TIdSMTP.Create(nil) do try Host := FSMTPServer; Connect; try Send(LMsg); finally Disconnect; end; //try Synchronize finally Free; end; // with TIdSMTP try
Как уже говорилось, чтобы оградить прорисовку визуальной части приложения от замедления при сбоях, которые могут происходить в каналах, можно использовать компонент TIdAntiFreeze. При этом весь клиентский код должен располагаться в одном потоке. Но чтобы полностью изолировать клиентский код от VCL-компонентов главной формы или разрешить одновременное выполнение нескольких клиентов, лучше иметь многопоточное приложение. Для этого компоненты Indy имеют специальный класс TIdThread (наследник класса TThread). Главная особенность этого класса заключается в том, что при использовании TIdThread не перекрывается метод Execute (как это требуется в Tthread), а перекрываетcя метод Run у всех потомков. Когда вторичные процессы становится активными, этот метод выполняется неоднократно — вплоть до исключения или указания о завершении процесса. Метод Run также поддерживает синхронизацию. Следующий код демонстрирует описание типа для такого потомка:
uses IdThread; type TSMTPThread = class(TIdThread) public FFrom: string; FMessage: string; FRecipient: string; FSMTPServer: string; FSubject: string; procedure Run; override; end;
В этом примере метод Run просто использует значения, введенные в поля на главной форме, чтобы создать сообщение для электронной почты. Когда работа потока заканчивается, вызывается метод Stop, а процедура Run получает сообщение о завершении процесса. Вызов метода Stop фактически не закрывает вторичного процесса. Вместо этого появляется отметка о том, что завершение возможно в соответствии с алгоритмом метода Run. До тех пор пока нет выхода из метода Run, компоненты TIdThread не завершают вторичный процесс. При использовании TIdThread не вызывается метод Terminate, который наследуется от класса TThread. Всегда следует вызывать метод Stop, если нужно прекратить работу вторичного процесса. Ниже приведен код метода Run:
procedure TSMTPThread.Run; var LMsg: TIdMessage; begin LMsg := TIdMessage.Create(nil); try with LMsg do begin From.Address := FFrom; Recipients.Add.Address := FRecipient; Subject := FSubject; Body.Text := FMessage; end; //with LMsg with TIdSMTP.Create(nil) do try Host := FSMTPServer; Connect; try Synchronize(formMain.Connected); Send(LMsg); with TSyncSendResult.Create do try FCmdResult := CmdResult; SendResult(Self); finally Free; end; //with TSyncSendResult try finally Disconnect; end; //try Synchronize Synchronize(formMain.Disconnected); finally Free; end; // with TIdSMPT try finally LMsg.Free; end; // try Stop; end;
Поток создается при помощи метода Create, которому передается фактический параметр типа Boolean. Если этот параметр имеет значение True, он должен активизироваться после передачи данных полям и таким свойствам, как FreeOnTerminate, с помощью метода Resume. У класса TidThread метод Create может быть перекрыт. По умолчанию в качестве параметра ему передается True. Можно выполнить вызов этого метода с параметром False, если нет необходимости передавать какие-либо данные в экземпляр класса. Ниже приводится код, описывающий обработчик события нажатия кнопки:
procedure TformMain.butnSendMailClick(Sender: TObject); begin butnSendMail.Enabled := False; with TSMTPThread.Create do begin FreeOnTerminate := True; OnTerminate := ThreadTerminated; FFrom := Trim(editFrom.Text); FMessage := memoMsg.Lines.Text; FRecipient := Trim(editTo.Text); FSMTPServer := Trim(editSMTPServer.Text); FSubject := Trim(editSubject.Text); Start; end; end; procedure TformMain.ThreadTerminated(ASender: TObject); var s: string; begin s := TIdThread(ASender).TerminatingException; if Length(s) > 0 then ShowMessage('An error occurred while sending message. ' + s); else ShowMessage('Message sent!'); butnSendMail.Enabled := True; end;
Заметьте, что метод ThreadTerminated, который используется как обработчик события OnTerminate, проверяет свойство TerminatingException типа string. Если при работе потока происходит исключение, то в свойство TerminatingException записывается диагностическое сообщение и работа потока завершается. Обработчик события может, как в нашем случае, вывести на дисплей либо диагностическое сообщение, либо сообщение пользователю о том, что почта отправлена, либо выполнить какие-нибудь другие действия.
Часто бывает необходимо модернизировать главную форму приложения из потоков, созданных клиентскими компонентами. В нашем примере клиент модифицирует список на главной форме, который содержит динамическую информацию о состоянии соединения типа: связь с сервером установлена; подтверждение о передаче почты; разрыв соединения.
Если бы экземпляр клиента был создан в основном потоке приложения, модификация информации была бы стандартной. Однако, если вы работаете с CLX или VCL внутри вторичного потока, вам следует использовать метод Synchronize, фактическим параметром которого должна быть процедура без параметров, содержащая операторы чтения и записи свойств визуальных компонентов или вызовы их методов. Метод Synchronize обеспечивает вызов процедуры, переданный ему в качестве параметра в основном потоке, обеспечивая безопасный способ обращения к методам и свойствам визуальных компонентов. В нашем приложении используется два различных способа синхронизации.
Первая методика использует традиционную технику применения метода Synchronize. Например, следующий оператор вызывает один из методов компонента главной формы приложения:
Synchronize(formMain.Connected);
Поскольку процедура, адрес которой передается методу Synchronize, не может иметь параметров, то для преодоления этого ограничения операторы чаще всего помещают обращения к визуальным компонентам в «обертку» в виде дополнительной процедуры, например, как показано ниже:
procedure TformMain.Connected; begin Status('Connected'); end;
Метод Connected, вызываемый через Synchronize, в свою очередь, вызывает метод Status, являющийся методом класса главной формы:
procedure TformMain.Status(AMsg: string); begin lboxStatus.ItemIndex := lboxStatus.Items.Add(AMsg); end;
Вторая методика использует синхронизирующий класс — вспомогательный класс для промежуточного хранения информации, которая затем используется при управлении визуальными компонентами в основном потоке.
Имеется три способа объявления синхронизирующего класса:
- Объявить один или несколько полей для временного хранения данных.
- Объявить один или несколько методов, которые будут модифицировать визуальные компоненты.
- Объявить метод, входные параметры которого указывают на вторичный поток или метод вторичного потока, выполняемый через метод Synchronize.
Синхронизирующие классы особенно полезны, когда имеется большое количество данных либо когда необходима синхронизация между двумя или более вторичными потоками. Можно создавать экземпляр синхронизирующего класса в пределах потока, заполнять поля, вызывать его методы, а затем удалять экземпляр. Пример такого класса:
type TSyncSendResult = class private FCmdResult: string; procedure ShowResult; //a TThreadMethod type public procedure DoSynchronize(AThread: TIdThread; AMethod: TThreadMethod); end; procedure TSyncSendResult.DoSynchronize(AThread: TIdThread; AMethod: TThreadMethod); begin AThread.Synchronize(AMethod); end; procedure TSyncSendResult.ShowResult; begin formMain.Status('Mail accepted ok.'); formMain.Status('Server said ' + FCmdResult); end; Этот класс используется в методе Run, как показано в следующем коде. with TSyncSendResult.Create do try FCmdResult := CmdResult; DoSynchronize(Self, ShowResult); finally Free; end; //with TSyncSendResult try
Построение собственного сетевого протокола
ервым шагом при разработке клиент-серверных приложений является выбор протокола. В зависимости от потребностей это могут быть HTTP, FTP или любые другие протоколы. Если ни один из стандартных протоколов не подходит, необходимо разрабатывать собственный. На первый взгляд эта задача кажется сложной, но это не так. Большинство протоколов строятся на обмене текстовой информацией. За командой (как правило, это текстовая строка) от клиента к серверу следует «квитанция», подтверждающая получение команды сервером, затем может следовать передача данных. Другими словами, протокольные соглашения похожи на разговор двух людей, каждый из которых руководствуется определенными правилами.
При разработке собственного протокола необходимо определить:
- структуру команд для сервера;
- возможные отклики сервера на полученные команды;
- формат передаваемых данных.
В качестве иллюстрации рассмотрим конкретное клиент-серверное приложение, обеспечивающее кластерные вычисления. Задачу сформируем следующим образом: на основании двух списков (списка IP-адресов с номерами портов и списка заданий) при помощи одной программы, которую мы будем называть клиентом, необходимо получить возможность удаленного запуска выполняемых файлов из списка заданий при помощи заранее размещенных и запущенных на удаленных компьютерах серверов. В итоге множество выполняемых файлов можно рассматривать как одну сетевую программу, контролируемую приложением-клиентом. Сфера применения приложений такого типа весьма широка. Речь может идти о трудоемких вычислениях (когда мощности одного процессора не хватает для решения задачи), или о решении задач системного администрирования (например, о поддержке целостности ЛВС), или о простом интегрирующем средстве (существует много приложений, которые необходимо объединить в нечто похожее на систему). По-видимому, можно придумать и другие способы применения. В операционной системе UNIX существует множество приложений, обеспечивающих подобную функциональность, к тому же большинство из них распространяется бесплатно. Однако до последнего времени в ОС Windows 95/98/Me/NT/2000/XP подобные инструментальные средства отсутствовали.
Клиент-серверное приложение, к рассмотрению которого мы переходим, состоит из двух EXE-файлов, предназначенных для организации параллельной пакетной обработки в локальной сети под управлением операционных систем Windows 95/98/NT/2000/XP с протоколом TCP/IP и WinSock (вся обработка осуществляется в рамках стандартных протоколов благодаря использованию компонентов TIdTCPClient и ТIdTCPServer). Комплекс свободен для использования (freeware), распространяется с исходным текстом. Предполагается, что обмен данными между EXE-файлами и вывод результатов работы сетевой программы осуществляются стандартными сетевыми средствами, например на сетевой диск.
Сервер запуска должен уметь запускать и завершать процесс по команде диспетчера и уведомлять диспетчер о завершении процессов. Диспетчер занимается распределением задач по компьютерам (процессорам). Данные и программы хранятся на файловом сервере или могут на время работы программы копироваться на обрабатывающий компьютер. При использовании пакетной обработки изменения в программе минимальны — обычно это вынесение границ внешнего цикла в параметры программы. При этом не нужно использовать какую-либо библиотеку. Следовательно, программы, вызываемые серверами, могут быть написаны на любом языке программирования. Другим достоинством системы является ее высокая надежность: для работы системы достаточно надежной работы одного диспетчера. Еще одно достоинство системы — возможность одновременного выполнения нескольких заданий от разных пользователей, что облегчает совместное использование ресурсов ЛВС.
Программу-сервер следует запустить на каждой ПЭВМ, где намереваются вести обработку. Эта программа обязательно должна иметь один параметр — уникальный номер порта (целое число, начиная от 500). Второй параметр (необязательный) — время искусственной задержки в миллисекундах для обеспечения синхронизации между EXE-файлами (по умолчанию 2000). При постоянной эксплуатации данного комплекса рекомендуется внести запуск сервера (или серверов) в меню Start каждой ПЭВМ или оформить их как сервисы Windows NT/2000/XP:
server.exe Н омерПорта[, ВремяИскусственнойЗадержки]
Программа-клиент должна находиться на компьютере, с которого будет осуществляться управление пакетом. Она имеет следующие параметры:
- Имя файла со списком программ для запуска (может отсутствовать; по умолчанию "adr_best.cfg").
- Имя файла со списком IP-адресов ЛВС (может отсутствовать; по умолчанию "prm_best.cfg").
- Время искусственной задержки (необязательный параметр).
Программа-клиент (диспетчер) управляется при помощи меню или кнопок, находящихся в верхней части окна. С помощью кнопки Запустить программу в сети или опции меню Начать обработку сетевая программа запускается на выполнение. Посредством кнопки или опции меню Сохранить параметры можно записать в файлы изменения, произведенные в списках программ с параметрами для запуска и адресов с номерами портов.
Строка списка программ для запуска состоит из имени EXE-файла с сетевым путем и параметрами через разделитель. Строка списка IP-адресов состоит из IP-адреса вычислительного узла сети и номера WinSock-порта, уникального в рамках одной ПЭВМ. Списки можно редактировать из программы-клиента: клавиша Ins — вставить новый элемент в список; клавиша Del — удалить элемент. Допускается редактирование списков любым автономным средством, например редактором текстового файла. На рис. 1 приведена форма программы-клиента с заданием на запуск трех одинаковых стандартных программ калькулятор на двухпроцессорной ПЭВМ с IP-адресом 127.0.0.1 и портами 501 и 502.
Выход из программы-клиента осуществляется нажатием соответствующей кнопки или выбором опции меню.
Программа-сервер стартует в минимизированном виде. Для завершения работы необходимо, находясь в окне программы, нажать правую клавишу мыши и выбрать опцию всплывающего окна Close или нажать Alt-F4. Фрагменты кода клиента и сервера представлены в листинге 1 и листинге 2 соответственно.
Суть протокола обмена данными между клиентом и сервером очень проста. Клиент посылает серверу команду, содержащую строку с именем выполняемого файла, который серверу необходимо вызвать. В строке могут быть знаки, отделяющие имя выполняемого файла от возможных параметров, с которыми приложение вызывается на выполнение . Сервер осуществляет необходимые действия и в качестве отклика для экземпляра клиента посылает произвольную текстовую строку, единственное предназначение которой — синхронизация действий, выполняемых клиентским приложением. Ниже приводится отвечающий за эту функциональность код сервера, в котором подчеркнуты операторы обмена между клиентом и сервером:
with AThread.Connection do try LCommand := ReadLn; // ожидание команды от клиента {старт вторичного процесса и ожидание его завершения} GetStartupInfo(StartupInfo); CreateProcess(Nil,PChar(LCommand),Nil,Nil,False, CREATE_DEFAULT_ERROR_MODE,Nil,Nil,StartupInfo,ProcessInfo); WaitForSingleObject(ProcessInfo.hProcess,INFINITE); ExitCode:=0; GetExitCodeProcess(ProcessInfo.hProcess,ExitCode); WriteLn('0'); // отклик для синхронизации на клиентском приложении finally Disconnect; end; //with
Собственно, это почти что весь серверный код. Если бы в нем не было оператора WriteLn('0'), то в клиентской части необходимо было бы организовывать синхронизацию (ожидание завершения запущенного на серверной ПЭВМ задания) какими-либо другими средствами. В нашем случае клиент просто посылает команду посредством оператора WriteLn и ожидает получения данных при помощи ReadLn, причем с полученными данными он ничего не делает:
with IdTCPClient do begin Connect; try WriteLn(FCustomerInfo); // команда для клиента S := ReadLn; // ожидание отклика от сервера Synchronize(UpdateList); finally Disconnect; end; // try finally sleep(TimeSleep); end; //with
Сеанс связи между экземпляром клиента и сервером заключается в передаче клиентом серверу содержимого поля FcustomerInfo типа string. Сервер записывает эти данные в переменную Lcommand, также типа string. Выполнив необходимые действия, сервер передает клиенту фиктивную строку ‘0’, которую клиент помещает в рабочую переменную S.
Поскольку клиентская программа, согласно постановке задачи, отвечает за множественные соединения с серверами, она выполнена как многопоточное приложение. Отличие от вышерассмотренного случая состоит в том, что в методе Execute отсутствует какое-либо «зацикливание» и не требуется никакой терминальной обработки. Код написан линейно:
ThreadArray[i] := TTCPClientThread.Create(True); with ThreadArray[i] do begin … Port := StrToInt(S); CustomerInfo := IntToStr(i)+' '+CheckListBox2.Items[i]; Resume; … end; //with
Создается экземпляр клиентского класса с отложенным выполнением; полям объекта передаются данные, необходимые для работы; управление передается методу Execute.
Основная сложность клиентского кода сосредоточена в процедуре DoIt, определяющей стратегию сетевой обработки. Процедура состоит из цикла повторяющихся действий. На основании списка IP-адресов с номерами портов вычисляется количество сетевых узлов, которые будут участвовать в обработке, и создается необходимое количество экземпляров клиентских классов. В качестве параметров им передаются данные из списка заданий на выполнение. Управление передается методом Execute для всех экземпляров, а затем ожидается их завершение с последующим высвобождением захваченных ресурсов.
Стратегия обработки строится следующим образом. Предполагается, что количество элементов в списке адресов всегда меньше или равно количеству элементов списка заданий; в противном случае список адресов искусственно обрезается. Если количество элементов в списках различается в разы, организуется единовременная обработка порций в соответствии с длиной списка адресов. Последняя порция может быть меньше, если длины списков не делятся нацело. Таким образом, поддерживается своего рода волнообразное заполнение предоставляемых ресурсов.
В статье «Параллельные алгоритмы» (см. КомпьютерПресс № 3’2001) приводится описание «пускача удаленных заданий» в клиент-серверных системах, построенных на сокетах. Приложения имеют аналогичный визуальный интерфейс, но строятся на сообщениях Windows, которые они направляют сами себе при помощи функции PostMessage, а также на компонентах TClientSocket и TServerSocket — для общения друг с другом. К достоинствам данной системы можно отнести небольшой размер и простоту кода клиента. Недостатков у нее, по сравнению с только что рассмотренным подходом, намного больше. Во-первых, при одновременном обращении несколько серверов к клиенту (диспетчеру) нарушается его функциональность, что в ряде случаев подрывает надежность работы системы. Во-вторых, если время обработки одного узла оказалось меньше времени загрузки вычислительной ЛВС, происходит потеря вычислительных узлов, поскольку работают только компьютеры с IP-адресами, которые стоят первыми в списке. В-третьих, если попытаться воспользоваться системой при отсутствии сетевой поддержки, появится сообщение, изображенное на рис. 2, что мешает проведению отладки.
В-четвертых, поскольку в компоненты TClientSocket и TserverSocket не встроена поддержка блокирующих вызовов, то отсутствует потенциальная возможность использования одних и тех же серверов для обеспечения работы различных экземпляров приложения-клиента.
Сравнение означенных двух вариантов исполнения клиент-серверного приложения, обеспечивающего кластерные вычисления, позволяет проанализировать методы решения сетевых задач, характерные для UNIX и Windows. Дело в том, что Windows 3.11 была построена на принципах корпоративной многозадачности, требующей для своего нормального функционирования организации событийного управления в приложениях, например, при помощи функции PostMessage. В то время подходы, используемые в UNIX, подвергались критике со стороны апологетов Windows за их, якобы, медлительность. И действительно: в среднем быстрее передать управление обработчику какого-то события, реализовав в нем короткий код, чем выполнять блокирующий вызов, напоминающий файловые операции. Однако при этом проблемы синхронизации приложения в целом ложатся на плечи разработчика. Времена изменились — ОС Windows давно уже поддерживает вытесняющую многозадачность. Прежние споры забываются, но груз старых технологических решений остается. Таким образом, на примере использования сетевых компонентов Delphi 6 можно наблюдать возможность фактического использования двух различных технологий программирования.
Подводя итог обсуждения функциональности рассмотренных приложений, необходимо отметить, что в них решены не все проблемы, которые могут возникать в приложениях подобного рода. Так, не поддерживается возможность удаления запущенных заданий; безопасность (в смысле доступа к ресурсам) ничем не обеспечивается. Но эта тема следующих публикаций.
КомпьютерПресс 4'2002