Delphi programming blog
Источник: http://teran.karelia.pro/articles/item_6113.html
 

Знакомство с DataSnap

Опубликовано 10.10.2013 г. 01:17

Бытует мнение, что лень мотивирует программистов к разраобтке ПО. Вот и в моем случае такие сиутации имеют место быть. В этот раз лень сподвигла меня на базовом уровне освоить созадния REST сервисов с использованием DataSnap.

Если кратко об идее, то суть такова. Сервис позволяет удаленно управлять аудиоплеером на компьютере. Удаленно в даном случае значит, управление через браузер, например, на мобильном устройстве. Пока что через браузер, но поскольку уже вышел Delphi XE5 с поддержкой Android, то можно реализовать и мобильное приложение для Android/iOS устройств.

В моем случае клиент - вэб-страница, сервер - Delphi DataSnap REST Application, а третье звено - аудиоплеер AIMP. Непосредственное управление плеером с помощью DLL. Таким образом сервис может подключать несколько библиотек для управления разными плеерами. Но обо всем по порядку.

Взаимодействие сервера и библиотек

Итак, при загрузке приложение сервера просматривает все динамические библиотеки в своей директории. Из каждой библиотеки сервер пытается получить экземпляр интерфейса для управления плеером IPlayerControl.

    IPlayerControl = interface(IInterface)
        [SID_IPlayerControl]

        procedure SetPlayerCallback(cb : IPlayerCallback); stdcall;

        procedure Next();  stdcall;
        procedure Prev();  stdcall;

        procedure Play();  stdcall;
        procedure Pause(); stdcall;
        procedure Stop();  stdcall;

        function getPlayerName():WideString; safecall;
        function getPlayerID():WideString; safecall;

        property PlayerName : WideString read getPlayerName;
        property ID : WideString read getPlayerid;
    end;

Данный интерфейс позволяет передавать простые команды плееру, такие как пауза, начало воспроизведения и т.п. Каждый объект реализующий интерфейс должен возвращать название плеера, и его идентификатор. Для этого предназначены соответствующие методы. Создан базовый класс, реализующий все эти методы, от которого уже наследуются конечные классы. Название и идентификатор назначаются с помощью RTTI атрибутов PlayerNameAttribute и PlayerIDAttribute. Для получения уведомлений от плеера о смене композиции или состояния воспроизведения добавлен интерфейс IPlayerCallback, который при загрузке библиотек передается экземпляру IPlayerControl с помощью метода SetPlayerCallback. В задачи реализованного базового класса входит извлечеение имени и индефикатора из атрибутов, и сохранение ссылки на экземпляр объекта для обратных вызовов IPlayerCallback. Остальные методы виртуальные, но без реализации. Реализация, очевидно, остается за конечными классами.

    TPlayerState  = (psPlaying, psStopPause, psClosed);

    TTrackInfo = record
        Artist : WideString;
        Title  : WideString;
    end;

    IPlayerCallback = interface(IInterface)
        [SID_IPlayerCallback]
        procedure PlayerStateChange(state :TPlayerState ); stdcall;
        procedure PlayerTrackChange(TrackInfo : TTrackInfo); stdcall;
    end;

Интерфейс уведомления имеет всего пару методов: это изменение состояния и изменение композиции. Информация о композиции заполняется с помощью структуры TTrackInfo.

 Реализация библиотеки для AIMP

 Данный плеер имеет весьма простой в использовании API. Заголовчные файлы доступны на Delphi, как и полнофункциональный пример. Плеер кстати (по крайней мере раньше) разрабывался на Delphi, как утверждает Википедия. Суть взаимодействия проста, необходимо найти дескриптор скрытого окна плеера и посылать ему оконные сообщения с командами на воспроизведение и остановку. Для получения уведомлений от плеера необходимо зарегистрировать свой дескриптор окна, передать его уже AIMP-у, и тогда он будет посылать сообщения нам. Реализация функционала так же проста как и сама идея. Класс управления наследуется от упомнутого выше базового - TCustomPlaerControl:

    [PlayerName('AIMP')]
    [PlayerID('aimp')]
    TAimpPlayerControl = class(TCustomPlayerControl)
      strict private
        FNotificationWnd : HWND;
        FWindowFound : boolean;
        FAimp : HWND;

        procedure FindAimpWindow();
        procedure SendAIMPCommand(cmd : integer);
        procedure AIMPNotification(var msg : TMessage);

        function getAimpNowPlaying():TTrackInfo;
      strict protected
      public
        constructor Create();
        destructor Destroy(); override;

        procedure Next();  override; stdcall;
        procedure Prev();  override; stdcall;

        procedure Play();  override; stdcall;
        procedure Pause(); override; stdcall;
        procedure Stop();  override; stdcall;
    end;

Конструктор и деструктор ничего не делают, кроме как создают и разрушают дескриптор окна. Приводить код не буду. Для создания окна используется метод AllocateHwnd, параметром которого является оконная процедура AIMPNotofocation(), и в конце концов дескриптор разрушается с помощью DeallocateHwnd(FNotificationWnd). Все управляющие процедуры (Next, Prev, Play и т.п.) просто вызывают метод SendAIMPCommand с параметром - кодом команды (соответствующие коды естественно приведены в заголовочных файлах, т.е. AIMP_RA_CMD_NEXT и т.д). Отправка сообщения команды происходит с помощью SendMessage(), изначально требуется найти дескриптор окна, если он еще не был найден. Для чего используется следующий код:

procedure TAimpPlayerControl.FindAimpWindow();
begin
    if FAimp > 0  then exit;

    FAimp := FindWindow(AIMPRemoteAccessClass, AIMPRemoteAccessClass);
    if FAimp > 0 then begin
        SendMessage(FAimp, WM_AIMP_COMMAND, AIMP_RA_CMD_REGISTER_NOTIFY, FNotificationWnd);
    end;
end;

procedure TAimpPlayerControl.SendAIMPCommand(cmd: integer);
begin
    FindAimpWindow();
    if FAimp = 0 then
        exit;

    SendMessage(FAimp, WM_AIMP_COMMAND, cmd, 0);
end;

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

Для получения уведомления от плеера используется зарегистрированная ранее оконная процедура AIMPNotification(). Смысл ее также достаточно прост, когда она получает сообщение, то заполняет нужную структуру данных TTrackInfo, и,  используя интерфейс обратного вызова IPlayerCallback, передает сведения на сервер DataSnap:

procedure TAimpPlayerControl.AIMPNotification(var msg: TMessage);
var track : TTrackInfo;
begin
  case msg.WParam of
    AIMP_RA_NOTIFY_TRACK_INFO : begin
            if Assigned(FCAllback) then begin
                track := getAimpNowPlaying();
                FCallback.PlayerTrackChange(track);
            end;
        end;
  end;
end;

 Оставшийся без внимания метод getAimpNowPlaying переводит данные из струкутры AIMP в нашу. В AIMP SDK есть пример. Единственное что хотелось бы туда добавить, это record helper для удобства работы. В противном случае необходима работа с массивами байт, содержащих строки, что в общем-то не удобно. Такой класс помощник кстати с помощью индексов свойств написать можно очень элегантно и компактно.

О сервере DataSnap REST Application

Здесь, если не погружаться в углубленное изучение технологии DataSnap, то весь функционал сервера делается с помощью мастера. Просто запускаем мастер DataSnap REST Application и получаем практически готовый к использованию код. Есть правда несколько моментов. Первый из них заключается в том, что для дальнейшего использования клиентских обратных вызовов необходимо в установить галку "Server Module" в опциях мастера. В этом случае мастер будет создавать отдельный Дата-модуль, на котором расположен основной в DataSnap компонент TDSServer. А также компонент TDSServerClass, единственная задача которого - сообщить серверу тип (метакласс) класса серверных методов. Серверные методы, это те самые методы, которые мы будем удаленно на сервере запускать. При установки опции "Sample Methods" в мастере будет создан демонстрационный класс с парой методов. Чтобы забыть про дата-модуль контейнер DSServer, скажу напоследок, что реализация его (датамодуля) меня удивила.  В то время когда можно было пойти ООП-путем и использовать классовые методы, свойства и классовые конструкторы для инициализации модуля, код в примере использует локальные переменные и глобальные функции для возврата экземпляра TDSModule.
Что касается класса серверных методов. Вся работа здесь построена на использовании механизма RTTI. Поэтому классы серверных методов обязательно снабжаются директивой {$METHODINFO ON}. Мой класс серверных методов прост почти также как и класс из примера. Он имеет всего один метод - передать команду плееру. Параметрами метода является "какому плееру" и "какую команду":

  TSPControl = class(TComponent)
  private
  public
    procedure PlayerAction(player : string; action : string);
  end;

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

procedure TSPControl.PlayerAction(player, action: string);
var ipc : IPlayerControl;
    pa : TPlayerAction;
begin
    ipc := TPlayerCollection.PlayerCollection.Players[player];
    if not assigned(ipc) then exit;

    try
        pa := TPlayerAction(GetEnumValue(typeInfo(TPlayerAction), 'pa' + Action));
        case pa of
            paNext  : ipc.Next();
            paPrev  : ipc.Prev();
            paPlay  : ipc.Play();
            paStop  : ipc.Stop();
            paPause : ipc.Pause();
        end;
    except
    end;
end;

При загрузке экземпляра IPlayerControl, сразу же создается экземпляр класса TPlayerCallback, реализующего IPlayerCallback. Ссылка на интерфейс также хранится все в том же словаре словаре. Эти объекты будут реализовывать обратные вызовы к REST-клиентам. Чтобы передать данные клиенту необходимо просто вызвать метод BroadcastMessage() экземпляра класса TDSServer из дата-модуля. Параметрами метода является строковое имя callback-канала, и второй параметр - передаваемые данные - JSON объект. На вход IPlayerCallback.PlayerTrackChange() поступает структура TTrackInfo,  на основании которой мы создаем JSON-обеъект и отправлем клиенту:

procedure TPlayerCallback.PlayerTrackChange(TrackInfo : TTrackInfo);
var info : TJsonObject;
    p : TJsonPair;
begin
    info := TJsonObject.Create();

    p := TJsonPair.Create('artist', trackInfo.Artist);
    info.AddPair(p);

    p := TJSONPair.Create('title', trackInfo.Title);
    info.AddPair(p);

    DSServer().BroadcastMessage('PlayerControlChannel', info);
end;

 Оставшаяся часть сервера - вэб-модуль. По сути эта часть реализует вэб-интерфейс для доступа к серверу. Вэб-модуль сам по себе представляет наследника дата-модуля. Мастер создает на нем необходимые компоненты, и реализует необходимый код обработки событий. Компоненты делятся на два 3 типа:

  • TDSHttpWebDispatcher и TWebFileDispatcher занимаются внутренней работой по передаче файлов (js,css) клиенту и т.п. хтростями;
  • классы для генерации JavaScript-кода для обращения к серверу со стороны клиента. Здесь TDSServerMetaDataProvider предоставляет информацию о классе серверных методов, т.е. он на основе RTTI обрабатывает класс серверных методов TSPControl и передает информацию экземпляру DSProxyGenerator, который в свою очередь уже генерирует JavaScript-код (или иной), аналогичный серверному. т.е. если на сервере у нас имеется метод TSPControl.PlayerAction, то пара этих классов на выходе создадут нам такой JS код, который позволит писать на стороне клиента js-код "new TSPControl().PlayerAction('aimp', 'play')"; и в результате такого вызова на стороне клиента будет вызван аналогичный метод на стороне сервера. Эти так называемые Proxy-JS-классы генерируеются при компиляции проекта, т.е. каждый раз когда вы меняете классы серверных методов, клиентские файлы также будут обновлены.
  • Третий тип компонентов TPageProducer ответственны за обработку шаблонов html-страниц. Каждый экземпляр связан со своей html-страницей и позволяет проводить пред-обработку шаблона. Тут дело в том, что в шаблоне мы можем использовать подстановочные тэги, которые оформляются в виде "<#tagname>". Когда producer встречает такой тэг, то вызывает событие OnHTMLTag, с помощью которого мы можем заменить этот тэг, на то что нам угодно.

Свой вэб модуль после работы мастера, я оставил почти без изменений. Добавил разве что новый PageProducer для управления страницей шаблона плеера и исправил один косяк мастера. При включении галки Server Module он забывает связать ServerMetaDataProvider с экземпляром TDSServer.

 Клиентская часть (web)

Конечная html страница для управления сервером хоть и является частью вэб-модуля, но таки уже является вэб-клиентом. Поэтому опишу в отдельном параграфе. Вобще сначала у меня была большая проблема стем чтобы заработали callback вызовы сервера. Не знаю в чем была проблема, но проект пришлось пересоздать заново. Работать они в первой версии проекта так и не захотели. Вобще в Delphi оказывается есть более-менее адекватный HTML редактор с автоподстановкой и редактированием свойств в ObjectInspector. Реализация здесь тоже весьма проста. При загрузке документа мы создаем подключение и устанваливем обработку callback-метода с сервера (создаем канал, устанавливаем обработчик). Обработчик этот получает отправленный нами JSON-объект с названием композиции. Реализовано обновление только композиции без состояния плеера.
При нажатии на кнопки управления мы обращаемся к сгенерированным javaScript proxy методам, аналогичным нашим серверным. Приведу тут полный код страницы, хоть он и большеват.

<html>
<head>
<link rel="stylesheet" type="text/css" href="css/main.css" />
<script type="text/javascript" src="js/json2.js"></script>
<script type="text/javascript" src="js/serverfunctionexecutor.js"></script>
<script type="text/javascript" src="js/connection.js"></script>
<script type="text/javascript" src="js/CallbackFramework.js"></script>
<script type="text/javascript" src="<#serverfunctionsjs>"></script>

<script type="text/javascript">

    window.onBeforeUnload = stopCallback;
    var channel = null;

    function startCallback(){
        channel = new ClientChannel("PlayerControlChannel" + new Date().getTime(), "PlayerControlChannel");
        var callback = new ClientCallback(channel, new Date().getTime() + '',
                function(info, dataType){
                    if(info != null && info.created == null){
                        var a = document.getElementById('artist');
                        if(info.artist != ''){
                            a.visible  = true;
                            a.textContent = info.artist;
                        }
                        else{
                            a.visible = false;
                            a.value = "";
                        }
                        var t = document.getElementById('title');
                        t.textContent = info.title;
                    }
                    return true;
                }

            );

        channel.connect(callback);

    }

    function stopCallback(){
        if (channel != null){
            channel.disconnect();
        }
    }

    function onLoad(){
      setConnection('<#host>', '<#port>', '<#urlpath>');
      startCallback();

    }

    function serverMethods(){
      return new <#classname>(connectionInfo);
    }

    function PlayerActionClick(cmd){
        serverMethods().PlayerAction("aimp", cmd);
    }
</script>

</head>
<body onLoad="onLoad();">
    <div id="info">
       <div id="artist"></div>
       <div id="title"></div>
    </div>
    <div>
        <button id="pause" onClick="PlayerActionClick(this.id);"></button>
        <button id="play"  onClick="PlayerActionClick(this.id);"></button>
        <button id="stop"  onClick="PlayerActionClick(this.id);"></button>
        <button id="prev"  onClick="PlayerActionClick(this.id);"></button>
        <button id="next"  onClick="PlayerActionClick(this.id);"></button>
    </div>
</body>
</html>

Вот собствнно и вся реализация моего первого DataSnap REST-сервера. В браузера это выглядит следующим образом:

В браузере телефона все выглядит и работает так же как и в обычном. Теперь запустив музыку в плеере на компьютере дома можно переключать ее с телефона, например, находясь на кухне (в этом собственно цель разработки и была 8) ). Другой варинат использования - запустили на просмотр кино - можно поставить паузу с дивана. Здесь придется написать соответствующую библиотеку для управления плеером.

Замеченный косяк в том, что кажется по прошествии какого-то времяни callback-канал отсоединяется. Но надо полагать это задается настройками, или пересозданием канала.

Дальнейший план

План собственно прост, во-первых немножко добавить функционал (громкость к примеру и т.п.), это быстро. Второе - написать еще пару библиотек для управления другими плеерами (MPHC, iTunes, WinAmp). Третье - сервер запустить в виде службы (сейчас то это VCL приложение). Четвертое - если появится возможсноть разработки под Android (т.е. Delphi XE5), то написать мобильное приложение с аналогичным функционалом (выложить его на Google Play покорить мир и заработать миллион :)) ).

Метки:  DataSnap 

Комментарии

samsim
08.01.2014 в 02:25
Хорошо сделано. Отличное соединение разный сред разработки программного обеспечения.
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно