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

December 10, 2009

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

Однако перед тем как перейти к теме java-кода, “разговаривающего” с SharedObject, сначала нужно закрыть вопрос о жизненном цикле SharedObject-ов. Т.е. когда “общий объект” создается и когда он уничтожается. Для создания хороших приложений нужно четко понимать в какой момент времени происходит каждое из этих событий и то, от чего они зависят. Например, ответим на вопрос: когда происходит создание SharedObject и можно ли в этот момент выполнить какую-то специальную работу по его подготовке к дальнейшему использованию? Существует два вида SharedObject-ов в зависимости от того, кем они создаются: сервером или клиентом. Показанный в прошлой статье пример чата, демонстрировал методику, когда SharedObject создавался flash-клиентом:
  1. chat = SharedObject.getRemote("chat", nc.uri);
  2. chat.addEventListener(SyncEvent.SYNC, onSync)
Итак, когда какой-либо из flash-клиентов вызывает метод getRemote, то flash обращается к серверу red5 и спрашивает его: вдруг, в рамках текущей комнаты (scope) уже есть SharedObject с запрошенным именем? Если это так, то клиенту возвращается ссылка на этот существующий SharedObject. В противном же случае red5 создает новый “пустой” SharedObject, который будет возвращен и этому и всем последующим клиентам. Всякий раз, когда кто-либо из посетителей чата отсоединяется от SharedObject-а, вызывая на нем метод close или просто закрывая окно браузера с нашим flash-роликом. То на сервер red5 посылается специальное сообщение об этом. И как только абсолютно все клиенты отключатся от “общего объекта”, то сервер red5 может его безопасно удалить. В практике использовать создание объектов, инициируемое клиентом, неудобно из-за того, что часто приходится выполнять какую-то подготовительную работу. Т.е. даже в нашем простом примере с чатом, я после получения ссылки на SharedObject, анализировал его состояние и если переданный мне SharedObject был “новорожденным”, то я создавал в нем свойство “история сообщений в чате”:
  1. if (!chat.data.remoteHistory)
  2.    chat.data.remoteHistory = [];
Как вы понимаете, в более серьезных приложениях процедура начальной настройки SharedObject-а будет гораздо сложнее, и ее лучше выполнять на java-стороне. Не говоря уже о том, что на java-стороне можно к подобному SharedObject-у привязать управляемые spring-ом объекты. Например, сервисы доступа к данным (dao), сервисы обмена сообщениями (jms); и все эти сервисы могут работать в рамках общей “длинной” транзакции. Т.е. действие, начатое одним пользователем (бизнес-операция и связанная с ней транзакция) будет завершено (транзакция будет подтверждена) другим пользователем. Главное, чтобы эти пользователи были подключены к одному SharedObject-у, тогда они могут видеть и работать с одной и той же информацией. Следующий текст о том, что такое scope или комнаты (room), как они соотносятся с приложением (app) и какой у них жизненный цикл, нужно читать, держа перед глазами текст, приведенный в прошлой статье: именно там я ввел базовые понятия комнат, их иерархии и связи с SharedObject-ами. Для демонстрации я создал новое red5 приложение, которое состоит из тех методов, что вызываются при наступлении различных событий в жизненном цикле red5 приложения. Устройство всех нижележащих методов тривиально: на экран просто печатается название метода, который был вызван:
  1. public class HelloApplication extends ApplicationAdapter {
  2.  
  3.  public boolean appConnect(final IConnection conn, Object[] params) {
  4.    System.out.println("appConnect: client " + conn.getClient());
  5.    return super.appConnect(conn, params);
  6.  }
  7.  
  8.  public boolean roomConnect(IConnection conn, Object[] params) {
  9.    System.out.println("roomConnect: conn " + conn);
  10.    return super.roomConnect(conn, params);   
  11.  }
  12.  
  13.  public boolean appStart(IScope app) {
  14.    System.out.println("appStart: scope " + app);
  15.    return super.appStart(app);    
  16.  }
  17.  
  18.  public void appStop(IScope app) {
  19.    System.out.println("appStop: scope " + app.getName());
  20.    super.appStop(app);   
  21.  }
  22.  
  23.  public boolean roomStart(IScope room) {
  24.    System.out.println("roomStart: room " + room.getName());
  25.    return super.roomStart(room);   
  26.  }
  27.  
  28.  public void roomStop(IScope room) {
  29.    System.out.println("roomStop: room " + room.getName());
  30.    super.roomStop(room);   
  31.  }
  32.  
  33.  public boolean appJoin(IClient client, IScope app) {
  34.    System.out.println("appJoin: app " + app.getName());
  35.    return super.appJoin(client, app);   
  36.  }
  37.  
  38.  public boolean roomJoin(IClient client, IScope room) {
  39.    System.out.println("roomJoin: room " + room.getName());
  40.    return super.roomJoin(client, room);   
  41.  }
  42.  
  43.  public void roomLeave(IClient client, IScope room) {
  44.     System.out.println("roomLeave: room " + room.getName());
  45.     super.roomLeave(client, room);  
  46.  }
  47.  
  48.  public void appLeave(IClient client, IScope app) {
  49.    System.out.println("roomLeave: app " + app.getName());
  50.    super.appLeave(client, app);  
  51.  }
  52. }
На стороне flash-клиента я создал простенькое приложение, состоящее из одного текстового поля и кнопки. В текстовое поле вы должны ввести произвольную строку – адрес подключения к red5 приложению. А после нажатия на кнопку “подключиться” именно этот адрес будет использован при вызове метода connect:
  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.   private var nc:NetConnection;
  5.  
  6.   private function doConnect(event:MouseEvent):void {
  7.     nc = new NetConnection();
  8.     nc.objectEncoding = ObjectEncoding.AMF0;
  9.     nc.addEventListener(NetStatusEvent.NET_STATUS, netStatus);
  10.     nc.connect('rtmp://localhost/'+txtRooms.text);  
  11.   }
  12.  
  13.   private function netStatus(event:NetStatusEvent):void {
  14.     trace (event);
  15.   }
  16. ]]></mx:Script>
  17.  <mx:VBox paddingTop="10" paddingLeft="10">
  18.    <mx:HBox>
  19.      <mx:TextInput id="txtRooms" text="warmodule/fruits/apples/"/>
  20.      <mx:Button click="doConnect(event)" label="подключиться"/>
  21.    </mx:HBox>
  22.  </mx:VBox> 
  23. </mx:Application>
Полученную пару приложений удобно использовать как исследовательский инструмент, чтобы понять то, как происходит создание комнат, как выполняется присоединение нового пользователя к существующей комнате и когда комнаты уничтожаются. Очевидно, что методы, начинающиеся на “app”, обозначают события из жизненного цикла всего приложения, а методы, начинающиеся на “room”, – связаны с жизненным циклом отдельной комнаты. Скомпилировав и запустив созданное java-приложение в среде red5-сервера, мы сразу же увидим в его консоли с сообщениями строку:
appStart: scope [WebScope@16e3a7e Depth = 1, Path = '/default', Name = 'warmodule']
Это значит, что метод appStart вызывается сразу после завершения запуска сервера и до того, как пришел запрос на подключение от первого из клиентов. Таким образом, внутри этого метода удобно размещать код инициализации ресурсов общих для всего приложения и для всех клиентов (например, загрузить spring контекст с сервисами приложения). Теперь откроем в браузере flash-клиент и немного с ним “поиграем”. Для начала введем в текстовое поле строку "warmodule/fruits/apples/". Таким образом, flash-клиент попробует подключиться по следующему полному адресу: “rtmp://localhost/warmodule/fruits/apples”. Здесь warmodule – это имя нашего веб-приложения, а имена “fruits/apples” задают иерархию двух комнат с такими же названиями. Из анализа сообщений напечатанных в консоли сервера можно сделать вывод, что app и join-методы методы вызываются в следующем порядке:
roomStart: room fruits
roomStart: room apples
appConnect: client Client: 0
appJoin: app warmodule
roomConnect: conn apples
roomJoin: room fruits
roomConnect: conn apples
roomJoin: room apples
Если открыть еще одно окно браузера и повторно нажать на кнопку подключения к серверу с тем же адресом, то список сообщений будет следующим:
appConnect: client Client: 1
appJoin: app warmodule
roomConnect: conn apples
roomJoin: room fruits
roomConnect: conn apples
roomJoin: room apples
Как видите, метод roomStart вызывается один только раз: при первом подключении клиента к комнате. А после того как комната “стартовала”, то клиенты к ней присоединяются (join). Методы connect вызываются всякий раз, когда к комнате или приложению приходит запрос на подключение. Если закрыть окошко браузера, то в консоли будет напечатан следующий набор сообщений, говорящих о том, что пользователь покинул (leave) следующие комнаты. Видите, что клиент “покидает” комнаты в порядке обратном тому, в котором он в них “заходил”:
roomLeave: room apples
roomLeave: room fruits
roomLeave: app warmodule
Когда же я закрою и второе окно браузера, то помимо “leave” методов, red5 вызовет еще методы остановки комнат “roomStop” последовательно для комнат apples и fruits. После того как комната была остановлена, то red5 уничтожает все ресурсы, связанные с ней (в том числе и SharedObject-ы). Если в последующем клиент еще раз захочет присоединиться к комнате, то она будет заново создана (вызваны методы “roomStart”). Далее: как и ожидалось, отключение абсолютно всех клиентов не приводит к остановке приложения. Т.е. метод “appStop” будет вызван только тогда, когда сам веб-сервер получит сигнал на завершение работы. В качестве эксперимента, попробуйте подключиться к серверу с адресом "warmodule/fruits/apples/", а сразу за этим в еще одном окне браузера подключиться с адресом "warmodule/fruits/grapes/". Вы должны увидеть, что метод “roomStart” будет вызван только для “новой” комнаты “grapes”, а для “старых” комнат будет вызван метод присоединения “roomJoin”. Приятно, что в любой момент времени вы можете легко узнать ту иерархию комнат, к которой присоединяется клиент, если воспользуйтесь следующим примером:
  1. IScope scope = Red5.getConnectionLocal().getScope();
  2. while (scope != null) {
  3.   System.out.println("scope " + scope);
  4.   scope = scope.getParent(); 
  5. }
Возвращаясь назад к задаче создания SharedObject-ов: внутри методов roomStart удобнее всего и выполнять создание “общих объектов”. В следующем примере я переиграю наш старый пример с чатом и перенесу код инициализации массива с историей сообщений в чате с flash-клиента на red5-сервер. Также я решил перенести с flash-стороны на java код метода, добавляющего в историю сообщений чата новую запись от клиента при его регистрации в чате, а также сообщение, посылаемое пользователем при нажатии на кнопку “send”. Здесь предполагается, что мы возьмем пример из прошлой статьи и, оставив без изменения его mxml-разметку, отвечающую за внешний вид, попробуем переписать часть бизнес-логики. Во-первых, я раз я решил что, модификация SharedObject-а, точнее его истории, будет выполняться на стороне сервера, а показываться на стороне клиента, то мне потребуется создать общий класс, экземпляры которого будут хранить записи в истории чата. Как я уже говорил в предыдущих статьях, создать подобный общий тип данных совсем не сложно: нужно на стороне java и на стороне flash-клиента создать классы с одинаковыми названиями и одинаковой структурой. Также требуется, чтобы у этих классов был конструктор с пустым списком параметров, а те поля, которыми мы хотим обмениваться должны быть либо объявлены как public, либо иметь привязанные к ним методы get и set. Итак, вот что у меня получилось на стороне flash:
  1. package blz.red5demo {
  2.  
  3. [RemoteClass(alias="blz.red5demo.ChatHistoryItem")]
  4.   public class ChatHistoryItem {
  5.     public var user:String;
  6.     public var date:Date;
  7.     public var message:String;
  8.     public function ChatHistoryItem() { }
  9.   } 
  10. }
И соответствующий ему аналог на стороне java:
  1. package blz.red5demo;
  2. import java.util.Date;
  3.  
  4. public class ChatHistoryItem {
  5.   public String user;
  6.   public Date date;
  7.   public String message; 
  8. }
Теперь смотрим, как выглядит код flash-клиента применительно к функции создания SharedObject-а. Эти действия я выполняю внутри функции netStatus сразу после того, как я получил извещение о том, что соединение с java-стороной было успешно установлено:
  1. private function netStatus(event:NetStatusEvent):void {
  2.   if (event.info.code == 'NetConnection.Connect.Success') {
  3.      chat = SharedObject.getRemote("chat", nc.uri);
  4.      chat.addEventListener(SyncEvent.SYNC, onSync)
  5.      chat.connect(nc);  
  6.   }
  7. }
Как видите, я обращаюсь все к тому методу SharedObject.getConnection для получения ссылки на SharedObject с именем “chat”, находящегося в той же scope (комнате), что была создана на стадии подключения к red5-серверу. В следующем java-коде я в тот же момент времени, когда создается комната, выполняю ее начальную настройку, т.е. создаю SharedObject:
  1. public boolean roomStart(IScope room) {
  2.    if (!super.roomStart(room))
  3.       return false;
  4.    createSharedObject(room, "chat", false);
  5.    ISharedObject chat = getSharedObject(room, "chat");
  6.    chat.setAttribute("remoteHistory", new ArrayList<ChatHistoryItem>());
  7.    return true;
  8. }
После того как соединение было установлено и создан SharedObject, то flash-клиент получит первое уведомление о синхронизации с SharedObject-ом. Именно здесь я должен послать в чат сообщение о том, что клиент только что зашел в чат. Но, я не хочу выполнять эту работу внутри flash-клиента – я хочу отправить извещение java-приложению, чтобы оно выполнило связанную с регистрацией нового клиента работу. Например, чат может ограничивать количество посетителей или требовать предварительной регистрации пользователей: т.е. действия, выполнить которые на стороне flash-клиента, будет тяжело. И только после всех этих проверок серверный код может изменить содержимое SharedObject-а. Для вызова серверного метода я использую знакомый нам по прошлым статьям метод “call” на объекте NetConnection (почему метод вызывается не на SharedObject, я расскажу попозже). Момент в том, что на этот раз второй параметр функции call равен null. Дело в том, что как раз вторым параметром flash-код передает ссылку на специальный объект Responder, который “слушает” извещения об успешном (или не успешном) завершении вызова серверного метода. Для моего примера чата такой контроль является избыточным, так что я решил отказаться от Responder-а.
  1. private function onSync(event:SyncEvent):void {
  2.   if (chatIsReady == false) {
  3.      chatIsReady = true;
  4.      nc.call("chatLogin", null, txtUser.text); 
  5.   }
  6.   localHistory.source = chat.data.remoteHistory;
  7. }
На java-стороне я определил внутри класса приложения метод chatLogin, который принимает как параметр имя пользователя, зашедшего в чат, и помещает эту информацию в историю сообщений чата:
  1. public void chatLogin(String userName) {
  2.   chatMessage(userName, "Пользователь вошел в чат");
  3. }
  4. public void chatMessage(String userName, String message) {
  5.   IScope scope = Red5.getConnectionLocal().getScope();
  6.   ISharedObject chat = getSharedObject(scope, "chat");
  7.   List<ChatHistoryItem> history = (List<ChatHistoryItem>) chat.getAttribute("remoteHistory");
  8.   ChatHistoryItem item = new ChatHistoryItem();
  9.   item.user = userName;
  10.   item.date = new Date();
  11.   item.message = message;
  12.   history.add(item);
  13.   chat.setAttribute("remoteHistory", history);
  14. }
Как видите, и на стороне сервера и на стороне flash-клиента вся работа с SharedObject-ом сводится к изменению составляющих его атрибутов. Плюс, в том случае, если java-код хочет “за один раз” изменить несколько атрибутов, то нужно окружить эти действия вызовом функций beginUpdate и endUpdate():
  1. chat.beginUpdate();
  2. // а тут меняем атрибуты
  3. chat.endUpdate();
Завершающий штрих нашей переделки чата – это изменение методики, с помощью которой в чат добавляется текстовое сообщение. Здесь я снова решил использовать прием с посредником: flash-клиент посылает извещение java-приложению, которое после каких-то проверок пришедшего сообщения (например, на предмет наличия стоп-слов) модифицирует SharedObject. А дальше “магия” red5 выполняет рассылку изменений всем зарегистрированным flash-клиентам:
  1. private function say(e:MouseEvent):void {
  2.  if (! chatIsReady) {
  3.     mx.controls.Alert.show("Сначала нужно подключиться к серверу");
  4.     return; 
  5.  }
  6.  nc.call("chatMessage", null, txtUser.text, txtMessage.text);
  7. }
То, что у меня получилось, показано на рис. 1.



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

Categories: Flash & Flex