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

October 25, 2009No Comments

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

В прошлый раз мы остановились на описании конфигурационных файлов, составляющих java часть проекта. Напомню, что любое веб-приложение в мире java должно содержать, как минимум, один конфигурационный файл: web.xml в подкаталоге WEB-INF. Пример файла, который я привел в конце прошлой статьи, был пуст и не содержал ни одной директивы. Если вы посмотрите на содержимое подкаталога conf, расположенного в корне того каталога, куда вы установили red5, то там вы увидите множество xml и properties-файлов. Все эти файлы содержат настройки как списка служб, запускаемых при старте сервера tomcat, так и настройки по-умолчанию для тех веб-приложений, что будут исполняться в среде red5. Большой необходимости в том, чтобы изменять эти файлы у вас не должно возникнуть, разве что можете обратить внимание на файлы red5.properties и red5-core.xml. В первом из них находится список главных конфигурационных переменных, влияющих на работу поддерживаемых red5 служб. Помните, я говорил в первой статье серии, что существует несколько протоколов, по которым flash-приложение может общаться с сервером, например, RTMP, RTMPT, RTMPTS. Так вот, в файле red5.properties содержатся номера портов, которые сервер red5 будет “слушать”, ожидая входящих подключений по этим протоколам (также можно изменить и номер порта, по которому будет “отдаваться” обычный http-трафик). Что касается файла red5-core.xml, то этот файл является конфигурационным файлом в стиле spring и содержит перечень служб, которые запускаются tomcat-ом при своем запуске. К примеру, протоколы RTMPT, RTMPTS изначально отключены и чтобы их включить, вам нужно убрать лишние комментарии внутри red5-core.xml. Сразу скажу, что сервер red5 был написан с использованием популярной java-технологии spring (http://springframework.org). Если вы никогда ранее не сталкивались с spring, то spring - это средство описания в виде xml-файлов списка компонент составляющих приложение и их настроек. Сейчас spring стал стандартом де-факто для создания “серьезных” java-приложений. Также стала часто встречаться практика, когда приложения (особенно opensource) не содержат отдельных конфигурационных файлов, управляющих их работой, а предлагают пользователям изменять поведение приложения посредством правок spring-конфигураций.

Возвращаясь назад к нашему примеру веб-приложения, давайте рассмотрим остальные файлы, составляющие содержимое модуля warmodule. Итак, в каталоге WEB-INF, помимо файла web.xml, еще находятся файлы red5-web.xml и red5-web.properties. Что касается имен этих файлов, то по умолчанию red5 ищет в каталоге WEB-INF веб-приложения xml-файлы, начинающиеся с “red5-”, а затем загружает эти файлы и интерпретирует их как инструкции по конфигурированию приложения. Имя же второго файла red5.properties не имеет какого-либо особого значения, но чтобы понять то, зачем он нужен, давайте рассмотрим содержимое файла red5-web.xml:
  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
  3. <beans>
  4.   <bean id="placeholderConfig" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
  5.     <property name="location" value="/WEB-INF/red5-web.properties"/>
  6.   </bean>
  7.   <bean id="web.context" class="org.red5.server.Context" autowire="byType"/>
  8.   <bean id="web.scope" class="org.red5.server.WebScope"
  9.       init-method="register">
  10.     <property name="server" ref="red5.server"/>
  11.     <property name="parent" ref="global.scope"/>
  12.     <property name="context" ref="web.context"/>
  13.     <property name="handler" ref="web.handler"/>
  14.     <property name="contextPath" value="${webapp.contextPath}"/>
  15.     <property name="virtualHosts" value="${webapp.virtualHosts}"/>
  16.   </bean>
  17.   <bean id="web.handler" class="blz.red5demo.HelloApplication" singleton="true"/>
  18. </beans>
Red5 ожидает, что мы предоставим ему информацию о конкретных сервисах, которые будут обрабатывать запросы от flash-приложения. И эти службы (приложения) я должен зарегистрировать внутри red5-web.xml файла. Как вы уже могли догадаться по первым его строкам, файл red5-web.xml – это снова конфигурационный файл spring. Каждое приложение в red5 “собирается” из трех частей: контекст приложения, затем класс, реализующий, собственно, логику работы приложения и так называемая “scope” (область вызова). Во-первых, я создал и поместил в spring-контекст класс blz.red5demo.HelloApplication (это мой собственный класс, пример которого я приведу далее). Ссылку на этот класс я поместил внутрь scope как значение переменной “handler”. Это значит, что если клиент захочет вызвать какой-нибудь метод на scope, то этот вызов будет делегирован к одноименному методу в составе класса HelloApplication. Вторая составляющая scope – “web.context” не так прост, и чтобы не углубляться в пока не нужные подробности скажу, что он является оберткой над spring-контекстом, в рамках которого работает вся инфраструктура red5. Scope существует не сама по себе, а обслуживает запросы, приходящие к серверу red5. Ссылка на который должна храниться как значение свойства “server”. Но в файле red5-web.xml нет объявления сервера red5, и это вполне ожидаемо: ведь сервер существует всегда в одном экземпляре для всех веб-приложений развернутых на нем. И если в рамках одного веб-приложения мы создаем несколько scope, то все они будут ссылаться на один и тот же сервер. Объявляется этот “мистический” сервер (в примере это переменная с именем “red5.server”) в тех конфигурационных файлах spring, что расположены в подкаталоге conf (о нем я уже упоминал ранее). Гораздо больший интерес представляет такие свойства scope как contextPath и virtualHosts. Дело в том, что раз на одном red5 сервере может быть размещено произвольное количество red5-приложений (scope), то нужен какой-то способ их различать между собой. Однако contextPath, не смотря на свое название, один в один совпадающее с понятием имени контекста для веб-приложений, все же может отличаться от имени контекста. Т.е. наше приложение (swf-файл) может быть размещено в каталоге warmodule, а название scope будет, например, logic и для соединения из flash-клиента с серверной частью приложения мы будем писать что-то вроде:
  1. var nc : RemoteNetConnection = new RemoteNetConnection();
  2. nc.connect('rtmp://localhost/logic’);
Тем не менее, во избежание путаницы я всегда рекомендую давать название scope совпадающее с именем контекста веб-приложения. Именно с этой целью я в прошлой статье решил показать то, как можно передавать с помощью sfwobject внутрь flash-приложения различные конфигурационные переменные, и в частности имя каталога, в котором размещено веб-приложение. Для того, чтобы дать возможность администратору веб-сервера гибко настраивать список доступных для вызова из flash приложений (scope) и их имена я решил вынести в отдельный конфигурационный файл red5.properties значения двух переменных: webapp.contextPath и webapp.virtualHosts.
  1. webapp.contextPath=/warmodule
  2. webapp.virtualHosts=localhost, 127.0.0.1
Первая из этих переменных - это имя scope, а вторая может задавать через запятую список ip-адресов или доменных имен, которые будет прослушивать сервер red5 на предмет адресованных scope запросов. Эта функция может пригодиться в том, случае если у вас на машине с сервером несколько сетевых карт и ip-адресов, а вы хотите ограничить входящие запросы только теми, которые приходят на какой-то конкретный ip-адрес.

Теперь я приведу пример класса HelloApplication. По правилам red5 любой класс, который обрабатывает серверные вызовы внутри red5, должен быть наследован от класса org.red5.server.adapter.ApplicationAdapter:
  1. package blz.red5demo;
  2. import org.red5.server.adapter.ApplicationAdapter;
  3. import org.red5.server.api.IConnection;
  4.  
  5. public class HelloApplication extends ApplicationAdapter {
  6.     @Override
  7.     public boolean appConnect(final IConnection iConnection, Object[] params) {
  8.         return super.appConnect(iConnection, params);
  9.     }
  10.     @Override
  11.     public void appDisconnect(IConnection conn) {
  12.         super.appDisconnect(conn);
  13.     }
  14. }
Метод appConnect вызывается тогда, когда на вход scope поступает запрос на установление соединения. Как видите, метод должен вернуть булеву переменную, которая будет обозначать то, хочет ли сервер разрешить соединение или в нем нужно отказать. По-умолчанию (метод super.appConenct), соединение будет разрешено. Метод appDisconnect будет вызваться в том случае, когда соединение между сервером и клиентом будет прервано. И произойдет это как в том случае, когда клиент явно скажет “разорвать соединение”, а также тогда, когда соединение рвется в результате какого-то сетевого сбоя.

Разобравшись с серверной частью примера, пора перейти к рассмотрению кода flash-клиента. Не мудрствуя лукаво, я решил создать простенький интерфейс в виде формы с одной единственной кнопкой. По нажатию на эту кнопку я буду посылать на сервер запрос с просьбой подключения к red5 серверу. При отправке запроса на подключение я могу сразу же передать на сервер какую-либо информацию, влияющую на процесс установления сессии. Например, передать имя и пароль для входа в приложение. Серверная часть проверит представленные данные и или разрешит, или отклонит запрос на подключение. Сейчас я не буду усложнять пример и создавать диалоговое окно для запроса имени и пароля. Так что обойдемся передачей фиксированного имени пользователя “DemoUser” и такого же пароля “DemoPassword”. Весь последующий код я поместил внутрь файла Main.mxml расположенного внутри подкаталога “flashmodule/src/main/flex”:
  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 flash.net.NetConnection
  6.  
  7.   private var nc: NetConnection;
  8.  
  9.   private function doConnect(event:MouseEvent):void {
  10.      var context : String = application.parent.loaderInfo.parameters['contextName'];
  11.      nc = new NetConnection ();
  12.      nc.objectEncoding = ObjectEncoding.AMF0;
  13.      nc.addEventListener(NetStatusEvent.NET_STATUS, netStatus);
  14.      nc.connect('rtmp://localhost/' + context, “DemoUser”, “DemoPassword”);
  15.   }
  16.  
  17.   private function netStatus(event:NetStatusEvent):void {
  18.     log.text += event.info.code + "\n";
  19.   }
  20.  ]]>
  21.  </mx:Script>
  22.  
  23.  <mx:VBox paddingTop="10" paddingLeft="10">
  24.    <mx:Button click="doConnect(event)" label="connect to Red5"/>
  25.    <mx:TextArea id="log" width="400" height="400" />
  26.  </mx:VBox> 
  27. </mx:Application>
В этом небольшом mxml-файле я создал панель-контейнер VBox. На эту панель, в свою очередь, были помещены кнопка (тег “mx:Button”) и текстовая область (тег “mx:TextArea”). Как только пользователь нажимает на кнопку, то вызывается функция обработчик события “doConnect”. Внутри этой функции я создал объект сетевого подключения NetConnection. Затем я указываю протокол, с помощью которого будет выполняться кодирование всех передаваемых по протоколу rtmp данных. Для этого я присвоил имя протокола AMF0 свойству objectEncoding (другое возможное значение протокола “AMF3”). Поскольку любое действие, связанное с отправкой запросов на сервер из flash-ролика выполняется асинхронно, а значит, может занять некоторое время. То мне пришлось создать специальную функцию netStatus, и назначить ее как обработчик события “ NetStatusEvent.NET_STATUS”. Т.е. функция netStatus будет автоматически вызвана, как только запрос на соединение с web-сервером будет завершен или успешно или нет. Последнее действие, выполняемое внутри функции doConnect, это отправка запроса на сервер. Для этого я вызвал метод connect, указав для него имя сервера на котором размещена инфраструктура red5 и имя веб-приложения, с которым нужно установить подключение. Имя приложения (scope) я взял из списка параметров, переданных flash-файлу из родительского html-файла (application.parent.loaderInfo.parameters). Помимо первого обязательного параметра функция connect может принимать еще произвольное число переменных, передаваемых на сервер. Как и обещал выше, я решил выполнить имитацию процедуры аутентификации пользователя, передав его имя и пароль. Что касается функции netStatus, то ее внутреннее устройство примитивно: проанализировать то, чему равна переменная “event.info.code”. Как раз внутри этой переменной и будет храниться статус выполнения операции соединения с сервером. В том случае, если соединение было успешным, то возвращается статус “NetConnection.Connect.Success”. В случае, если по какой-либо причине соединение с сервером не удалось установить (например, поврежден канал связи), то статус будет равен “NetConnection.Connect.Failed”. И, наконец, в случае, если соединение с сервером было успешным, но сервер проанализировал присланные ему данные и решил, что в соединении нужно отказать, то статус результата будет равен “NetConnection.Connect.Rejected”. В том случае, когда соединение с сервером прерывается, то функция netstatus также будет вызвана. Но уже со значением для переменной “event.info.code” равной “NetConnection.Connect.Closed”. Также как и функция appDisconnect на серверной стороне приложения, функция netstatus будет вызвана и в случае, если связь между клиентом и сервером была прервана аварийно. Для удобства, я решил напечатать значение переменной “event.info.code” внутрь текстового поля, размещенного чуть ниже кнопки подключения (см. рис. 1).



Теперь нужно вернуться назад к серверному скрипту и подправить функцию appConnect, так чтобы она проверяла, переданные ей как параметры, имя и пароль:
  1. public boolean appConnect(final IConnection iConnection, Object[] params) {
  2.   if (params.length ==2 && "DemoUser".equals(params[0]) && "DemoPassword".equals(params[1]))
  3.      return super.appConnect(iConnection, params);
  4.   return false;
  5. }
Теперь осталось только запустить пример на выполнение и проверить, что происходит по нажатию на кнопку “connect to Red5”. Для того, чтобы выполнить компиляцию проекта мне достаточно было перейти в корень каталога с проектом и набрать в командной строке:
mvn clean install
После некоторого ожидания в каталоге warmodule/target должен появиться файл “warmodule.war”. Его нужно скопировать внутрь подкаталога webapps и перезапустить сервер red5. Или как альтернативный вариант можно скопировать в каталог webapp не war-файл, а расположенный рядом с ним подкаталог warmodule. Затем я ввел в адресной строке браузера путь к моему приложению (http://localhost:5080/warmodule) и увидел окно flash-приложения (см. рис. 1). По нажатию на кнопку “connect to Red5”, в текстовом поле должна появиться запись NetConnection.Connect.Success. Попробуйте добавить к flash-приложению еще одну кнопку “Close”, по нажатию на которую, над переменной nc вызывается метод close. Это должно привести к разрыву подключения с сервером и, опять-таки, вызову метода netStatus с переменной “event.info.code” равной “NetConnection.Connect.Closed”.

Теперь, после того как мы установили соединение с сервером, можно попробовать и вызывать на нем (точнее в scope или приложении) какие-нибудь методы. В самом начале этой серии статей я поставил целью создать хотя бы простое приложение, посылающее на сервер строку с именем пользователя и получающее в ответ строку вида “Hello, %username%”. Для этого я в состав класса HelloApplication добавил новый метод sayHello.
  1. public String sayHello (String userName){
  2.   return “Hello, ”+ userName;
  3. }
Для того, чтобы вызвать из flash этот метод я добавил на форму VBox еще одну кнопку:
  1. <mx:Button click="sayHello(event)" label="Say Hello"/>
При нажатии на которую будет вызвана функция sayHello, которая в свою очередь пошлет запрос на сервер:
  1. private function sayHello (event:MouseEvent):void {
  2.     var r : Responder = new Responder(onSayHello, netStatus);
  3.     nc.call("sayHello", r, “My User Name);
  4. }
  5. private function onSayHello(r: Object):void {
  6.      trace(r);
  7. }
Для того, чтобы обратиться к любому методу на стороне сервера нужно вызвать на объекте NetConenction метод call, передав ему как первый параметр строку с именем метода, к которому мы хотим обратиться. Вторым параметром для call идет специальный объект Responder. Как я уже отмечал ранее, в flash все запросы к серверу носят асинхронную природу. А это значит, что нужно передать NetConnection информацию о двух функциях: одна из них будет вызвана, как только будут получен ответ с сервера (onSayHello). Вторая же функция – уже знакомая нам netStatus служит для обработки ошибок.

Показанному мною примеру еще есть куда развиваться. Во-первых, остался открытым вопрос о передаче на сервер и обратно более сложных структур данных, кроме чисел или строк (например, собственный класс User , состоящий из таких полей, как ФИО, дата рождения и т.д.). Но обо всем этом в следующий раз. Также в следующий раз я расскажу о том, как можно из java-кода сделать обратный вызов к какой-либо функции расположенной на стороне flash-клиента. Например, создавая чат, мы, получив от одного из зарегистрированных пользователей сообщение, должны выполнить его рассылку всем остальным пользователям.

Categories: Flash & Flex