Мультимедиа-программирование вместе с Red 5 server. Часть 8

January 20, 2010

Трудно привести пример какого-нибудь серьезного приложения, перед которым не стояла бы задача сохранения информации между сеансами работы. И не важно где будет эта информация сохранена: то ли на локальном жестком диске компьютера, то ли на интернет -сервере. В разных приложениях могут меняться требования к объемам хранимой информации, надежности хранилища, возможности параллельной работы с данными нескольких пользователей, поддержке транзакций и т.д. Сегодняшняя статья завершит рассказ о SharedObject-ах. В частности, сегодня я расскажу о том, как можно сохранять состояние “общего объекта” в некотором хранилище в перерывах между отдельными сеансами.

Как вы помните из прошлых статей, SharedObject представляет собой “упаковку” пар “ключ и его значение”. Всякий раз, когда любой из множества клиентов, подключенных к SharedObject-у, изменяет какой-то из его атрибутов, то генерируется событие, рассылаемое всем остальным пользователям. Это событие-оповещение содержит информацию о том, что же поменялось внутри SharedObject-а. И вся эта накопленная внутри SharedObject-а информация автоматически становится доступной любому новому клиенту, как только он подключится к SharedObject-у. Негативный момент в том, что SharedObject “живет” лишь до тех пор, пока от него не отключатся все клиенты. Когда это происходит, то сервер red5 уничтожает SharedObject вместе со всем его содержимым. Такое поведение мне не нравится, и я хотел бы, чтобы содержимое SharedObject имело более продолжительный срок жизни. Например, перед уничтожением SharedObject-а, его содержимое будет сохраняться, а при создании нового SharedObject-а, его содержимое будет восстанавливаться из некоторого хранилища. Еще я хочу избежать проблемы связанной с тем, что при “падении” red5-сервера содержимое всех SharedObject-ов будет утеряно (это происходит т.к. SharedObject-ы хранятся в оперативной памяти). Для того, чтобы экспериментировать с “persistent shared object” я создал простенький flash-клиент, показанный на рис. 1.



Как видите, состоит это flash-приложение из двух кнопок и списка. По нажатию на первую кнопку flash-клиент подключается к red5-серверу и создает SharedObject. Затем, по нажатию на вторую кнопку, будет меняться содержимое SharedObject-а. Список служит для реализации обратной связи: я буду помещать в него текущее содержимое SharedObject-а. Пример не сложный, а после того как мы разберем на его основе нюансы работы с “persistent shared object” вы сможете вернуться, например, к начатому в прошлых статьях примеру с чатом и легко добавить для него возможность сохранения истории сообщений, например, в файле или даже базе данных. Итак, первый пример кода – это mxml-разметка интерфейса flash-клиента:
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
  3. <mx:Script><![CDATA[
  4.  
  5. private var nc:NetConnection;
  6. private var sharedObj:SharedObject;
  7.  
  8. private function doConnect(event:MouseEvent):void {
  9.   nc = new NetConnection();
  10.   nc.objectEncoding = ObjectEncoding.AMF0;
  11.   nc.addEventListener(NetStatusEvent.NET_STATUS, netStatus);
  12.   nc.connect('rtmp://localhost/warmodule/room');
  13. }
  14.  
  15. private function netStatus(event:NetStatusEvent):void{
  16.   if (event.info.code == 'NetConnection.Connect.Success') {
  17.      sharedObj = SharedObject.getRemote("sharedObj", nc.uri, true);
  18.      sharedObj.addEventListener(SyncEvent.SYNC, onSync);
  19.      sharedObj.connect(nc);
  20.   } 
  21. }
  22.  
  23. private function onSync(event:SyncEvent):void {
  24.   log.dataProvider = sharedObj.data.attr?sharedObj.data.attr:[];
  25. }
  26.  
  27. private function doPut(event:MouseEvent):void {
  28.   if (!sharedObj.data.attr)
  29.      sharedObj.data.attr = ['started'];
  30.   else
  31.     sharedObj.data.attr.push('put at ' + new Date ());
  32.   sharedObj.setDirty("attr");
  33. }
  34. ]]></mx:Script>
  35.  
  36.  <mx:VBox paddingTop="10" paddingLeft="10">
  37.   <mx:HBox>
  38.      <mx:Button click="doConnect(event)" label="connect to Red5"/>
  39.      <mx:Button click="doPut(event)" label="modify shared object"/>
  40.   </mx:HBox>
  41.   <mx:List id="log" width="300" height="200" />
  42.  </mx:VBox>
  43. </mx:Application>
Как видите, здесь нет ничего сложного: по нажатию на кнопку “doConnect” я пробую установить соединение с red5-сервером. Затем, когда это соединение было создано, я вызываю метод “SharedObject.getRemote” для того, чтобы получить ссылку на “удаленный объект” и сохраняю ссылку на него в переменную sharedObj. Дальнейшие действия тривиальны: я привязываю свою функцию onSync к событию, рассылаемому red5 при изменении SharedObject-а. А внутри этой функции копирую содержимое переменной “attr” (значением ее является массив строк) внутрь списка log. Функция doPut служит для того, чтобы добавить к текущему содержимому массива “attr” строку с текущей датой и временем. Для проверки того, что приведенный нами пример использования SharedObject-а действительно умеет сохранять свое состояние между сеансами flash-клиентов, сделаем так. Запустим одно единственное окно браузера и откроем в нем адрес http://localhost:5080/warmodule/. После подключения к red5-серверу несколько раз нажмите кнопку “modify shared object”, так чтобы содержимое списка заполнилось сообщениями вида “put at %дата%”. Теперь закройте окно браузера, и хотя вы этого не видите, но red5 уничтожил созданный вами SharedObject вместе с его содержимым. Однако, если заново открыть страничку http://localhost:5080/warmodule/ и нажать на кнопку подключения к red5-серверу, то расположенный внизу диалогового окна список “log” будет заполнен теми сообщениями, которые вы отправили в него в предыдущем сеансе. Для того, чтобы этот пример заработал я абсолютно ничего не делал и не настраивал на стороне java. Вся “магия” заработала лишь благодаря тому, что при создании SharedObject-а я указал третьим параметром функции getRemote значение “true”. Это значение говорит о том, что я хочу получить именно “persistent shared object”:
  1. sharedObj = SharedObject.getRemote("sharedObj", nc.uri, true);
Теперь разберемся с тем, что происходит на стороне red5 сервера и где, вообще, хранятся SharedObject ы? В терминологии red5 к каждому SharedObject-у, а также scope (комнате) присоединен специальный объект-хранилище “store”. В поставке red5 идут только два вида подобных хранилищ: “RamPersistence”и “FilePersistence”. Если мы при создании SharedObject-а явно не указали, что хотим пользоваться услугами “persistence”, то содержимое SharedObject-а будет храниться внутри “RamPersistence”. И, как это следует из его названия, информация хранится в оперативной памяти сервера, а значит теряется при перезапусках сервера или при уничтожении SharedObject-а. “Persistent shared object” пользуется услугами хранилища “FilePersistence”, т.е. хранит информацию внутри файлов. Но вот какой формат этих файлов и где они располагаются? В моем случае, red5 сервер установлен в каталог “E:\Program_Files_2\Red5_0.8”, а веб-приложение называется “warmodule”. Содержимое SharedObject-а будет сохранено внутри файла, расположенного по следующему пути “E:\Program_Files_2\Red5_0.8\webapps\warmodule\persistence\SharedObject\room\sharedObj.red5”. Red5 создает подкаталог “persistence” внутри каталога с вашим веб-приложением. Далее, red5 создает подкаталог с именем “SharedObject”, что говорит о том, информация какого типа будет в нем храниться. Следующий подкаталог “room” совпадает с именем той комнаты (room, scope), к которой мы подключались перед созданием sharedObject-а:
  1. nc.connect('rtmp://localhost/warmodule/room');
И, собственно, имя файла равно имени того, SharedObject-а, что мы указали при вызове метода “SharedObject.getRemote”. Если открыть файл sharedObj.red5, то мы увидим в нем “кракозюбры”, среди которых будут просматриваться относительно понятные фрагменты, содержащие текст сообщений, которые я добавлял внутрь SharedObject-а, когда нажимал на кнопку “modify shared object”. Подсистема сохранения содержимого SharedObject-а в файл тесно связана с остальными подсистемами red5. Так важен ответ на вопрос: умеет ли red5 сохранять SharedObject-ы, содержащие не примитивные переменные (строки, даты, числа), но и сложные пользовательские объекты. В прошлой статье серии я показывал прием, когда создаваемый нами чат хранил историю сообщений не в виде простого массива строк, но в виде объектов класса ChatHistoryItem:
  1. package blz.red5demo {
  2.   [RemoteClass(alias="blz.red5demo.ChatHistoryItem")]
  3.   public class ChatHistoryItem {
  4.     public var user:String;
  5.     public var date:Date;
  6.     public var message:String;
  7.  
  8.     public function ChatHistoryItem() {}
  9.   }
  10. }
Благодаря тому, что на стороне java и на стороне flash существуют классы ChatHistoryItem с одинаковыми именами и одинаковым устройством. То red5 и flash отлично умели конвертировать java-объекты в их flash-аналоги. К счастью, механизм сохранения SharedObject-ов полностью поддерживает пользовательские java и flash классы. При сохранении содержимого объекта в файл red5 сохранит лишь те поля классов, которые имеют модификатор доступа public или имеют специальную пару методов get и set. К слову, если какое-то из полей java-класса вы не хотите сохранять в файл, то его нужно промаркировать аннотацией DontSerialize, например, так:
  1. package blz.red5demo;
  2. import org.red5.annotations.DontSerialize;
  3.  
  4. public class ChatHistoryItem {
  5.   @DontSerialize
  6.   private String user;
  7.   private java.util.Date date;
  8.   private String message;
  9. }
В практике, формат хранения данных в файле и порядок следования полей класса в нем совершенно не важен. Однако, если вдруг вы захотите получить контроль над этими вещами, то в red5 реализована привычная для java-мира идея, когда класс, например наш ChatHistoryItem, реализует интерфейс IExternalizable c парой методов readExternal и writeExternal, внутри которых вы берете на себя ответственность за сохранение содержимого SharedObject-а в файл в том формате, который вам потребуется. Гораздо важнее другое: в red5, несмотря на вышеописанный набор полезных “плюшек” и средств контроля за сохранение содержимого SharedObject-а в файл, совершенно не продуман вопрос контроля за тем где, собственно, хранить эти файлы. Смешно сказать, но логика использования для этих целей каталога “persistence” зашита глубоко внутри red5 кода и разработчики не предусмотрели простого способа указать, что этот каталог должен быть расположен где-то еще, например, на другом жестком диске. Более того, сама идея хранения потенциально “дорогостоящей” информации в виде обычного файла “очень дурно пахнет”. Главной претензией к файлу как хранилищу информации является отсутствие развитых средств контроля за доступом к этому файлу нескольких пользователей одновременно. Для решения этой проблемы red5 использует специальную стратегию отложенного по времени сохранения содержимого SharedObject-а в файл. Если вы запустите мой пример и несколько раз нажмете на кнопку модификации “общего объекта”, то изменения в файл будут записаны не мгновенно, а через несколько секунд. Т.е. когда SharedObject изменяется, то он просит конкретную реализацию своего хранилища FilePersistence сохранить себя в файл. Но FilePersistence делегирует эту работу еще одному классу FilePersistenceThread. Класс FilePersistenceThread существует только в одном экземпляре и используется множеством FilePersistence-хранилищ. Поступающие запросы на сохранение информации помещаются в специальную очередь заданий. Затем каждые 10 секунд эта очередь заданий просматривается и обрабатывается. Гораздо хуже другое: часто сохранение информации внутри SharedObject-а связано с какой-то сложной бизнес-логикой, которая в свою очередь завязана на работу с другим хранилищем информации, например, базой данных oracle или mysql. Т.е. изменение SharedObject-а должно идти в рамках общей транзакции, затрагивающей несколько источников данных (так называемые распределенные транзакции). Например, мы делаем многопользовательскую internet-игру в стиле фэнтези. Информация об игроках, их уровне здоровья, снаряжении в виде всяческих волшебных мечей и топоров хранится в “настоящей” базе данных oracle. Однако для того, чтобы несколько игроков могли видеть характеристики друг друга и взаимодействовать, мы решаем поместить “снимок” их текущего состояния в SharedObject. Предположим, что игрок встретился в пещере со страшным Бармалеем, который убивает игрока. Как только это происходит, мы вызываем специальную процедуру, которая должна изменить содержимое базы данных, поставив напротив фамилии игрока отметку “убит”. Аналогичным образом нужно изменить и содержимое SharedObject-а (а в случаях FilePersistence нужно дождаться момента, когда содержимое SharedObject-а действительно сохранится в файл). Но если в промежуток времени между этими двумя операциями произошел сбой, то содержимое базы данных будет отличаться от содержимого SharedObject-а (после того как red5 сервер был перезапущен и прочитал содержимое SharedObject-а). Такие ситуации нужно отслеживать и иметь способ их разрешения. Отличным решением будет либо отказ от использования “persistent shared object”, либо научить его работать в паре с основным хранилищем данных и в рамках одной транзакции. Итак, главный вопрос: умеет ли red5 работать с другими реализациями хранилищ данных, кроме как RamPersistence и FilePersistence? Да умеет, хотя разработчики red5 изо всех сил старались при этом усложнить жизнь разработчику. Так самым простым способом подменить реализацию хранилища данных, было бы поместить внутрь файла red5-web.xml следующего бина:
  1. <bean id="sharedObjectService" class="org.red5.server.so.SharedObjectService">
  2.  <property name="persistenceClassName" value="blz.red5demo.MyPersistence"/> 
  3. </bean>
Как видите, свойство “persistenceClassName” должно быть равно имени класса, реализующего интерфейс “IPersistenceStore”. Правда этот способ не всегда работает, что связано с особенностями работы java-загрузчика классов. Т.е. класс “blz.red5demo.MyPersistence” должен быть не частью java-классов создаваемого вами приложения, а частью классов самого сервера. Подробно останавливаться на этом варианте я не буду, но если он вас заинтересует, то вы всегда можете уточнить у меня детали по почте. Второй способ подмены хранилища данных, предполагает “ручное” создание SharedObject-а с настройкой используемого им Store. Напомню, что есть два вида SharedObject-ов в зависимости от того, кто инициирует их создание: клиент или сервер (см. прошлую статью серии об этом виде SharedObject-ов). Упомянутый первым способ (с модификацией файла “red5-web.xml”) отлично работает для обоих этих вариантов. А второй способ (пример с которым я сейчас приведу) предполагает, что вы сами внутри расположенного на java-стороне метода “запуск комнаты” создаете SharedObject и подготавливаете его для дальнейшей работы:
  1. public boolean roomStart(IScope room) {
  2.   if (!super.roomStart(room))
  3.     return false;
  4.   Object oldStore = room.getAttribute(IPersistable.TRANSIENT_PREFIX + "_SO_PERSISTENCE_STORE_");
  5.   try {
  6.      DbStore store = new DbStore(room);
  7.      room.setAttribute(IPersistable.TRANSIENT_PREFIX + "_SO_PERSISTENCE_STORE_", store);
  8.      ISharedObject chat = getSharedObject(room, "sharedObj", true);
  9.      return true;
  10.    } 
  11.    finally {
  12.      room.setAttribute(IPersistable.TRANSIENT_PREFIX + "_SO_PERSISTENCE_STORE_", oldStore);
  13.    }
  14. }
Каждая комната (room) содержит специальный атрибут с именем IPersistable.TRANSIENT_PREFIX + "_SO_PERSISTENCE_STORE_". Значением которого является ссылка на используемое данной комнатой хранилище данных. Которое, в свою очередь, присоединяется ко всякому SharedObject-у, созданному внутри этой комнаты. Т.е. я сохранил старое значение хранилища комнаты в переменную oldStore. Затем создал новое хранилище DbStore и привязал его к комнате. После чего я вызвал метод getSharedObject, который и создал новый “persistent shared object” с именем “sharedObj” привязанный к моей реализации хранилища DbStore. Важно не забыть по завершению метода roomStart вернуть назад настройки комнаты (что и делается внутри секции finally). Что касается исходного кода класса DbStore, то я его специально не привожу т.к. он хотя и велик по размеру, но никаких сложностей в реализации не представляет: все сводится к реализации пары методов save и load, вызываемых тогда, когда нужно загрузить и восстановить содержимое SharedObject-а в базе данных. А что касается конкретной стратегии сохранения SharedObject-а, то обычно все сводится к “примитивному” копированию двоичного представления SharedObject-а в специальное BLOB поле (один в один как это делал FilePersistence, но только не в файл, а в базу данных). Создавать же какой-то сложный и “умный” анализатор содержимого SharedObject-а с последующей раскладкой этой информации по отдельным нормализованным таблицам глупо т.к. содержимое SharedObject-а должно быть подчиненным по отношению к главной информации, а не создавать дублирующуюся структуру.

На этом я завершаю большой раздел, посвященный задачам общения между flash и java, задачам передачи информации в обоих направлениях и задачам работы с SharedObject-ами. В следующей статье я перейду к завершающей части для всей серии статей – задаче захвата flash-клиентом видео и аудио потоков c помощью микрофона и веб-камеры с последующей передачей этой информации на red5-сервер.

Categories: Flash & Flex