Red5: Практика работы с потоковым мультимедиа. Часть 4

Рассказ о том, как создать веб-приложение, работающее в среде 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:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="placeholderConfig" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="/WEB-INF/red5-web.properties"/>
</bean>
<bean id="web.context" class="org.red5.server.Context" autowire="byType"/>
<bean id="web.scope" class="org.red5.server.WebScope"
init-method="register">
<property name="server" ref="red5.server"/>
<property name="parent" ref="global.scope"/>
<property name="context" ref="web.context"/>
<property name="handler" ref="web.handler"/>
<property name="contextPath" value="${webapp.contextPath}"/>
<property name="virtualHosts" value="${webapp.virtualHosts}"/>
</bean>
<bean id="web.handler" class="blz.red5demo.HelloApplication" singleton="true"/>
</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-клиента с серверной частью приложения мы будем писать что-то вроде:
var nc : RemoteNetConnection = new RemoteNetConnection();
nc.connect('rtmp://localhost/logic’);

Тем не менее, во избежание путаницы я всегда рекомендую давать название scope, совпадающее с именем контекста веб-приложения. Именно с этой целью я в прошлой статье решил показать, как можно передавать с помощью sfwobject внутрь flash-приложения различные конфигурационные переменные, и в частности имя каталога, в котором размещено веб-приложение. Для того чтобы дать возможность администратору веб-сервера гибко настраивать список доступных для вызова из flash-приложений (scope) и их имена, я решил вынести в отдельный конфигурационный файл red5.properties значения двух переменных: webapp.contextPath и webapp.virtualHosts.
webapp.contextPath=/warmodule
webapp.virtualHosts=localhost, 127.0.0.1

Первая из этих переменных - это имя scope, а вторая может задавать через запятую список ip-адресов или доменных имен, которые будет прослушивать сервер red5 на предмет адресованных scope запросов. Эта функция может пригодиться в том случае, если у вас на машине с сервером несколько сетевых карт и ip-адресов, а вы хотите ограничить входящие запросы только теми, которые приходят на какой-то конкретный ip-адрес.

Теперь я приведу пример класса HelloApplication. По правилам red5 любой класс, который обрабатывает серверные вызовы внутри red5, должен быть наследован от класса org.red5.server.adapter.ApplicationAdapter:
package blz.red5demo;
import org.red5.server.adapter.ApplicationAdapter;
import org.red5.server.api.IConnection;

public class HelloApplication extends ApplicationAdapter {
@Override
public boolean appConnect(final IConnection iConnection, Object[] params) {
return super.appConnect(iConnection, params);
}
@Override
public void appDisconnect(IConnection conn) {
super.appDisconnect(conn);
}
}

Метод appConnect вызывается тогда, когда на вход scope поступает запрос на установление соединения. Как видите, метод должен вернуть булеву переменную, которая будет обозначать то, хочет ли сервер разрешить соединение, или в нем нужно отказать. По умолчанию (метод super.appConenct), соединение будет разрешено. Метод appDisconnect будет вызваться в том случае, когда соединение между сервером и клиентом будет прервано. И произойдет это как в том случае, когда клиент явно скажет “разорвать соединение”, так и тогда, когда соединение рвется в результате какого-то сетевого сбоя.

Разобравшись с серверной частью примера, пора перейти к рассмотрению кода flash-клиента. Не мудрствуя лукаво, я решил создать простенький интерфейс в виде формы с одной единственной кнопкой. По нажатию на эту кнопку я буду посылать на сервер запрос с просьбой подключения к red5- серверу. При отправке запроса на подключение я могу сразу же передать на сервер какую-либо информацию, влияющую на процесс установления сессии. Например, передать имя и пароль для входа в приложение. Серверная часть проверит представленные данные и или разрешит, или отклонит запрос на подключение. Сейчас я не буду усложнять пример и создавать диалоговое окно для запроса имени и пароля. Так что обойдемся передачей фиксированного имени пользователя “DemoUser” и такого же пароля “DemoPassword”. Весь последующий код я поместил внутрь файла Main.mxml, расположенного внутри подкаталога “flashmodule/src/main/flex”:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
<mx:Script><![CDATA[
import flash.events.MouseEvent;
import flash.net.NetConnection

private var nc: NetConnection;

private function doConnect(event:MouseEvent):void {
var context : String = application.parent.loaderInfo.parameters['contextName'];
nc = new NetConnection ();
nc.objectEncoding = ObjectEncoding.AMF0;
nc.addEventListener(NetStatusEvent.NET_STATUS, netStatus);
nc.connect('rtmp://localhost/' + context, “DemoUser”, “DemoPassword”);
}

private function netStatus(event:NetStatusEvent):void {
log.text += event.info.code + "\n";
}
]]></mx:Script>
<mx:VBox paddingTop="10" paddingLeft="10">
<mx:Button click="doConnect(event)" label="connect to Red5"/>
<mx:TextArea id="log" width="400" height="400" />
</mx:VBox> </mx:Application>

<img1>В этом небольшом 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, так чтобы она проверяла переданные ей как параметры имя и пароль:

public boolean appConnect(final IConnection iConnection, Object[] params) {
if (params.length ==2 && "DemoUser".equals(params[0]) && "DemoPassword".equals(params[1]))
return super.appConnect(iConnection, params);
return false;
}

Теперь осталось только запустить пример на выполнение и проверить, что происходит по нажатию на кнопку “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.

public String sayHello (String userName){
return “Hello, ”+ userName;
}

Для того чтобы вызвать из flash этот метод, я добавил на форму VBox еще одну кнопку:
<mx:Button click="sayHello(event)" label="Say Hello"/>
При нажатии на которую будет вызвана функция sayHello, которая в свою очередь пошлет запрос на сервер:

private function sayHello (event:MouseEvent):void {
var r : Responder = new Responder(onSayHello, netStatus);
nc.call("sayHello", r, “My User Name”);
}
private function onSayHello(r: Object):void {
trace(r);
}

Для того чтобы обратиться к любому методу на стороне сервера, нужно вызвать на объекте NetConenction метод call, передав ему как первый параметр строку с именем метода, к которому мы хотим обратиться. Вторым параметром для call идет специальный объект Responder. Как я уже отмечал ранее, в flash все запросы к серверу носят асинхронную природу. А это значит, что нужно передать NetConnection информацию о двух функциях: одна из них будет вызвана, как только будут получен ответ с сервера (onSayHello). Вторая же функция – уже знакомая нам netStatus — служит для обработки ошибок.

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

black-zorro@tut.by black-zorro.com


Компьютерная газета. Статья была опубликована в номере 39 за 2009 год в рубрике программирование

©1997-2024 Компьютерная газета