Мультимедиа-программирование вместе с Red 5 server. Часть 5
В прошлой статье я завершил рассказ о том, как создать каркас для веб-приложения, выполняющегося в среде red5 и представляющего набор сервисов для flash-приложения. Пример был не самым сложным и всего лишь показывал то, как можно отправить из flash запрос на подключение к веб-приложению, а после этого как вызвать какой-нибудь метод, передав ему как параметры строку и получив в ответ такую же строку. В “настоящем” приложении, обмен данными будет включать в себя отправку более сложных и “приближенных к жизни” структур данных: массивы, объекты. Именно это и будет темой сегодняшнего материала.
Для того, чтобы передать из flash-клиента на сервер и обратно сложный объект, например, объект User, состоящий из нескольких полей (ФИО, дата рождения, список друзей), во-первых, необходимо разработать и на стороне flash и на стороне java набор классов с одинаковой структурой. Так для flash-клиента я в подкаталоге “flex” создал иерархию подкаталогов “blz/red5demo” и поместил туда файл “User.as” с описанием класса User:
package blz.red5demo {
[RemoteClass(alias="blz.red5demo.User")]
[Bindable]
public class User {
public var id: String;
public var fio: String;
public var sex: Boolean;
public var weight: Number;
public var salary: int;
public var birthDay : Date;
private var _friends : Array = new Array();
public function User() {}
public function get friends(): Array {
return _friends;
}
public function set friends(f: Array): void {
_friends = f;
}
}
}
Устройство класса User примитивно: семь полей демонстрирующих различные типы данных, которые можно передавать между flash и java. Внимание следует обратить на различные стратегии доступа к полям. Так все поля, кроме friends (массива друзей), объявлены как public. Что касается поля friends, то оно закрыто от прямого обращения, но для доступа к нему я создал пару функций get и set, вызывая которые, можно изменять значение списка друзей. Дело в том, что если какое-то поле в классе не является public и не имеет связанных с ним функций get и set, то flash не сможет ни передать на сервер значение такого поля, ни принять его обратно. Преобразование между простыми типами данных actionscript и java (например, строки, числа, даты) выполняется автоматически. А вот для того, чтобы flash мог декодировать незнакомый для него тип данных User, я должен был пометить передаваемый по сети класс с помощью actionscript аннотации RemoteClass. Указав этим имя java-класса, расположенного на стороне сервера, и имеющего такую же структуру, как и его actionscript компаньон. Для того, чтобы java-код мог декодировать присланный ему объект User, мне необходимо создать в точно таком же каталоге “blz/red5demo” класс User с совпадающими названиями и типами полей, и поля эти должны быть либо public, либо иметь ассоциированные функции get и set. Т.е. все точно так, как я сделал и на стороне flash.
package blz.red5demo;
import java.util.Date;
import java.util.List;
public class User {
public String id;
public String fio;
public Double weight;
public Boolean sex;
public Date birthDay;
public Long salary;
public List<User> friends;
}
Следующий пример построен на базе все той же mxml-формы, что я использовал в предыдущем примере, и содержит две кнопки и текстовое поле. По нажатию на первую кнопку на сервер отправляется запрос с просьбой подключения. Вторая кнопка служит для вызова метода “sendUser”, как раз и передающего на сервер объект User. Как и в прошлый раз, в тестовое поле будет печататься информация о статусе выполнения запросов.
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute">
<mx:Script><![CDATA[
import blz.red5demo.User;
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://center/warmodule');
}
// функция генерирующая случайным образом объект User
public static function generateRandomUser(withFriends: Boolean):User {
var u : User = new User();
u.id = "Random_Id_" + Math.random();
u.fio = "Random_Fio_" + Math.random();
u.sex = Math.random() > 0.5;
u.weight = Math.random() * 1000;
u.salary = Math.random() * 100000;
u.birthDay = new Date();
if (withFriends) {
var nc: int = (Math.random() * 10);
for (var i: int = 0; i < nc; i++) {
u.friends.push(generateRandomUser(false));
}
}
return u;
}
// функция посылающая на сервер объект User
private function sendUser(event:MouseEvent):void {
var r : Responder = new Responder(onSendUser, netStatus);
nc.call("sendUser", r, generateRandomUser(true))
}
// показываем ответ полученный от сервера
private function onSendUser(r: Object):void {
trace("users list" + r);
}
// сообщаем о статусе подключения к серверу
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:Button click="sendUser(event)" label="send to Red5 User object"/>
<mx:TextArea id="log" width="400" height="200"/>
</mx:VBox>
</mx:Application>
Вкратце: первым делом пользователь должен подключиться к серверу и для этого он нажимает на кнопку “connect to Red5”. После того, как сеанс был установлен, можно вызывать на сервер любой метод с любыми параметрами (за это отвечает функция sendUser). А посылаемый на сервер объект конструируется случайным образом внутри функции generateRandomUser. У которой есть единственный параметр, управляющий тем, будет ли у “создаваемого человечка” заполнено поле со списком друзей. Список этих самых друзей технически представлен в виде обычного actionscript массива. Этот массив будет преобразован в java-коллекцию ArrayList. К сожалению, если вы хотите отправлять на сервер наборы данных в виде коллекции ArrayCollection, то такое преобразование автоматически выполнено не будет. Теперь перейдем к рассмотрению того, как устроен код на стороне сервера. Помимо того, что я создал java-аналог класса User, мне пришлось добавить в класс “приложение” метод sendUser следующего вида:
public class HelloApplication extends ApplicationAdapter {
public List<User> sendUser(User user) {
List<User> users = new ArrayList<User>();
for (int i = 0; i < 10; i++) {
User pupil = new User();
pupil.fio = "Generated_User_" + i;
pupil.birthDay = new Date();
users.add(pupil);
}
return users;
}
}
Функция sendUser формирует как результат своей работы коллекцию (ArrayList) объектов User и отправляет их обратно flash-клиенту. Пример не заслуживает никаких дополнительных комментариев, за исключением вопроса об отладке. Действительно, производительность разработки любого приложения сильно зависит от удобства работы программиста и, в частности, наличия отладчика, позволяющего “подсмотреть” что же на самом деле происходит внутри приложения: какие наборы данных посылаются и принимаются. В первой статье серии я поставил своей целью познакомить вас с тем, какие средства для работы с flex и java есть в среде intellij idea (
http://www.jetbrains.com/). Приятно, что мы можем вести параллельную разработку и отладку и java и actionscript-кода в единой среде разработки. Для того, чтобы проверить как работает наше приложение, я хочу и flash и java-код запустить в режиме отладки, чтобы к нему можно было подключиться из среды intellij idea. В прошлой статье я уже упоминал о том, что для корректной работы flash-отладчика нужно выполнить компиляцию проекта в режиме “debug” (за это отвечали настройки в pom-файле проекта) и установить debug-версию flashplayer. Что касается java-кода, то я должен запустить сервер не с помощью команды “red5.bat”, а с помощью “red5-debug.bat”. В результате сервер после запуска будет ждать входящих подключений на порту 8787. Далее вернувшись в среду idea, я с помощью меню “Run -> Edit Configurations” создаю две конфигурации для запуска проекта. Первая конфигурация имеет тип “Remote” и служит для подключения к любой java машине запущенной на локальном компьютере (или на любом другом в сети). В качестве единственной настройки конфигурации запуска я указал номер порта, к которому idea должна подключиться (8787). Затем я создал точку остановки как раз на заголовке функции sendUser. Аналогичным образом я создаю конфигурацию запуска для flex-части приложения. Т.е. я выбрал тип конфигурации “flex”, а в графе URL указал адрес, по которому на сервере расположено мое flash-приложение http://localhost:5080/warmodule. Создав конфигурации, я могу их запустить, что в случае flash-модуля приведет к появлению нового окна браузера и открытию в нем странички http://localhost:5080/warmodule/. После того, как я подключился к серверу и нажал кнопку "send to Red5 User object”, то intellij idea перехватит мой запрос и покажет окно отладчика. На рис. 1
показано окно отладчика actionscript, а на рис. 2
показано окно java-отладчика. Обратите внимание на то, как удобно просматривать внизу окна отладчика сведения об доступных переменных (устройство класса User). Для того, чтобы закрыть вопрос, связанный с отладкой приложений, я обязан упомянуть об одном интересном продукте – Charles (домашний сайт
http://www.charlesproxy.com/). Charles – это один из многих представителей семейства прокси-серверов. Он прозрачно встраивается между веб-приложением и веб-сервером, перехватывает и показывает в удобочитаемой форме информацию обо всех посылаемых запросах и ответах на них. Подобных proxy-серверов очень много, но Charles выгодно отличается от остальных представителей тем, что “понимает” формат сообщений amf0 и amf3. Сразу скажу, что использовать Charles для контроля за передаваемыми по протоколу rtmp сообщениями не возможно. Но в том случае, если архитектурно вы решили передавать amf-объекты из flash на сервер по обычному протоколу http, то charles вам наверняка пригодится (продукт платный и стоит 50$).
Важное отличие протокола rtmp от http в том, что в случае http фактически не существует постоянного канала подключения между клиентом и сервером. Т.е. всякий раз, когда клиент нуждается в каком-то файле на сервере, то он должен послать отдельный запрос. Это проявляется в так называемой “забывчивости” сервера. Т.е. сервер без помощи таких специальных средств как cookie или переменных, передаваемых вместе с запросом, не в состоянии проассоциировать вот этот текущий запрос ни с конкретным пользователем, ни с предыдущими запросами. Говорить в этом случае о том, что инициатива в отправке сообщений может исходить не от клиента, а от сервера, не возможно. Протокол rtmp – совсем другое дело: он устанавливает постоянное подключение между клиентом и сервером, что дает возможность серверу в любой момент времени вызвать метод на стороне клиента. В следующем примере я продемонстрирую такой прием. Сначала на стороне flash-клиента я создал новый класс как наследник от NetConnection:
package blz.red5demo {
import flash.net.NetConnection;
public class RemoteNetConnection extends NetConnection{
public function serverMessage(msg : String):void {
trace (msg);
}
}
}
В составе этого класса я определяю тот набор функций, которые хочу разрешить вызывать серверному приложению. Следующим шагом нужно использовать именно этот “усовершенствованный” класс для установления подключения к red5-серверу.
private function doConnect(event:MouseEvent):void {
var context : String = application.parent.loaderInfo.parameters['contextName'];
nc = new RemoteNetConnection();
nc.objectEncoding = ObjectEncoding.AMF0;
nc.addEventListener(NetStatusEvent.NET_STATUS, netStatus);
nc.connect('rtmp://localhost/warmodule');
}
Теперь мне осталось только привести пример кода на стороне сервера. Чтобы не усложнять пример, я в методе установления подключения “appConnect” запускаю таймер, который отсчитывает ровно две секунды. А после этого серверный скрипт вызывает метод serverMessage, передав ему в качестве параметра строку с текущим временем. Вся эта “магия” работает благодаря сервисному классу ServiceUtils и его методу invokeOnConnection. Методу нужно передать как параметры не только имя метода вызываемого на стороне клиента и параметры для него, но, что самое важное, указание на то для какого подключения (сеанса или клиента) нужно делать подобный “обратный” вызов. Учитывая, что метод appConnect вызывается для всех подключений клиентов, то вы можете создать список всех подключений и в случае необходимости послать сообщение либо конкретному клиенту, либо сразу им всем. Хотя подобное “сделанное на коленке” решение можно использовать для создания самых сложных приложений (те же чаты или многопользовательские игры), но в практике им пользоваться не стоит. В red5 и flash есть специально предназначенные для такого класса задач инструменты: SharedObjects и Rooms, но обо всем этом в следующий раз.
@Override
public boolean appConnect(final IConnection conn, Object[] params) {
boolean b = super.appConnect(conn, params);
if (b) {
TimerTask task = new TimerTask() {
public void run() {
ServiceUtils.invokeOnConnection(conn, "serverMessage", new Object[]{"Current time " + new Date()});
}
};
new Timer(true).schedule(task, 2000);
}
return b;
}
Небольшой совет, который я хочу дать перед тем, как завершить сегодняшний материал, связан с ускорением цикла разработки. Так, когда вы запускаете на выполнение команду maven, выполняющую компиляцию проекта, то видите на экране, сколько времени занимает каждый из этапов. В моем частном случае, на сборку всего проекта тратится порядка 50 секунд, из которых на сборку веб-модуля уходит 6 секунд, а на компиляцию flash-интерфейса оставшиеся 44 секунды. Кажется немного, но учитывайте, что за рабочий день подобных циклов, может быть под сотню. Плюс, время будет расти и по мере увеличения количества составляющих проект файлов. Можно ли как-то “разогнать” maven? Да, можно. Методика ускорения работы основана на том факте, что maven для того, чтобы скомпилировать ваш проект загружает в память большое количество библиотек и на это тратится время. Но хуже всего то, что по окончанию сборки проекта, все эти библиотеки выгружаются из оперативной памяти. Есть плагин для maven, который запускает maven в интерактивном режиме командной строки, которая будет работать постоянно, пока вы явно ее не закроете. В этой командной строке вы можете многократно запускать команду компиляции. В моем случае время сборки проекта упало до 4 секунд. Естественно, что подобная методика применима для разработки любого управляемого maven проекта и на любых технологиях (не только flash). Технически, для того, чтобы дать maven поддержку командной строки я должен добавить в корневой pom-файл проекта подключение следующего плагина сразу после секции с перечнем входящих в состав проекта модулей:
<build>
<plugins>
<plugin>
<groupId>org.twdata.maven</groupId>
<artifactId>maven-cli-plugin</artifactId>
<version>0.6.4</version>
</plugin>
</plugins>
</build>
Теперь я запускаю команду maven “m2 cli:execute-phase” (cli расшифровывается как command line interpretation) и после некоторого ожидания, получаю на экране приглашение командной строки, в которой можно многократно запускать команду компиляции проекта “clean install” и отрабатывать она будет за считанные секунды.