Учебный пример: Жизненный цикл серверных методов

By: Vsevolod Leonov

Abstract: В ходе выполнения примера данной статьи мы изучим различные варианты управления жизненным циклом экземпляра класса для серверных методов DataSnap

    Введение

Ключом к масштабируемости решений на основе DataSnap является управление жизненным циклом экземпляра серверных методов. В архитектуре DataSnap клиенты удаленно запускают открытые методы так называемого «класса серверных методов» (“server methods classes”). Программисту не нужно заботится о создании и удалении экземпляра серверного класса. Нужно лишь сообщить, каким будет типа жизненного цикла. По-умолчанию жизненный цикл методов сервера – «сессия» (“session”), что означает создание отдельного экземпляра класса серверных методов для каждого подключенного клиента, а также автоматическое его удаление при отключении клиента. Это подобно концепции «stateful» («с состоянием») в Enterprise Java Beans (EJBs). Другой возможной опцией для жизненного цикла является опция «сервер» (“server”). В этом сценарии все клиенты вызывают методы одного и того же экземпляра серверных методов. Удостоверьтесь, что ваша реализация экземпляра «серверных» методов является потоково-безопасной, а эти методы могут вызываться одновременно из разных потоков. Последняя опция жизненного цикла для экземпляра серверных методов является «вызов» (“invocation”). Это представляет собой самую масштабируемую возможность, т.к. экземпляр класса серверных методов создается на время вызова метода. Экземпляр создается непосредственно перед вызовом метода и уничтожается, когда вызов метода завершается. Данным механизм подобен концепции «stateless» («с поддержкой текущего состояния») в EJBs.

В этом учебном примере мы собираемся использовать Delphi XE для создания простого примера, демонстрирующего различные варианты жизненного цикла, с помощью клиентского и серверного приложения. Сначала мы собираемся проверить типа сценария жизненного цикла по-умолчанию, когда каждый подключенный клиент имеет свой собственный экземпляр класса серверных методов внутри сервера. Во второй части примера мы поработаем с двумя опциями жизненного цикла – “server” и “invocation”.

    Создание «клиентского» и «серверного» проектов

Начнем с создания сервеного приложения DataSnap.

Кликните на пункте меню «File – New – Other». Внутри диалога «New Items» кликните два раза на иконке мастера «DataSnap Server» в катеогии «Delphi Projects – DataSnap Server», чтобы создать независимое серверное приложение Delphi DataSnap.

Hide image

На первой странице мастера оставим тип проекта «VCL Forms Application» по-умолчанию и на второй странице снимем галочку для опции генерации демонстрационных методов (generate sample methods).

Hide image

Оставьте по-умолчанию остальные опции в данном мастере и нажмите на кнопку «Finish».

Теперь настало время сохранить проект. Выберите «File – Save All». Теперь вы можете сохранить ваши файлы в любом удобном для вас месте и дать им любые имена. Для всех файлов в этом эпизоде я создал папку «C:\DataSnapLabs\ServerClassesLifecycle». Первый файл для сохранения представляет собой модуль с главной формой приложения, поэтому я назвал его «FormServerUnit». Далее я оставил имена по-умолчанию для классов серверных методов и для модуля серверного контейнера. Весь проект целиком я назвал как «LifecycleServer». Опять – имя можно задать любое.

Так как я собираюсь использовать несколько активных окон на экране в рамках данного примера, я изменю заголовок формы сервера как «Lifecycle Server» и минимизирую его размер, поскольку на нем не предполагается размещать визуальные компоненты. Большинство типов серверных приложений, таких как веб-приложения или консольные приложения, вообще не имеют окон…

Теперь давайте быстро добавим клиентское приложение для нашего сервера. Кликнем правой кнопкой мыши в Project Manager на верхнем узле, который представляет собой проектную группу, и выберем «Add New Project». В диалоге «New Items» выберем приложение Delphi VCL Forms и кликнем на ОК. Сохраним всё. Сохраним главную форму приложения как «FormClientUnit», а проект как «LifecycleClient». Саму проектную группу сохраним как «Lifecycle».

Сделано. Теперь вернемся к серверному проекту. Как всегда, только один проект может быть активным в IDE. Убедитесь, что активным в IDE является серверный проект. Самый простой способ сделать проект активным – это выполнить двойной клик на его имени в Project Manager. Теперь активный проект отображается «жирным» шрифтом в Project Manager, а его имя вынесено в заголовок окна IDE.

Hide image

    Реализация серверных методов

В мастере «DataSnap Wizard» мы решили не добавлять демонстрационных методов к серверному классу, так что он теперь абсолютно пустой и не содержит методов. Определение клиентского интерфейса в архитектуре DataSnap очень простое. В реализации серверного класса любых методов публикация любых методов в разделе public или published автоматически делает их доступными для клиентов. Это является одним из наиболее сильных сторон DataSnap и делает весь процесс очень динамичным. Для того чтобы создать клиента в любой распределенной технологии, обычно необходим некий документ, описывающий интерфейс сервера, или какой либо другой способ получения информации об именах доступных методов, типах и именах их параметров, возвращаемых значениях и т.д. В технологии DCOM существует концепция библиотеки типов (type library), в CORBA существует IDL, а для SOAP Web Services существует WSDL. Разработка клиентов/серверов DataSnap концептуально близка к генерации прокси для сервисов soap (soap services proxies) на базе запущенных приложений веб-сервисов и задания пути «\wsdl» для получения WSDL во время работы (runtime). При использовании DataSnap нам нужно иметь доступ к запущенному экземпляру сервера DataSnap. Именно так. Вы получите всю необходимую информацию во время работы (runtime).

В процессе создания сервера DataSnap всегда желательно иметь тестовое клиентское приложение и с его помощью проверить сервер. Здесь не нужно откладывать реализацию тестового клиента на последний момент. Если реализация серверных методов изменяется в процессе разработки, нужно лишь перегенерировать клиентские прокси так, чтобы синхронизировать клиентский и серверный проекты.

Для демонстрационных целей давайте добавим в реализацию класса серверных методов public метод «GetID», возвращающий уникальный идентификатор. Тут клиентское приложение сможет определить, с каким экземпляром класса серверных методов оно взаимодействует.

Я добавил модуль небольшой утилиты, которая содержит одну глобальную функцию, вызов которой является оболочкой для функции ОС, генерирующей Globally Unique Identifiers (GUID).

Выберите «File – New – Unit». Сохраните новый модуль как «GUID_utils» и замените его содержимое на следующий текст:

unit GUID_utils;

interface

function GetNewGUID: string;

implementation

uses
  ActiveX, SysUtils;

function GetNewGUID: string;
var aGUID: TGUID;
begin
  CoCreateGUID(aGUID);
  Result := GUIDToString(aGUID);
end;

end.

Листинг 1: GUID_utils.pas.

Мы собираемся использовать данную функцию для реализации класса серверных методов.

Переключитесь на «ServerMethodsUnit2» (или какой там у вас номер) и замените его содержимое на код:

unit ServerMethodsUnit2;

interface

uses
  SysUtils, Classes, DSServer;

type
{$METHODINFO ON}
  TServerMethods2 = class(TComponent)
  private
    FID: string;
  public
    constructor Create(AOwner: TComponent); override;
    function GetID: string;
  end;
{$METHODINFO OFF}

implementation

uses GUID_utils;

{ TServerMethods2 }

constructor TServerMethods2.Create(AOwner: TComponent);
begin
  inherited;
  FID := GetNewGUID;
end;

function TServerMethods2.GetID: string;
begin
  Result := FID;
end;

end.

Листинг 2: ServerMethodsUnit2.pas.

Класс серверного метода содержит одно private-поле строкового типа «FID», переопределенный конструктор, в котором мы инициализируем «FID» за счет полученного GUID, а public-метода не принимает никаких аргументов, но просто возвращает значение, хранящееся в поле FID.

А как теперь DataSnap «знает», что класс «TServerMethods2» содержит серверные методы, которые нужно экспонировать клиентам? Это как раз и задача специального компонента «TDSServerClass», которые реализует связь между главным компонентом «TDSServer» и любым классом серверных методов. В одном приложении-сервере DataSnap всегда присутствует один компонент «TDSServer», несколько транспортных компонентов, посредством которых «TDSServer» связывается с клиентами и несколькими компонентами «TDSServerClass». Этот компонент обеспечивает событие «OnGetClass», когда программист может задать класс серверного метода, экземпляр которого должен быть создан при поступлении запроса от клиента.

Hide image

Мастера серверов DataSnap всегда генерируют реализацию события «DSServerClass.OnGetClass».

procedure TServerContainer2.DSServerClass1GetClass(
  DSServerClass: TDSServerClass; var PersistentClass: TPersistentClass);
begin
  PersistentClass := ServerMethodsUnit2.TServerMethods2;
end;

Наиболее важная часть понимания в реализации события «OnGetClass» состоит в том, что программист в этом параметре задает тип ссылки, а не ссылку на экземпляр. В архитектуре DataSnap программист не выполняет непосредственный контроль над жизненным циклом серверного класса. Для этого существует свойство «Lifecycle» в компоненте «TDSServerClass», которое декларативно управляет временем жизни. По-умолчанию значение данного свойства «DSServerClass1.LifeCycle» является «Session».

Hide image

Теперь настало время откомпилировать и запустить приложение, так как нам нужен запущенный экземпляр сервера, чтобы реализовать клиента.

Соберем все проекты (Build all projects). Удостоверьтесь, что серверный проект является активным в IDE, и кликните на кнопку с зеленой стрелкой для запуска сервера вне отладчика. Минимизируйте окно сервера приложений.

    Тестирование жизненного цикла по схеме «сессия» («session»)

Кликните два раза на проекте «LifecycleClient» в менеджере проектов для его активации.

Откройте клиентскую форму и добавьте одну кнопку и одну метку на нее. Очевидно, самый простой путь добавления компонентов на форму – это сервис «IDE Insight». Просто нажмите F6 (или «Ctrl+.») и просто начните вводить имя компонента, который вы хотите найти. Измените заголовок кнопки на «Get ID».

Добавьте на форму компонент «TSQLConnection».

«TSQLConnection» представляет собой невизуальный компонент, который обеспечивает подключение к драйверам DBExpress. Фреймворк DBExpress полностью перестроен еще в Delphi 2007. В Delphi 2009 подключение к серверам DataSnap реализовывалось в виде драйвера DBX4, который использовал соединение не с сервером СУБД, а сервером DataSnap. С точки зрения клиента экземпляр сервера DataSnap выглядит очень похоже на сервер баз данных, а серверные методы выглядят очень похоже на хранимые процедуры.

Выберите компонент «TSQLConnection1» на форме. Измените его свойство «Driver» на «DataSnap». Обратите внимание, теперь вы можете раскрыть свойство «Driver» в инспекторе объектов и задать свойства, специфичные для DataSnap.

Hide image

Желательно установить свойство «SQLConnection1.LoginPrompt» в значение «False» во избежание появления формы входа при запуске клиента.

Если сервер запущен, а все основные параметры сконфигурированы корректно, можно поставить свойство «Connected» в «True», чтобы соединить клиента в режиме design-time с работающим сервером.

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

Кликните правой кнопкой на компоненте «SQLConnection1» и выберите «Generate DataSnap Client Classes» из контекстного меню.

Hide image

Эта команда сгенерирует новый модуль с клиентскими классами DataSnap, которые будет использованы в клиентском приложении. Сохраните этот модуль как «proxy».

// 
// Created by the DataSnap proxy generator.
// 2/14/2011 10:39:48 PM
// 

unit proxy;

interface

uses DBXCommon, DBXClient, DBXJSON, DSProxy, Classes, SysUtils, DB, SqlExpr, DBXDBReaders, DBXJSONReflect;

type
  TServerMethods2Client = class(TDSAdminClient)
  private
    FGetIDCommand: TDBXCommand;
  public
    constructor Create(ADBXConnection: TDBXConnection); overload;
    constructor Create(ADBXConnection: TDBXConnection; AInstanceOwner: Boolean); overload;
    destructor Destroy; override;
    function GetID: string;
  end;

implementation

function TServerMethods2Client.GetID: string;
begin
  if FGetIDCommand = nil then
  begin
    FGetIDCommand := FDBXConnection.CreateCommand;
    FGetIDCommand.CommandType := TDBXCommandTypes.DSServerMethod;
    FGetIDCommand.Text := 'TServerMethods2.GetID';
    FGetIDCommand.Prepare;
  end;
  FGetIDCommand.ExecuteUpdate;
  Result := FGetIDCommand.Parameters[0].Value.GetWideString;
end;


constructor TServerMethods2Client.Create(ADBXConnection: TDBXConnection);
begin
  inherited Create(ADBXConnection);
end;


constructor TServerMethods2Client.Create(ADBXConnection: TDBXConnection; AInstanceOwner: Boolean);
begin
  inherited Create(ADBXConnection, AInstanceOwner);
end;


destructor TServerMethods2Client.Destroy;
begin
  FreeAndNil(FGetIDCommand);
  inherited;
end;

end.

Листинг 3: proxy.pas.

Для того чтобы вызвать серверные методы из клиентского приложения, нужно создать экземпляр сгенерированного клиентского класса, который имеет то же имя, что и серверный класс, но с добавлением «Client» в конце.

Реальное подключение к серверным методом действительно обеспечивает компонент «SQLConnection1» на форме. Именно поэтому конструктор клиентского класса принимает параметр «DBXConnection», который представляет собой свойство класса «TSQLConnection» и реальный класс, ответственный за подключение на глубоком «до-компонентном» слое.

Переключитесь на главную форму клиента и из меню «File» выберите «Use Unit» для добавления сгенерированного модуля прокси в раздел «uses» модуля формы.

Кликните на компонент «кнопка» формы и реализуйте следующее событие «OnClick», которое создает прокси, вызывает его метод «GetId», отображает полученное значение в компоненте «мета» и уничтожает клиентский прокси.

procedure TForm3.Button1Click(Sender: TObject);
var aClient: TServerMethods2Client;
begin
  aClient := TServerMethods2Client.Create(SQLConnection1.DBXConnection);
  try
    Label1.Caption := aClient.GetID;
  finally
    aClient.Free;
  end;
end;

Выполните операцию «Save All» и запустите клиентское приложение без отладчика, кликнув на кнопку с зеленой стрелкой.

Если кликнуть на кнопку, то отобразиться глобальный идентификатор экземпляра класса серверных методов, запущенного внутри удаленного сервера приложений DataSnap.

Если кликнуть на кнопку несколько раз, вы увидите, что идентификатор не изменился.

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

Hide image

    Тестирование жизненного цикла по схеме «сервер» («server») и «вызов» («invocation»)

Давайте теперь протестируем другие схемы организации жизненного цикла в рамках созданных клиентов и серверов, определяемых свойством «TDSServerClass.LifeCycle».

Закройте все клиентские приложения и сервер.

Начнем с добавления еще двух компонентов «TDSServerClass» на сервер, таким образом, у нас будет три компонента серверных классов для всех возможных вариантов жизненного цикла.

Выберите компонент «DSServerClass1» на форме, обратитесь к инспектору объектов и измените свойство «Name» на «DSServerClass_Session».

Убедитесь, что компонент выбран в дизайнере; нажмите Ctrl-C и два раза Ctrl-V для добавления еще двух компонентов серверного класса в контейнер.

Правильным образом переименуйте эти компоненты как «DSServerClass_Server» и «DSServerClass_Invocation», а затем измените их свойства «Lifecycle» соответствующим образом.

Hide image

Если вы попытаетесь запустить серверный проект на этой стадии на выполнение, то вы получите ошибку, что класс «DSServerMethods2» уже добавлен к списку серверных методов.

Здесь вопрос в том, что имена такова архитектура DataSnap. Других вариантов нет. Различные компоненты «TDSServerClass» должны ссылки на различные классы в событии «OnGetClass». Вы не можете напрямую использовать одну и ту же реализацию методов серверного класса для различных компонентов «TDSServerClass».

Чтобы учебный пример был понятным, мы сделаем так, чтобы не было конфликтов имен классов серверных методов.

Выберем модуль класса серверных методов и сохраним его как «ServerMethodsUnitSession» и поменяем имя серверного класса на «TServerMethodsSession».

Перейдем в модуль класса серверных методов и сохраним его как «ServerMethodsUnitSessions» и поменяем имя серверного класса на «TServerMethodsSession».

Теперь мы будем реплицировать функциональность серверного класса просто изменяя имена серверных классов.

Добавим в проект новый модуль. Назовем его «ServerMethodsUnitServer».

Скопируем и заменим реализацию класса серверных методов «ServerMethodsUnitSession» на «ServerMethodsUnitServer», сохраняя название модуля.

Добавим в проект новый модуль. Назовем его «ServerMethodsUnitInvocation».

Скопируем и заменим реализацию класса серверных методов, сохраняя название модуля.

На это стадии менеджер проектов выглядит так:

Hide image

Последней нашей задачей является реализация события «OnGetClass» для всех трёх компонентов. Добавьте все новые в раздел uses модуля серверного контейнера.

Из-за того, что мы копировали компоненты серверных классов, все три из них указывают на один и ту же реализацию «OnGetClass». Если вы хотите задать различные обработчики событий, просто удалите содержимое этого свойства на закладке «Events» инспектора объектов и дважды кликните на пустой реализации.

Раздел реализации модуля контейнера должен выглядеть следующим образом:

// …

implementation

uses
  ServerMethodsUnitSession,ServerMethodsUnitInvocation,
  ServerMethodsUnitServer;

{$R *.dfm}

procedure TServerContainer2.DSServerClassInvocationGetClass(
  DSServerClass: TDSServerClass; var PersistentClass: TPersistentClass);
begin
  PersistentClass := TServerMethodsInvocation;
end;

procedure TServerContainer2.DSServerClass_ServerGetClass(
  DSServerClass: TDSServerClass; var PersistentClass: TPersistentClass);
begin
  PersistentClass := TServerMethodsServer;
end;

procedure TServerContainer2.DSServerClass_SessionGetClass(
  DSServerClass: TDSServerClass; var PersistentClass: TPersistentClass);
begin
  PersistentClass := TServerMethodsSession;
end;

end.

Теперь сервер готов. Выполним сборку всех проектов и запустим сервер без отладчика.

Последняя доработка будет касаться клиента.

Переключитесь на клиентский проект. Выберите компонент «SQLConnection1» и, если его свойство «Connected» установлено в «true», измените его на «false», а затем обратно в «true», чтобы убедиться в том, что мы подключены к последней версии сервера.

Кликните правой кнопкой мыши на компоненте «SQLConnection1» и выберите из контекстного меню «Generate DataSnap Client Classes» для перегенерации исходного кода прокси. Сохраните модифицированный модуль «прокси». Обратите внимание, что для каждого класса серверных методов существует соответствующий сгенерированный клиентский класс, так что теперь вместо одного клиентского прокси-класса мы имеем три.

Переключитесь на главную форму клиентского приложения. Добавьте две дополнительные кнопки и две метки, а также добавьте код для вызова серверных классов для различных кнопок и отображения результатов в метках.

procedure TForm3.ButtonInvocationClick(Sender: TObject);
var aClient: TServerMethodsInvocationClient;
begin
  aClient := TServerMethodsInvocationClient.Create(
    SQLConnection1.DBXConnection);
  try
    LabelInvocation.Caption := aClient.GetID;
  finally
    aClient.Free;
  end;
end;

procedure TForm3.ButtonServerClick(Sender: TObject);
var aClient: TServerMethodsServerClient;
begin
  aClient := TServerMethodsServerClient.Create(
    SQLConnection1.DBXConnection);
  try
    LabelServer.Caption := aClient.GetID;
  finally
    aClient.Free;
  end;
end;

procedure TForm3.ButtonSessionClick(Sender: TObject);
var aClient: TServerMethodsSessionClient;
begin
  aClient := TServerMethodsSessionClient.Create(
    SQLConnection1.DBXConnection);
  try
    LabelSession.Caption := aClient.GetID;
  finally
    aClient.Free;
  end;
end;

Когда вы запустите два экземпляра клиентского проекта без отладчика, вы сможете проверить:

  • Оба клиента соединены с тем же экземпляром с жизненным циклом «сервер», который не изменяется (сколько ни кликай кнопки);
  • Для каждой сессии в соответствующем режиме создается отдельный экземпляр, который не изменяется;
  • Каждый раз, когда вы кликаете на кнопку «вызов» («invocation»), вы получаете новый идентификатор, что означает запрос каждый раз к новому экземпляру класса серверных методов.

Это именно то, что мы добивались!

Hide image

    Итог

В этом уроке учебных примеров «Delphi Labs» мы рассмотрели различные опции для организации жизненного цикла класса серверных методов в DataSnap XE.

Здесь клиент с сервером обменивались информацией посредством протокола TCP/IP. Каждый экземпляр серверных методов получал свой собственный GUID в конструкторе. С использованием метода «GetID», который возвращал уже сгенерированный ID, клиентское приложение могло проверять, с каким экземпляром серверного класса оно взаимодействует.

Весь исходный код данного учебного примера доступен по адресу http://cc.embarcadero.com/Item/28199

Видеоролик, демонстрирующий реализацию этого примера, можно найти здесь (http://www.youtube.com/watch?v=VI_o6bwIfkM и http://www.youtube.com/watch?v= XcLOm-v7Ing ).

Более подробная информация об учебных примеров в рамках проекта «Delphi Labs» расположена на моем боге (http://blogs.embarcadero.com/pawelglowacki)

Server Response from: ETNASC04