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

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

Однако перед тем как перейти к теме java-кода, “разговаривающего” с SharedObject, сначала нужно закрыть вопрос о жизненном цикле SharedObject- ов. То есть когда “общий объект” создается и когда он уничтожается. Для создания хороших приложений нужно четко понимать, в какой момент времени происходит каждое из этих событий и то, от чего они зависят. Например, ответим на вопрос: когда происходит создание SharedObject и можно ли в этот момент выполнить какую-то специальную работу по его подготовке к дальнейшему использованию? Существует два вида SharedObject-ов в зависимости от того, кем они создаются — сервером или клиентом. Показанный в прошлой статье пример чата демонстрировал методику, когда SharedObject создавался flash-клиентом:

chat = SharedObject.getRemote("chat", nc.uri);
chat.addEventListener(SyncEvent.SYNC, onSync)

Итак, когда какой-либо из flash-клиентов вызывает метод getRemote, то flash обращается к серверу red5 и спрашивает его: вдруг, в рамках текущей комнаты (scope) уже есть SharedObject с запрошенным именем? Если это так, то клиенту возвращается ссылка на этот существующий SharedObject. В противном же случае red5 создает новый “пустой” SharedObject, который будет возвращен и этому, и всем последующим клиентам. Всякий раз, когда кто-либо из посетителей чата отсоединяется от SharedObject-а, вызывая на нем метод close или просто закрывая окно браузера с нашим flash- роликом, на сервер red5 посылается специальное сообщение об этом. И как только абсолютно все клиенты отключатся от “общего объекта”, то сервер red5 может его безопасно удалить. В практике использовать создание объектов, инициируемое клиентом, неудобно из-за того, что часто приходится выполнять какую-то подготовительную работу. То есть даже в нашем простом примере с чатом я после получения ссылки на SharedObject анализировал его состояние, и если переданный мне SharedObject был “новорожденным”, то я создавал в нем свойство “история сообщений в чате”:

if (!chat.data.remoteHistory)
chat.data.remoteHistory = [];

Как вы понимаете, в более серьезных приложениях процедура начальной настройки SharedObject-а будет гораздо сложнее и ее лучше выполнять на java- стороне. Не говоря уже о том, что на java-стороне можно к подобному SharedObject-у привязать управляемые spring-ом объекты. Например, сервисы доступа к данным (dao), сервисы обмена сообщениями (jms); и все эти сервисы могут работать в рамках общей “длинной” транзакции. То есть действие, начатое одним пользователем (бизнес-операция и связанная с ней транзакция), будет завершено (транзакция будет подтверждена) другим пользователем. Главное, чтобы эти пользователи были подключены к одному SharedObject-у, тогда они могут видеть и работать с одной и той же информацией.

Следующую часть о том, что такое scope, или комнаты (room), как они соотносятся с приложением (app) и какой у них жизненный цикл, нужно читать, держа перед глазами текст, приведенный в прошлой статье: именно там я ввел базовые понятия комнат, их иерархии и связи с SharedObject-ами. Для демонстрации я создал новое red5-приложение, которое состоит из тех методов, что вызываются при наступлении различных событий в жизненном цикле red5-приложения. Устройство всех приведенных ниже методов тривиально: на экран просто печатается название метода, который был вызван:

public class HelloApplication extends ApplicationAdapter {
public boolean appConnect(final IConnection conn, Object[] params) {
System.out.println("appConnect: client " + conn.getClient());
return super.appConnect(conn, params);}

public boolean roomConnect(IConnection conn, Object[] params) {
System.out.println("roomConnect: conn " + conn);
return super.roomConnect(conn, params); }

public boolean appStart(IScope app) {
System.out.println("appStart: scope " + app);
return super.appStart(app); }

public void appStop(IScope app) {
System.out.println("appStop: scope " + app.getName());
super.appStop(app); }

public boolean roomStart(IScope room) {
System.out.println("roomStart: room " + room.getName());
return super.roomStart(room); }

public void roomStop(IScope room) {
System.out.println("roomStop: room " + room.getName());
super.roomStop(room); }

public boolean appJoin(IClient client, IScope app) {
System.out.println("appJoin: app " + app.getName());
return super.appJoin(client, app); }

public boolean roomJoin(IClient client, IScope room) {
System.out.println("roomJoin: room " + room.getName());
return super.roomJoin(client, room); }

public void roomLeave(IClient client, IScope room) {
System.out.println("roomLeave: room " + room.getName());
super.roomLeave(client, room); }

public void appLeave(IClient client, IScope app) {
System.out.println("roomLeave: app " + app.getName());
super.appLeave(client, app); }
}

На стороне flash-клиента я создал простенькое приложение, состоящее из одного текстового поля и кнопки. В текстовое поле вы должны ввести произвольную строку – адрес подключения к red5-приложению. А после нажатия на кнопку “подключиться” именно этот адрес будет использован при вызове метода connect:

<<>>
<<сайт layout="absolute">>>
<<>><<private var nc:NetConnection;

private function doConnect(event:MouseEvent):void {
nc = new NetConnection();
nc.objectEncoding = ObjectEncoding.AMF0;
nc.addEventListener(NetStatusEvent.NET_STATUS, netStatus);
nc.connect('rtmp://localhost/'+txtRooms.text); }

private function netStatus(event:NetStatusEvent):void {trace (event);}
]]>>><<
>>
<<>>
<<>>
<<>>
<<>>
<<
>>
<<
>> <<
>>

Полученную пару приложений удобно использовать как исследовательский инструмент, чтобы понять, как происходит создание комнат, как выполняется присоединение нового пользователя к существующей комнате и когда комнаты уничтожаются. Очевидно, что методы, начинающиеся на “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”. Приятно, что в любой момент времени вы можете легко узнать ту иерархию комнат, к которой присоединяется клиент, если воспользуетесь следующим примером:
IScope scope = Red5.getConnectionLocal().getScope();
while (scope != null) {
System.out.println("scope " + scope);
scope = scope.getParent();
}

Возвращаясь назад к задаче создания SharedObject-ов: внутри методов roomStart удобнее всего и выполнять создание “общих объектов”. В следующем примере я переиграю наш старый пример с чатом и перенесу код инициализации массива с историей сообщений в чате с flash-клиента на red5-сервер. Также я решил перенести с flash-стороны на java код метода, добавляющего в историю сообщений чата новую запись от клиента при его регистрации в чате, а также сообщение, посылаемое пользователем при нажатии на кнопку “send”. Здесь предполагается, что мы возьмем пример из прошлой статьи и, оставив без изменения его mxml-разметку, отвечающую за внешний вид, попробуем переписать часть бизнес-логики. Во-первых, раз я решил, что модификация SharedObject-а, точнее, его истории, будет выполняться на стороне сервера, а показываться на стороне клиента, то мне потребуется создать общий класс, экземпляры которого будут хранить записи в истории чата. Как я уже говорил в предыдущих статьях, создать подобный общий тип данных совсем несложно: нужно на стороне java и на стороне flash-клиента создать классы с одинаковыми названиями и одинаковой структурой. Также требуется, чтобы у этих классов был конструктор с пустым списком параметров, а те поля, которыми мы хотим обмениваться, должны быть либо объявлены как public, либо иметь привязанные к ним методы get и set. Итак, вот что у меня получилось на стороне flash:
package blz.red5demo {

[RemoteClass(alias="blz.red5demo.ChatHistoryItem")]
public class ChatHistoryItem {
public var user:String;
public var date:Date;
public var message:String;
public function ChatHistoryItem() { }
} }
И соответствующий ему аналог на стороне java:
package blz.red5demo;
import java.util.Date;

public class ChatHistoryItem {
public String user;
public Date date;
public String message; }
Теперь смотрим, как выглядит код flash-клиента применительно к функции создания SharedObject-а. Эти действия я выполняю внутри функции netStatus сразу после того, как получил извещение о том, что соединение с java-стороной было успешно установлено:

private function netStatus(event:NetStatusEvent):void {
if (event.info.code == 'NetConnection.Connect.Success') {
chat = SharedObject.getRemote("chat", nc.uri);
chat.addEventListener(SyncEvent.SYNC, onSync)
chat.connect(nc); }
}
Как видите, я обращаюсь все к тому же методу SharedObject.getConnection для получения ссылки на SharedObject с именем “chat”, находящегося в той же scope (комнате), что была создана на стадии подключения к red5-серверу. В следующем java-коде я в тот же момент времени, когда создается комната, выполняю ее начальную настройку, т.е. создаю SharedObject:
public boolean roomStart(IScope room) {
if (!super.roomStart(room))
return false;
createSharedObject(room, "chat", false);
ISharedObject chat = getSharedObject(room, "chat");
chat.setAttribute("remoteHistory", new ArrayList<<>>());
return true;
}

После того как соединение было установлено и создан SharedObject, flash-клиент получит первое уведомление о синхронизации с SharedObject-ом. Именно здесь я должен послать в чат сообщение о том, что клиент только что зашел в чат. Но я не хочу выполнять эту работу внутри flash-клиента – я хочу отправить извещение java-приложению, чтобы оно выполнило связанную с регистрацией нового клиента работу. Например, чат может ограничивать количество посетителей или требовать предварительной регистрации пользователей — то есть действия, выполнить которые на стороне flash-клиента будет тяжело. И только после всех этих проверок серверный код может изменить содержимое SharedObject-а. Для вызова серверного метода я использую знакомый нам по прошлым статьям метод “call” на объекте NetConnection (почему метод вызывается не на SharedObject, я расскажу попозже). Момент в том, что на этот раз второй параметр функции call равен null. Дело в том, что как раз вторым параметром flash-код передает ссылку на специальный объект Responder, который “слушает” извещения об успешном (или неуспешном) завершении вызова серверного метода. Для моего примера чата такой контроль является избыточным, так что я решил отказаться от Responder-а.
private function onSync(event:SyncEvent):void {
if (chatIsReady == false) {
chatIsReady = true;
nc.call("chatLogin", null, txtUser.text); }
localHistory.source = chat.data.remoteHistory;
}

На java-стороне я определил внутри класса приложения метод chatLogin, который принимает как параметр имя пользователя, зашедшего в чат, и помещает эту информацию в историю сообщений чата:
public void chatLogin(String userName) {
chatMessage(userName, "Пользователь вошел в чат");
}
public void chatMessage(String userName, String message) {
IScope scope = Red5.getConnectionLocal().getScope();
ISharedObject chat = getSharedObject(scope, "chat");
List<<>> history = (List<<>>) chat.getAttribute("remoteHistory");
ChatHistoryItem item = new ChatHistoryItem();
item.user = userName;
item.date = new Date();
item.message = message;
history.add(item);
chat.setAttribute("remoteHistory", history);
}

Как видите, и на стороне сервера, и на стороне flash-клиента вся работа с SharedObject-ом сводится к изменению составляющих его атрибутов. К тому же, в том случае, если java-код хочет “за один раз” изменить несколько атрибутов, то нужно окружить эти действия вызовом функций beginUpdate и endUpdate():
chat.beginUpdate();
// а тут меняем атрибуты
chat.endUpdate();

Завершающий штрих нашей переделки чата – это изменение методики, с помощью которой в чат добавляется текстовое сообщение. Здесь я снова решил использовать прием с посредником: flash-клиент посылает извещение java-приложению, которое после каких-то проверок пришедшего сообщения (например, на предмет наличия стоп-слов) модифицирует SharedObject. А дальше “магия” red5 выполняет рассылку изменений всем зарегистрированным flash-клиентам:
private function say(e:MouseEvent):void {
if (! chatIsReady) {
mx.controls.Alert.show("Сначала нужно подключиться к серверу");
return; }
nc.call("chatMessage", null, txtUser.text, txtMessage.text);
}

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

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


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

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