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

November 27, 2009

Прошлые статьи были посвящены рассмотрению простейшей схемы взаимодействия между пользователями и приложением, исполняющимся в среде red5. Т.е. я показывал то, как flash-клиент может вызвать любой метод на сервере, и то, как сервер может вызвать метод на стороне клиента. Однако подобное двунаправленное общение между клиентом и сервером - это еще не самое "вкусное", что мы можем сделать с помощью red5. Существует целый класс приложений, в которых ставка сделана на организацию взаимодействия между самими клиентами: это организация видеоконференций, многопользовательские игры и т.д. Давайте посмотрим, чем нам может помочь red5 в этом случае.

Предыдущая статья завершилась на том, что я показал, как сервер может вызвать любой метод, расположенный на стороне клиента. И сделать это сервер может в любой момент времени благодаря тому, что подключение клиента к серверу выполняется по протоколу rtmp. Соединение в этом случае является постоянным и сохраняется до тех пор, пока клиент не явно не решит отключиться от сервера. Вы можете легко создать такое red5-приложение, которое сохраняет все клиентские подключения в виде какого-то списка или массива. Затем можно выполнять рассылку по всем этим подключениям извещений о событиях, произошедших с одним из клиентов. Например, для чата, после того как один пользователь ввел текст сообщения и нажал кнопку "отправить на сервер", то сервер выполняет рассылку этого текстового сообщения всем остальным зарегистрированным клиентам. Несмотря на то, что подобное "сделанное" на коленке решение будет вполне работоспособным, мы пойдем другим путем. Дело в том, что задача организации коллективного общения возникла далеко не вчера и в среде red5 есть специальные механизмы для этого (SharedObjects). Понятие SharedObject-ов имеет несколько значений в flash и actionscript и наверняка знакомо любому, даже начинающему, flash-разработчику. Даже если вы никогда ничего не слышали о red5 или flash media server, да и вообще никогда не сталкивались с задачами общения flash-клиента с сервером, то все равно мы могли столкнуться с SharedObject. Дело в том, что существует два вида SharedObject-ов: локальные (local) и распределенные (remote). В соответствии с этим есть три основных сценария использования SharedObject-ов. Во-первых, вы можете использовать SharedObject-ы без red5 сервера только для того, чтобы сохранять некоторую информацию на локальном компьютере клиента между несколькими сеансами работы с приложением. Эти, так называемые "локальные", SharedObject-ы чем-то похожи на доступные во всех браузерах cookie. Технически, для создания локального SharedObject-а вы делаете вызов метода SharedObject.getLocal():
  1. var so:SharedObject = SharedObject.getLocal("some-path");
  2.  so.data.username = "My User Name";
  3.  // а теперь сохраним информацию на диск
  4.  so.flush();
После того, как вы установили набор свойств объекта data и вызывали метод flush, то flash сохранит значение переменной "username" на диск. Для хранения информации с помощью SharedObject-ов изначально выделяется 100 кб места, хотя в случае необходимости этот лимит может быть расширен. К примеру, если вы создаете flash-игру тетрис, то можете сохранять внутри локальных SharedObject-ов таблицу рекордов или настройки игры. Поскольку моя статья рассказывает о задачах организации общения между flash и red5, то я не буду углубляться в рассказ о локальных SharedObject-ах. А вместо этого давайте рассмотрим второй сценарий работы с ними. Продолжая пример с игрой тетрис, мы можем создать общую таблицу рекордов, в которой будут записываться достижения всех, кто играет по Интернет. Такая таблица должна храниться на сервере и должна быть доступна всем клиентам. Если в какой-то момент времени соединение с сервером было утеряно, то ничего страшного: информация внутри SharedObject-а может быть сохранена локально на жесткий диск, а после восстановления соединения будет разослано извещение всем подключенным к серверу клиентам и серверу, о том, что таблица рекордов была обновлена. Третий способ работы с SharedObject-ов предполагает, что информацию не нужно сохранять на сервере, а все что требуется - так это организация обмена сообщениями в режиме реального времени между всеми подключенными к серверу клиентами. В рамках примера с онлайн-игрой тетрис подобное использование SharedObject-ов идеально подходит для организации чата между игроками.

Естественно, что идея "общих удаленных объектов" появилась задолго до red5 - еще в версии flash mx (flash 6). Тогда на стороне сервера RemoteObjects поддерживались единственным на тот момент времени медиа-сервером от macromedia|adobe (Adobe Flash Media Server). Итак, подводя итог SharedObjects - это механизм синхронизации информации между несколькими клиентами, а также способ для сохранения информации между несколькими сеансами либо на сервере, либо на локальном компьютере клиента. SharedObjects являются "общими" не для всех клиентов нашего приложения вообще, а для тех из них, кто подключен к одной "области" (room или scope). Давайте вернемся немного назад к тому моменту времени, когда я приводил пример адреса, по которому flash-клиент может подключиться к red5-серверу. Вот пример подобного URL-а - "rtmp://localhost/warmodule". Здесь "warmodule" - это имя веб-приложения. Но приложение может быть очень большим, предоставлять различные наборы функционала и различным группам пользователей. Например, приложение для онлайн-игр в шашки может предоставлять возможность создать игровую комнату, затем присоединить к этой комнате пару игроков и изолировать эту комнату от всех остальных комнат. Игроки, зашедшие в одну и ту же комнату, получают возможность совместно играть, обмениваться аудио-сообщениями, картинками и т.д. Вот пример еще одного адреса для подключения к red5 приложению: "rtmp://localhost/warmodule/games/chess/room12". Здесь говорится, что внутри приложения "warmodule" есть иерархия "комнат" (они же области видимости). Комната "games" содержит внутри себя комнату "chess", внутри которой находится уже конкретная комната с игроками "room12". Т.е. комнаты могут быть организованы в виде иерархического дерева; и создание всей этой иерархии не требует никаких дополнительных настроек на стороне red5-сервера. Далее, в каждой комнате может находиться отдельный SharedObject, доступный всем клиентам, зашедшим в эту комнату и пославшим запрос на подключение к "общему объекту". Для демонстрации того, как работать с SharedObject я попробую создать простенький чат. Визуально чат будет выглядеть как текстовое поле с кнопкой, по нажатию на которую выполняется модификация SharedObject-а "история сообщений". Затем работает магия red5, которая отправит извещения всем остальным участникам чата о добавлении сообщения. Это сообщение будет "поймано" и сам текст сообщения будет добавлен к табличке (grid-у) размещенной внизу формы приложения. То, что должно у нас получиться, показано на рис. 1.



Пример я начну с рассмотрения mxml-разметки создающей каркас внешнего вида формы:
  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.   import flash.events.MouseEvent;
  5.   import mx.collections.ArrayCollection;
  6.   import mx.controls.Alert;
  7.  
  8.   [Bindable]
  9.   private var localHistory: ArrayCollection = new ArrayCollection ();
  10.  ]]></mx:Script>
  11.  <mx:VBox paddingTop="10" paddingLeft="10">
  12.   <mx:HBox>
  13.     <mx:TextInput id="txtUser"/>
  14.     <mx:Button click="doConnect(event)" label="connect to Red5"/>
  15.   </mx:HBox>
  16.   <mx:HBox>
  17.     <mx:TextInput id="txtMessage"/>
  18.     <mx:Button click="say(event)" label="Send"/>
  19.   </mx:HBox>
  20.   <mx:DataGrid id="gridMessages" height="275" editable="false" dataProvider="{localHistory}">
  21.     <mx:columns>
  22.        <mx:DataGridColumn dataField="user" width="50" headerText="User"/>
  23.        <mx:DataGridColumn dataField="date" width="50" headerText="Date"/>
  24.        <mx:DataGridColumn dataField="message" width="600" headerText="Message"/>
  25.     </mx:columns>
  26.    </mx:DataGrid>
  27.   </mx:VBox>
  28. </mx:Application>
Общая схема размещения элементов управления на форме не блещет изяществом: корневой элемент VBox служит для вертикального расположения составляющих форму блоков. Так первым идет текстовое поле "txtUser", в которое пользователь должен ввести свое имя, перед тем как выполнить подключение к чату. Само же подключение инициируется по нажатию на кнопку "connect to Red5" с помощью функции doConnect (описание этой и других задействованных в примере функций будет чуть позже). После того как пользователь подключился, он может рассылать сообщения. Содержимое сообщения вводится в текстовое поле "txtMessage" и после того как пользователь нажмет на кнопку отправки ("Send"), то сообщение уйдет на red5-сервер, а затем сам red5 разошлет извещения всем подключенным к серверу клиентам о добавившемся в историю чата сообщении. Нижнюю часть формы занимает таблица (gridMessages) из трех колонок: имя пользователя, дата отправки сообщения и сам текст сообщения. В качестве источника данных для таблицы выступает переменная localHistory (на нее указывает свойство dataProvider). Предполагается, что информация внутри источника записей (массив localHistory) хранится в виде "пачки" записей. Каждая запись должна состоять из трех полей user, date и message (именно этими названиями оперирует свойство "dataField" каждой из колонок таблицы). Хотя, созданную разметку формы можно скомпилировать и запустить на выполнение, но для завершения примера не хватает самого главного - кода функций подключения к SharedObject и обработки события "синхронизация данных с сервером". Все примеры приведенного далее кода нужно будет вставлять в секцию Script, сразу после объявления переменной localHistory:
  1. private var nc:NetConnection;
  2.  
  3. private function doConnect(event:MouseEvent):void {
  4.  
  5.   if (! txtUser.text){
  6.     Alert.show ("Сначала нужно указать ваше имя");
  7.     return;  
  8.   }
  9.   var context : String = application.parent.loaderInfo.parameters['contextName'];
  10.   nc = new NetConnection();
  11.   nc.objectEncoding = ObjectEncoding.AMF0;
  12.   nc.addEventListener(NetStatusEvent.NET_STATUS, netStatus);
  13.   nc.connect('rtmp://192.168.1.2/warmodule/chat');
  14.   txtUser.enabled = false; 
  15. }
Функция doConnect вызывается по нажатию на кнопку "connect to Red5". Сначала она проверяет то, что пользователь указал свое имя для регистрации в чате. Если это так, то создается объект RemoteConnection и устанавливается ссылка на функцию netStatus, которая будет получать сообщения о результатах установления соединения с red5-сервером. Внутри функции netStatus я проверяю что, соединение было успешно установлено (NetConnection.Connect.Success), и тогда создам объект RemoteConnection. Указав как параметры при вызове метода getRemote имя создаваемого объекта и адрес подключения. Третий не обязательный параметр функции getRemote позволяет добавить к SharedObject функциональность по сохранению своего состояния на сервер сервер (persistent).
  1. private var chat : SharedObject;
  2.  
  3. private function netStatus(event:NetStatusEvent):void {
  4.   if (event.info.code == 'NetConnection.Connect.Success'){
  5.      chat = SharedObject.getRemote("chat", nc.uri);
  6.      chat.addEventListener(SyncEvent.SYNC, onSync)
  7.      chat.connect(nc);
  8.   } 
  9. }
По правде говоря, можно выполнять создание SharedObject-а непосредственно внутри функции doConnect, без того, чтобы дожидаться успеха подключения к серверу red5, но на практике так делать не стоит. Далее, после того как SharedObject был создан, я буду получать сообщения "SyncEvent.SYNC" всякий раз, когда происходит изменение в содержимом SharedObject-а. Т.е. всякий раз, когда какой-либо из пользователей (или я сам) меняет содержимое истории сообщений в чате. Сообщение SyncEvent.SYNC придет также и в самый первый раз, когда SharedObject был только что создан. В этот "начальный" момент я хочу послать в чат сообщение "Зашел в чат" от имени пользователя, который это сделал. Я решил блокировать работу чата (блокировать кнопку отправки сообщений) до момента успешного установления соединения с red5 и создания SharedObject. Для этого я создал переменную chatIsReady, значение которой я устанавливаю в true только при получении первого сообщения SyncEvent.SYNC (это значит, после того как SharedObject был создан и подготовлен к работе). Как только это случилось, я должен проанализировать содержимое переменной chat.data.remoteHistory. Дело в том, что событие SyncEvent.SYNC приходит всем пользователям, кто подключается к чату. Т.е. массив с историей сообщений чата должен создать только самый первый клиент, а все остальные должны реиспользовать этот массив. После того, как chat.data.remoteHistory был создан (или не создан) я помещаю в него сообщение "Зашел в чат" и сообщаю SharedObject-у о том, что свойство "remoteHistory" изменило свое содержимое (с помощью метода setDirty). Последнее, что делает функция onSync - это копирование истории сообщений чата внутрь массива localHistory. А раз сам массив "привязан" к компоненту "mx:DataGrid", то на форме отобразятся последние изменения в чате:
  1. private var chatIsReady: Boolean = false;
  2.  
  3.  private function onSync(event : SyncEvent):void {
  4.    if (chatIsReady == false){
  5.       chatIsReady = true;
  6.       if (!chat.data.remoteHistory)
  7.         chat.data.remoteHistory = [];
  8.       chat.data.remoteHistory.push ({user: txtUser.text, date: new Date(), message : 'Зашел в чат'});
  9.       chat.setDirty("remoteHistory");
  10.    }
  11.    localHistory.source = chat.data.remoteHistory;
  12.  }
Для завершения примера мне осталось только привести код функции "отправить сообщение в чат". Здесь тоже нет ничего сложного: после проверки того, что чат готов к работе (переменная chatIsReady) я модифицирую свойство chat.data.remoteHistory и извещаю red5 о том, что содержимое переменной remoteHistory изменилось. После этого red5 выполнит отправку сообщения SyncEvent.SYNC всем подключившимся к SharedObject-у пользователям:
  1. private function say(e: MouseEvent):void {
  2.    if (! chatIsReady){
  3.       mx.controls.Alert.show("Сначала нужно подключиться к серверу");
  4.       return;   
  5.    }
  6.    chat.data.remoteHistory.push (
  7.       {
  8.           user: txtUser.text, 
  9.           date: new Date(), 
  10.           message: txtMessage.text
  11.       }
  12.    );
  13.    chat.setDirty("remoteHistory");
  14.  }
Как видите, работа с SharedObject-ами сводится к модификации какого-либо свойства и созданию специальной функции слушающей извещения от red5 о том, что сделал с SharedObject-ом какой-то из множества пользователей, подключенных к приложению (scope, комнате). Вызов метода setDirty нужен только в том случае, если вы изменяете содержимое сложного объекта или массива. А если вы меняете "примитивное" свойство (например, строку или число), то red5 отправит извещения автоматически. Следующий пример покажет еще один полезный прием работы с SharedObject - вызов одним из клиентов какого-нибудь метода у всех остальных клиентов. Предположим, что мы хотим добавить к чату функцию "exit". Т.е. я добавлю на форму кнопку "выход", по нажатию на которую клиент вызовет у всех остальных клиентов функцию onExit, передав ей как параметр имя пользователя, который хочет покинуть чат. Сначала я добавлю mxml-кнопку на форму:
  1. <mx:Button click="doExit(event)" label="Exit " />
Внутри функции обработчика события "клик по кнопке" я вызову на SharedObject-е метод send, передав ему как параметры, во-первых, имя общего метода, который нужно вызывать. А затем идет произвольной длины список переменных, которые будут переданы вызываемой remote-функции:
  1. private function doExit (e: MouseEvent):void{
  2.   if (! chatIsReady){
  3.     mx.controls.Alert.show("Сначала нужно подключиться к серверу");
  4.     return;
  5.   }
  6.   chat.send("onExit", txtUser.text); 
  7. }
Что касается функции onExit, то я ее описал рядом с функциями doExit, doSay и другими:
  1. private function onExit (...userName):void{
  2.   mx.controls.Alert.show("Получено сообщение о том, что пользователь "+ userName + " покинул чат");
  3. }
Однако, если просто запустить вышеприведенный код, то мы получим сообщение об ошибке: "Не удалось найти свойство onExit в SharedObject; отсутствует значение по умолчанию". Все дело в том, что мы не привязали функцию doExit к самому SharedObject-у. Для этого мне нужно сразу после создания SharedObject-а установить значение его свойства client равному тому объекту, внутри которого будет выполняться поиск "общей" функции (this):
  1. chat = SharedObject.getRemote("chat", nc.uri);
  2. chat.addEventListener(SyncEvent.SYNC, onSync)
  3. chat.client = this;// вот она привязка
  4. chat.connect(nc);
Для проверки можно запустить два браузера, в каждом из них открыть приложение и зарегистрироваться в чате. После того как один из клиентов нажмет кнопку "doExit", то и в первом и втором браузере должно появиться окошко с сообщением (см. рис. 2).



На сегодня все. В следующий раз я расскажу о том, как можно создавать "сохраняемые на сервере" SharedObject-ы, как можно изменять свойства SharedObject-ов с сервера и как написать такой java-код, который будет реагировать на сообщения, рассылаемые из flash-клиента.

Categories: Flash & Flex