Статьи

Создание системы голосования по сети P2P с использованием RTMFP

Flash Player 10 принес нам протокол RTMFP , а затем обновление 10.1 добавило гранулярность flash.net.NetGroup . Что это значит для нас? Краткий ответ: простота одноранговой связи. Не очень короткий ответ: сеть с нулевой конфигурацией для обмена сообщениями между несколькими клиентами Flash Player.

Мы посмотрим, что такое RTMFP, как его можно использовать в локальной сети даже без использования сервера Cirrus от Adobe, а затем приступим к реализации наших классов для использования функций netGroup. Первым этапом будет очень простой и понятный клиент для чата, позволяющий наблюдать за обменом сообщениями, а затем мы продолжим разработку приложения для голосования, чтобы создать нечто более приближенное к реальным приложениям.


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

Открыть другую копию в новом окне


RTMFP расшифровывается как Real Flow Media Protocol. Этот сетевой протокол был разработан Adobe и опубликован в Flash Player 10 и AIR 1.5. Перефразируя страницу «Лаборатории Adobe», можно назвать протокол с малой задержкой и одноранговыми возможностями (среди прочих). Здесь мы не будем вдаваться в подробности о том, как это работает, потому что все описано на странице Cirrus на странице Adobe Labs .

Этот протокол обеспечивает автоматическое обнаружение служб, что очень похоже на протокол Apple Bonjour. С точки зрения разработчика Flash, это сложный инструмент, позволяющий автоматически обнаруживать других клиентов нашего «сервиса» в сети. Затем наши Flash-клиенты будут отправлять сообщения (данные или медиа) напрямую друг другу. В Flash Player 10.0 эти соединения были простыми: пирующий издатель отправлял данные каждому другому пиру. Теперь в 10.1 мы имеем многоадресную рассылку, и каждый узел может автоматически действовать как ретранслятор для передачи данных следующему узлу.

Что такое многоадресная рассылка? В традиционных сетевых коммуникациях мы часто работаем в одноадресном режиме, например, HTTP-запрос, сообщение между двумя участниками по сети. Чтобы отправить данные нескольким целям, вы можете сначала подумать об одноадресном широковещании, которое отправляет данные для каждой цели. Если у вас есть n целей, данные отправляются n раз. Довольно дорого. Тогда мы могли бы подумать о трансляции. Чтобы сделать это очень просто, широковещательная рассылка отправляет данные на каждый узел сети. Это означает, что мы также отправляем его на компьютеры, которые не заинтересованы в этом сообщении. Он может выполнять эту работу, но создает дополнительное использование полосы пропускания для сетевых узлов, которые не заинтересованы нашими данными. Затем последовала многоадресная передача, которая является способом передачи данных, который используется в RTMFP. Многоадресная рассылка отправляет одно сообщение, тогда сетевые узлы по мере необходимости позаботятся о его распространении и репликации. Это очень короткое резюме, и страница маршрутизации Википедии отлично справляется с этой задачей.

Для нас важно знать, что что-то сложное и оптимизированное сделано под капотом, и что нам понадобится действительный адрес многоадресной рассылки. Итак, что такое действительный адрес многоадресной рассылки? Диапазон IP-адресов, которые мы могли бы использовать, был установлен IANA. Для IPv4 мы могли бы выбрать любой в диапазоне от 224.0.0.0 до 239.255.255.255, но некоторые адреса были зарезервированы: все 224.0.0.0 до 224.0.0.255 не должны использоваться. Но некоторые адреса за пределами этого диапазона также зарезервированы. Чтобы получить официальный список, вам просто нужно посмотреть официальный список IANA, чтобы узнать, зарезервирован ли адрес, который вы планируете использовать. На следующих шагах мы будем использовать 239.252.252.1 в качестве нашего многоадресного адреса. Знание того, является ли ваш адрес допустимым многоадресным, может быть полезным для отладки вашего проекта, поскольку распространенной ошибкой является использование традиционного одноадресного адреса, например 192.168.0.3. Чтобы подготовиться к будущему, действительные адреса многоадресной рассылки при использовании IPv6 должны находиться в диапазоне ff00 :: / 8.

Последнее замечание: сервер Cirrus от Adobe позволяет обнаруживать одноранговые узлы за пределами вашей локальной сети (т. Е. Через Интернет). Чтобы иметь возможность использовать сервер Cirrus, вам просто нужно зарегистрироваться, чтобы иметь ключ разработчика Cirrus. При использовании RTMFP без сервера Cirrus, мы будем иметь все одноранговые функции, но ограниченные локальной сетью.


Так что Flash Player теперь поддерживает RTMFP. Отлично. Но как мы это используем? Магия подключения происходит в классе flash.net.NetConnection . А поддержка групп, добавленная Flash Player 10.1, осуществляется через класс flash.net.NetGroup .

На следующих шагах мы рассмотрим процесс подключения и какие ключевые слова участвуют, а затем мы объединим это вместе. Итак, давайте начнем с перечисления того, что нам нужно для установления соединения:


Очевидно, нам нужен экземпляр NetConnection, который включен в версию 4.1 Flex SDK . Этот объект NetConnection предоставляет открытый метод connect() . Согласно документации этого метода , connect() ожидает RTMFP URL для создания однорангового соединения и многоадресной IP-связи. Для простого однорангового обнаружения в локальной сети достаточно использовать "rtmfp:" . Для использования соединения с сервером Cirrus нам просто нужно добавить строки сервера и devkey к этому адресу. Это дает действительно простой метод подключения в нашем менеджере:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
/**
 * Using a serverless rtmfp does work for local subnet peer discovery.
 * For internet usage with Cirrus server, something like
 * rtmfp://cirrus.adobe.com/»; will be more useful
 */
public var _rtmfpServer:String = «rtmfp:»;
 
/**
 * If you are using a Cirrus server, you should add your developer key here
 */
public var _cirrusDevKey:String = «»;
 
protected var netConnection:NetConnection = null;
 
public function connect():void
{
    netConnection = new NetConnection();
    netConnection.addEventListener(NetStatusEvent.NET_STATUS, netConnection_netStatusHandler, false,0,true);
    netConnection.connect(_rtmfpServer + _cirrusDevKey);
}

В этом уроке я предполагаю, что вы будете импортировать необходимые классы самостоятельно или что ваша среда IDE сделает это за вас.

После того как мы вызвали метод netConnection.connect() , нам нужно подождать и прослушать событие NetStatusEvent.NET_STATUS со свойством info.code "NetConnection.Connect.Success" . Эта информация об успехе говорит нам, что netConnection готов, поэтому мы сможем создать netGroup в нашем методе netConnection_onConnectSuccess ().

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
/**
 * Callback for NetStatusEvent.NET_STATUS events on my netConnection
 * object, set in the connect() method
 *
 * @param event
 */
private function netConnection_netStatusHandler(event:NetStatusEvent):void
{
     
    switch(event.info.code)
    {
        // Handle connection success
        case NetConnectionCodes.CONNECT_SUCCESS:
            netConnection_onConnectSuccess();
            break;
    //…

NetGroup будет «лобби», в котором общаются наши коллеги. Поэтому это необходимо указать. Вот для чего flash.net.GroupSpecifier класс flash.net.GroupSpecifier . Его конструктор ожидает имя группы, которое будет идентифицировать нашу группу. Мы составим это имя с помощью applicationName и суффикса groupName. Таким образом, будет легко создать несколько групп в одних и тех же приложениях без изменения менеджера netGroup, который мы создаем. Класс netGroup предоставляет несколько полезных параметров:

  • multicastEnabled , который используется для потоков (при использовании с методами netStream.play () или netStream.publish ()). В нашем случае мы не будем работать со средствами массовой информации, но я все еще сохраню это свойство здесь, потому что оно очень полезно для средств массовой информации; по умолчанию это ложь.
  • objectReplicationEnabled — логическое значение (по умолчанию false), которое указывает, включена ли репликация. Для отправки простого сообщения, которое может быть точно таким же, но в разное время (например, уведомление о новом появлении того же события), я обнаружил, что очень полезно явно установить для него значение true, иначе некоторые узлы могут быть слишком умными и думаю, что они уже отправили это сообщение и, следовательно, не будут реплицировать его следующему узлу, чтобы не каждый узел группы получил новое вхождение сообщения.
  • postingEnabled , по умолчанию false, устанавливается, если публикация разрешена в группе. Очевидно, мы установим его в true, так как каждый узел отправит свое обновление через этот механизм.
  • routingEnabled включает прямую маршрутизацию в netGroup.
  • ipMulticastMemberUpdatesEnabled — это флаг, который включает многоадресные IP-сокеты. Это улучшает производительность P2P в контексте локальной сети. Поскольку наш пример будет основан на локальной сети, мы, очевидно, установим для него значение true.
  • serverChannelEnabled помогает обнаружению одноранговых serverChannelEnabled путем создания канала на сервере Cirrus. Поскольку в нашем примере мы не будем использовать Cirrus, мы его пока проигнорируем.

Как только мы установили все эти свойства в соответствии с нашими потребностями, мы установили адрес многоадресной рассылки, который мы будем использовать для соединения данных в нашей группе. Это делается путем вызова groupSpecifier.addIPMulticastAddress("239.252.252.1:20000") . Теперь все настроено, нам просто нужно создать экземпляр NetGroup с этим параметром, чтобы произошло групповое соединение:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
 * applicationName is the common part of the groupSpecifier name we will be using
 */
public var applicationName:String = «active.tutsplus.examples.netGroup/»;
 
/**
 * groupName is added to applicationName to be able to manage several
 * groups for the same application easily.
 */
public var groupName:String = «demoGroup»;
 
/**
 * Callback for NetConnectionCodes.CONNECT_SUCCESS net status for my
 * netConnection object.
 */
private function netConnection_onConnectSuccess():void
{
    var groupSpecifier:GroupSpecifier;
     
    groupSpecifier = new GroupSpecifier(applicationName + groupName);
    groupSpecifier.multicastEnabled = true;
    groupSpecifier.objectReplicationEnabled = true;
    groupSpecifier.postingEnabled = true;
    groupSpecifier.routingEnabled = true;
    groupSpecifier.ipMulticastMemberUpdatesEnabled = true;
    
    groupSpecifier.addIPMulticastAddress(«239.252.252.1:20000»);
     
    netGroup = new NetGroup(netConnection, groupSpecifier.groupspecWithAuthorizations());
    netGroup.addEventListener(NetStatusEvent.NET_STATUS, netGroup_statusHandler);
}

Затем netGroup отправляет событие NetStatusEvent.NET_STATUS со свойством code "NetGroup.Connect.Success" когда оно готово к использованию. Теперь мы можем начать использовать метод netGroup.post(object) для отправки сообщений нашим партнерам.

Мы немного поговорили о NetStatusEvent который дает нам информацию об успешном подключении. Но это также полезно для извлечения всего, что происходит с нашей netConnection или netGroup . Поэтому мы добавим некоторые значения в дерево netConnection / дел, чтобы получить соответствующие значения NetStatusEvent для нашего netConnection :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
switch(event.info.code)
{
    // Handle connection success
    case NetConnectionCodes.CONNECT_SUCCESS:
        netConnection_onConnectSuccess();
        break;
     
    // Handle every case of disconnection
    case NetConnectionCodes.CONNECT_CLOSED:
    case NetConnectionCodes.CONNECT_FAILED:
    case NetConnectionCodes.CONNECT_REJECTED:
    case NetConnectionCodes.CONNECT_APPSHUTDOWN:
    case NetConnectionCodes.CONNECT_INVALIDAPP:
        netConnection_onDisconnect();
        break;
     
    case «NetGroup.Connect.Success»:
        netGroup_onConnectSuccess();
        break;
     
    default:
        break;
     
}

Слушатель netStatusEvent нашего netStatusEvent будет иметь аналогичные тесты для прослушивания групповых действий: отправленные сообщения, одноранговый (или сосед в API-интерфейсе netGroup) присоединенный или оставленный, поэтому у нас будет этот простой метод маршрутизации (некоторые свойства будут реализованы позже, например: или пользовательский класс NetGroupEvent , наше VoNetGroupMessage , но вы ознакомитесь с полученными кодами и можете заметить, что у нас уже есть непосредственный счет подключенных в данный момент одноранговых узлов, прочитав netGroup.neighborCount.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private function netGroup_statusHandler(event:NetStatusEvent):void
{
    switch(event.info.code)
    {
         
        case «NetGroup.Connect.Rejected»:
        case «NetGroup.Connect.Failed»:
            disconnect();
            break;
         
        case «NetGroup.SendTo.Notify»:
            break;
        case «NetGroup.Posting.Notify»:
            messageReceived(new VoNetGroupMessage(event.info.message));
            break;
         
        case «NetGroup.Neighbor.Connect»:
            dispatchEvent(new NetGroupEvent(NetGroupEvent.NEIGHBOR_JOINED));
            // no break here to continue on the same segment than the disconnect part
        case «NetGroup.Neighbor.Disconnect»:
            neighborCount = netGroup.neighborCount;
            break;
         
        case «NetGroup.LocalCoverage.Notify»:
        case «NetGroup.MulticastStream.PublishNotify»:
        case «NetGroup.MulticastStream.UnpublishNotify»:
        case «NetGroup.Replication.Fetch.SendNotify»:
        case «NetGroup.Replication.Fetch.Failed»:
        case «NetGroup.Replication.Fetch.Result»:
        case «NetGroup.Replication.Request»:
        default:
            break;
    }
}

Мы выделили основные ключевые слова и методы для использования NetGroup . На следующем шаге мы немного формализуем это, написав наш собственный класс NetGroupManager чтобы упростить этот API для базового использования при обмене сообщениями.


Поскольку мы не хотим беспокоиться о внутренней работе после того, как она будет выполнена, мы создадим служебный класс для управления сетевым взаимодействием. Этот менеджер будет использовать NetGroupEvent для отправки своего обновления и объект NetGroupMessage для унификации структуры и получения некоторых помощников. Это даст нам общий менеджер, написанный один раз, готовый для использования в любом проекте.

Во-первых, в качестве инструмента отладки мы создадим маленького помощника. В Flash Builder создайте новый проект библиотеки Flex, назовите его LibHelpers , создайте новый пакет 'helpers' и вставьте этот класс DemoHelper в этот пакет:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package helpers
{
    public class DemoHelper
    {
         
        /**
        * Centralisation of all trace methods, and prefix this with a little
        * timestamp
        */
        static public function log(msg:String):void
        {
            trace(timestamp +’\t’ + msg);
        }
         
         
        /**
         * Useful function for logging, formatting the current time in a easy
         * to read line.
         * @return a ‘:’ delimited time string like 12:34:56:789
         *
         */
        static public function get timestamp():String
        {
            var now:Date = new Date();
            var arTimestamp:Array = [now.getHours(), now.getMinutes(), now.getSeconds(), now.getMilliseconds()];
            return arTimestamp.join(‘:’);
        }
         
    }
}

Это дает нам нечто более полезное, чем простая трассировка (). Вызов DemoHelper.put('some string'); выведет сообщение с отметкой времени в качестве префикса. Также очень удобно сгруппировать все функции трассировки в этом центральном месте, чтобы можно было легко отключить их.

Важное предупреждение : поскольку flash.net.netGroup был добавлен в Flash Player 10.1, убедитесь, что вы обновили Flash Builder для работы с версией Flex 4.1.

Теперь вы можете создать новый проект библиотеки Flex с именем LibNetGroup . Создайте пакет 'netgroup' в этом проекте, в который мы добавим наши файлы.


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

Так что это дало нам простое событие netgroup.NetGroupEvent :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
package netgroup
{
    import flash.events.Event;
     
    public class NetGroupEvent extends Event
    {
         
        static public const POSTING:String = ‘posting’;
        static public const CONNECTED_TO_GROUP:String = ‘connectedToGroup’;
        static public const DISCONNECTED_FROM_GROUP:String = ‘disconnectedFromGroup’;
        static public const NEIGHBOR_JOINED:String = ‘neighborJoined’;
         
        public var message:Object;
        public var subject:String;
         
         
        public function NetGroupEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)
        {
            super(type, bubbles, cancelable);
        }
    }
}

Следующим простым шагом мы настроим формат сообщения. Сообщение будет содержать тему, отправителя и некоторые данные. Тема полезна для маршрутизации полученного сообщения в реальном приложении, данные будут неким родовым объектом. Вспомните метод netGroup.post() который мы кратко упомянули на шаге 2. Он принимает объект в качестве аргумента. Итак, у нас будет несколько методов сериализации для создания VoNetMessage из объекта и создания объекта из экземпляра VoNetMessage . Этот метод сериализации будет реализован с помощью простого метода получения, и мы добавим сюда немного соли.

Помните ли вы, что одноранговый автоматически обрабатывает маршрутизацию сообщений? Если узел получает сообщение, которое он уже отправил, он будет считать это сообщение старым и не будет отправлять его снова. Но что произойдет, когда наше приложение отправит небольшое простое уведомление, например, одну строку 'endOfVote' чтобы уведомить клиентов о завершении голосования. Если мы начнем новое голосование, а затем остановим его, это очень простое сообщение будет отправлено еще раз. Точно такое же содержание. Но мы должны убедиться, что коллеги знают, что это еще не новое сообщение. Самый простой способ убедиться в этом — добавить соль в наше сообщение при сериализации перед отправкой. В этом примере я использую простое случайное число, но вы можете добавить метку времени или начать создавать уникальные идентификаторы сообщений. Ты босс. Самое главное, чтобы знать об этой ловушке. Тогда вы готовы справиться с этим так, как вы предпочитаете.

Вот наш netgroup.VoNetGroupMessage в его простейшей форме:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package netgroup
{
    /**
     * VoNetGroupMessage is a little helper that force the subject/data/sender
     * fields before sending a generic object over the air.
     *
     */
    public class VoNetGroupMessage extends Object
    {
          
        public var subject:String;
        public var data:Object;
        public var sender:String;
         
        public function get object():Object
        {
            var o:Object = {};
            o.subject = subject;
            o.data = data;
            o.sender = sender;
            o.salt = Math.random()*99999;
           
            return o;
        }
         
        public function VoNetGroupMessage(message:Object=null)
        {
            if (message == null) message = {};
            subject = message.subject;
            data = message.data;
            sender = message.sender;
        }
    }
}

Чтобы отправить сообщение группе, это будет так же просто, как создать экземпляр VoNetGroupMessage , VoNetGroupMessage его свойства субъекта и данных, а затем передать получатель объекта в метод netGroup.post() . Когда сообщение будет получено от группы, создание его экземпляра с помощью объекта, полученного в качестве аргумента конструктора, автоматически заполнит свойства, готовые для чтения, например:

1
var message:VoNetGroupMessage = new VoNetGroupMessage(objectReceived)

Менеджер будет «черным ящиком», с которым наше приложение будет взаимодействовать, чтобы общаться с коллегами. Мы не хотим, чтобы наша бизнес-логика заботилась о том, как осуществляется сетевое взаимодействие, мы просто хотим отправлять и получать сообщения. Итак, начнем с определения общедоступного API, который мы хотим для нашего менеджера:

  • Он будет отправлять события о том, что происходит, поэтому он расширяет EventDispatcher
  • Будет легко настроить использование собственного значения rtmpServer, developerKey, если необходимо, applicationName и groupName. Нет необходимости редактировать класс, чтобы использовать его для другого приложения.
  • Метод connect () для подключения к нашим партнерам.
  • Метод connect ()
  • Метод sendMessage ().

Давайте посмотрим на это в деталях


Он расширит EventDispatcher и отправит события, которые мы подготовили в NetGroupEvent . У нас были мета-теги событий, чтобы облегчить их использование:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
/**
* Event sent every time a message is received.
*/
[Event(name=»posting», type=»netgroup.NetGroupEvent»)]
 
/**
 * Event sent once the netConnection and netGroup connections have been
 * successful, and the group is ready to be used.
 */
[Event(name=»connectedToGroup», type=»netgroup.NetGroupEvent»)]
 
/**
 * Event sent the netGroup connection is closed.
 */
[Event(name=»disconnectedFromGroup», type=»netgroup.NetGroupEvent»)]
 
/**
 * Event sent the netGroup connection is closed.
 */
[Event(name=»neighborJoined», type=»netgroup.NetGroupEvent»)]

Он также будет иметь некоторые общедоступные привязываемые свойства, чтобы взглянуть на состояние группы. Будьте осторожны, я позволил им использовать их как общедоступные свойства, чтобы сделать код меньшим и легко связываемым, но они не должны быть внешне редактируемыми в реальном проекте.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
/* —————————————————————— */
/* — Useful public properties for managing the group — */
/* —————————————————————— */
static public const STATE_DISCONNECTED:String = ‘disconnected’;
static public const STATE_CONNECTING:String = ‘connecting’;
static public const STATE_CONNECTED:String = ‘connected’;
    
private var _neighborCount:int;
[Bindable(event=’propertyChange’)]
/** neighborCount is a public exposure of the netGroup.neighborCount property */
public function get neighborCount():int { return _neighborCount;
public function set neighborCount(value:int):void
{
    if (value == neighborCount) return;
    _neighborCount = value;
    dispatchEvent(new Event(‘propertyChange’));
}
 
[Bindable] public var connectionState:String = STATE_DISCONNECTED;
 
[Bindable] public var connected:Boolean = false;
[Bindable] public var joinedGroup:Boolean = false;

Давайте передадим ему несколько открытых свойств в качестве аргументов: rtmfpServer , cirrusDevKey , applicationName и groupName . Это должно звучать знакомо, если вы прочитали предыдущие шаги.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* —————————————————————— */
/* — Connection parameters — */
/* —————————————————————— */
 
/**
 * Using a serverless rtmfp does work for local subnet peer discovery.
 * For internet usage with Cirrus server, something like
 * rtmfp://cirrus.adobe.com/»; will be more useful
 */
public var _rtmfpServer:String = «rtmfp:»;
 
/**
 * If you are using a Cirrus server, you should add your developer key here
 */
public var _cirrusDevKey:String = «»;
 
/**
 * applicationName is the common part of the groupSpecifier name we will be using
 */
public var applicationName:String = «active.tutsplus.examples.netGroup/»;
 
/**
 * groupName is added to applicationName to be able to manage several
 * groups for the same application easily.
 */
public var groupName:String = «demoGroup»;

У него будет несколько методов: connect() , disconnect() и sendMessage() . Метод connect почти уже известен, так как все важные вещи были рассмотрены на предыдущем шаге. Метод disconnect — это просто оболочка метода netConnection.close() .

Метод sendMessage будет использовать все, что мы объяснили в структуре VoNetGroupMessage , поэтому нет ничего удивительного в его реализации: он создает экземпляр объекта VoNetGroupMessage , заполняет его поля, затем сериализует его с использованием объекта getter и использует его для netGroup.post() ,

01
02
03
04
05
06
07
08
09
10
11
12
13
14
public function sendMessage(subject:String, messageContents:Object):void
{
    if (!connected)
        throw «Error trying to send a message without being connected»;
     
    // Create the message to send, setting my ID on it and filling it
    // with the contents
    var message:VoNetGroupMessage = new VoNetGroupMessage();
    message.subject = subject;
    message.data = messageContents;
    message.sender = netConnection.nearID;
     
    netGroup.post(message.object);
}

Мы уже имели быстрый взгляд на значения NetStatusEvent на предыдущих шагах; единственная отсутствующая часть — это обработчик messageReceived , вызываемый при получении статуса NetGroup.Posting.Notify . В прослушивателе NetStatusEvent мы создаем экземпляр сообщения, используя полученный объект, а затем передаем его методу messageReceived :

01
02
03
04
05
06
07
08
09
10
private function netGroup_statusHandler(event:NetStatusEvent):void
{
 
    switch(event.info.code)
    {
        // {… }
        case «NetGroup.Posting.Notify»: //
            messageReceived(new VoNetGroupMessage(event.info.message));
            break;
        // other cases…

Этот обработчик messageReceived отправит сообщение. Бизнес-уровень, который мог бы подписаться на это событие, будет затем обрабатывать внутренние детали сообщения, работа netGroupManager , который обрабатывает одноранговые соединения, отправляет и получает данные, выполнена.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
/**
 * NetGroup callback for a netGroup.post(..) command.
 *
 * @param message Object, the structure of this object being defined in the sendMessage handler
 *
 * @see #sendMessage()
 */
private function messageReceived(message:VoNetGroupMessage):void
{
    // Broadcast this message to any suscribers
    var event:NetGroupEvent = new NetGroupEvent(NetGroupEvent.POSTING);
    event.message = message.data;
    event.subject = message.subject;
    dispatchEvent(event);
}

В качестве вехи приведен полный код нашего класса netgroup.NetGroupManager :

001
002
003
004
005
006
007
008
009
010
011
012
013
014
+015
016
+017
018
019
020
021
022
023
024
025
026
027
028
029
+030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
+055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
package netgroup
{
    import flash.events.Event;
    import flash.events.EventDispatcher;
    import flash.events.IEventDispatcher;
    import flash.events.NetStatusEvent;
    import flash.net.GroupSpecifier;
    import flash.net.NetConnection;
    import flash.net.NetGroup;
    import flash.net.NetStream;
    import flash.sampler.getGetterInvocationCount;
    import flash.utils.getTimer;
     
    import helpers.DemoHelper;
     
    import mx.events.DynamicEvent;
     
    import org.osmf.net.NetConnectionCodes;
    import org.osmf.net.NetStreamCodes;
     
    /**
    * Event sent every time a message is received.
    */
    [Event(name=»posting», type=»netgroup.NetGroupEvent»)]
     
    /**
     * Event sent once the netConnection and netGroup connections have been
     * successful, and the group is ready to be used.
     */
    [Event(name=»connectedToGroup», type=»netgroup.NetGroupEvent»)]
     
    /**
     * Event sent the netGroup connection is closed.
     */
    [Event(name=»disconnectedFromGroup», type=»netgroup.NetGroupEvent»)]
     
    /**
     * Event sent the netGroup connection is closed.
     */
    [Event(name=»neighborJoined», type=»netgroup.NetGroupEvent»)]
     
     
      
     
    /**
     *
     *
     */
    public class NetGroupManager extends EventDispatcher
    {
        static public const STATE_DISCONNECTED:String = ‘disconnected’;
        static public const STATE_CONNECTING:String = ‘connecting’;
        static public const STATE_CONNECTED:String = ‘connected’;
         
         
        /* —————————————————————— */
        /* — Connection parameters — */
        /* —————————————————————— */
         
        /**
         * Using a serverless rtmfp does work for local subnet peer discovery.
         * For internet usage with Cirrus server, something like
         * rtmfp://cirrus.adobe.com/»; will be more usefull
         */
        public var _rtmfpServer:String = «rtmfp:»;
         
        /**
         * If you are using a Cirrus server, you should add your developer key here
         */
        public var _cirrusDevKey:String = «»;
         
        /**
         * applicationName is the common part of the groupSpecifier name we will be using
         */
        public var applicationName:String = «active.tutsplus.examples.netGroup/»;
         
        /**
         * groupName is added to applicationName to be able to manage several
         * groups for the same application easily.
         */
        public var groupName:String = «demoGroup»;
         
        /* —————————————————————— */
        /* — Usefull public properties for managing the group — */
        /* —————————————————————— */
        private var _neighborCount:int;
        [Bindable(event=’propertyChange’)]
        /** neighborCount is a public exposure of the netGroup.neighborCount property */
        public function get neighborCount():int { return _neighborCount;
        public function set neighborCount(value:int):void
        {
            if (value == neighborCount) return;
            _neighborCount = value;
            dispatchEvent(new Event(‘propertyChange’));
        }
         
        [Bindable] public var connectionState:String = STATE_DISCONNECTED;
         
        [Bindable] public var connected:Boolean = false;
        [Bindable] public var joinedGroup:Boolean = false;
         
        protected var netConnection:NetConnection = null;
        protected var netStream:NetStream = null;
        protected var netGroup:NetGroup = null;
         
         
          
        
        public function NetGroupManager(target:IEventDispatcher=null)
        {
            super(target);
        }
         
     
         
        public function connect():void
        {
            _log(«Connecting to ‘» + _rtmfpServer);
            connectionState = STATE_CONNECTING;
             
            netConnection = new NetConnection();
            netConnection.addEventListener(NetStatusEvent.NET_STATUS, netConnection_netStatusHandler, false,0,true);
            netConnection.connect(_rtmfpServer + _cirrusDevKey);
        }
         
         
        /**
         * disconnect will be automatically called when a netGroup connection or
         * a netStream connection will be rejected or have failed.
         *
         * @see #netConnection_onDisconnect()
         */
        public function disconnect():void
        {
            if(netConnection) netConnection.close();
        }
         
         
        public function sendMessage(subject:String, messageContents:Object):void
        {
            if (!connected)
                throw «Error trying to send a message without being connected»;
             
            // Create the message to send, setting my ID on it and filling it
            // with the contents
            var message:VoNetGroupMessage = new VoNetGroupMessage();
            message.subject = subject;
            message.data = messageContents;
            message.sender = netConnection.nearID;
             
            netGroup.post(message.object);
        }
         
         
         
         
         
        /**
         * Callback for NetConnectionCodes.CONNECT_SUCCESS net status for my
         * netConnection object.
         */
        private function netConnection_onConnectSuccess():void
        {
            var groupSpecifier:GroupSpecifier;
             
            connected = true;
            connectionState = STATE_CONNECTED;
            _log(«Connected»);
             
            groupSpecifier = new GroupSpecifier(applicationName + groupName);
            groupSpecifier.multicastEnabled = true;
            groupSpecifier.objectReplicationEnabled = true;
            groupSpecifier.postingEnabled = true;
            groupSpecifier.routingEnabled = true;
             
            // Server channel might help peer finding each other when using a Cirrus server
            //groupSpecifier.serverChannelEnabled = true;
             
            // ipMulticastMemberUpdatesEnabled is new in Flash player 10.1
            groupSpecifier.ipMulticastMemberUpdatesEnabled = true;
             
            /*
            The multicast IP you enter has to be in the 224.0.0.0 to
            239.255.255.255 range, or ff00::/8 if you’re using IPv6
            Some IP are reserved and should not be used, see
            http://www.iana.org/assignments/multicast-addresses/ for this list.
            */
            groupSpecifier.addIPMulticastAddress(«239.252.252.1:20000»);
             
            netGroup = new NetGroup(netConnection, groupSpecifier.groupspecWithAuthorizations());
            netGroup.addEventListener(NetStatusEvent.NET_STATUS, netGroup_statusHandler);
             
            /* netStream will not be used in this example, but I kept the code
            here to give you an entry point to go further.
            // netStream = new NetStream(netConnection, groupSpecifier.groupspecWithAuthorizations());
            // netStream.addEventListener(NetStatusEvent.NET_STATUS, NetConnectionStatusHandler);
             
            /*
            You might enable the following line to have a look at what’s your
            current groupSpecifier looks like:
              sample ouput :
              10:58:45:529 Join ‘G:01010103010c2c0e6163746976652e74757473706c75732e6578656d706c65732e6e657447726f75702f64656d6f47726f7570011b00070aee0000014e20’
            */
            // _log(«Join ‘» + groupSpecifier.groupspecWithAuthorizations() + «‘»);
        }
         
         
        /**
         * Callback for NetConnectionCodes.CONNECT_SUCCESS net status for my
         * netConnection object.
         */
        private function netStream_onConnectSuccess():void
        {
            netStream.client = this;
            // We’re not using media for now, but it’s the right place to use
            // netStream.attachAudio(..) and netStream.attachCamera if you want
            // to create a video chat or broadcast some media
            netStream.publish(«mystream»);
        }
         
         
        private function netGroup_onConnectSuccess():void
        {
            joinedGroup = true;
             
            dispatchEvent(new NetGroupEvent(NetGroupEvent.CONNECTED_TO_GROUP));
        }
         
         
        private function netConnection_onDisconnect():void
        {
            _log(«Disconnected\n»);
            connectionState = STATE_DISCONNECTED;
            netConnection = null;
            netStream = null;
            netGroup = null;
            connected = false;
            joinedGroup = false;
             
            dispatchEvent(new NetGroupEvent(NetGroupEvent.DISCONNECTED_FROM_GROUP));
        }
         
         
        /**
         * Callback fot NetStatusEvent.NET_STATUS events on my netConnection
         * object, set in the connect() method
         *
         * @param event
         */
        private function netConnection_netStatusHandler(event:NetStatusEvent):void
        {
            _log("netConnection status:" + event.info.code);
             
            switch(event.info.code)
            {
                // Handle connection success
                case NetConnectionCodes.CONNECT_SUCCESS:
                    netConnection_onConnectSuccess();
                    break;
                 
                // Handle very case of disconnection
                case NetConnectionCodes.CONNECT_CLOSED:
                case NetConnectionCodes.CONNECT_FAILED:
                case NetConnectionCodes.CONNECT_REJECTED:
                case NetConnectionCodes.CONNECT_APPSHUTDOWN:
                case NetConnectionCodes.CONNECT_INVALIDAPP:
                    netConnection_onDisconnect();
                    break;
                 
                case "NetGroup.Connect.Success":
                    netGroup_onConnectSuccess();
                    break;
                 
                default:
                    break;
                 
            }
        }
         
    
         
        private function netGroup_statusHandler(event:NetStatusEvent):void
        {
            _log("netGroup status:" + event.info.code);
            switch(event.info.code)
            {
                 
                case "NetGroup.Connect.Rejected":
                case "NetGroup.Connect.Failed":
                    disconnect();
                    break;
                 
                case "NetGroup.SendTo.Notify":
                    messageReceived(new VoNetGroupMessage(event.info.message));
                    break;
                case "NetGroup.Posting.Notify": //
                    messageReceived(new VoNetGroupMessage(event.info.message));
                    break;
                 
                case "NetGroup.Neighbor.Connect":
                    dispatchEvent(new NetGroupEvent(NetGroupEvent.NEIGHBOR_JOINED));
                    // no break here to continue on the same segment than the disconnect part
                case "NetGroup.Neighbor.Disconnect":
                    neighborCount = netGroup.neighborCount;
                    break;
                 
                     
                case "NetGroup.LocalCoverage.Notify": //
                     
                case "NetGroup.MulticastStream.PublishNotify": // event.info.name
                case "NetGroup.MulticastStream.UnpublishNotify": // event.info.name
                case "NetGroup.Replication.Fetch.SendNotify": // event.info.index
                case "NetGroup.Replication.Fetch.Failed": // event.info.index
                case "NetGroup.Replication.Fetch.Result": // event.info.index, event.info.object
                case "NetGroup.Replication.Request": // event.info.index, event.info.requestID
                 
                default:
                    break;
                
            }
        }
         
         
         
        private function netStream_statusHandler(event:NetStatusEvent):void
        {
            switch(event.info.code)
            {
                /*
                The netStream usefull event codes are kept here, even if in
                this demo you're will not be using them. This gives you a brief
                overview of what you receive.
                */
                case "NetStream.Connect.Success":
                    netStream_onConnectSuccess();
                    break;
                 
                case "NetStream.Connect.Rejected":
                case "NetStream.Connect.Failed":
                    disconnect();
                    break;
                 
                case "NetStream.MulticastStream.Reset":
                case "NetStream.Buffer.Full":
                    break;
                 
            }
        }
         
         
         
        /**
         * NetGroup callback for a netGroup.post(..) command.
         *
         * @param message Object, the structure of this object being defined in the sendMessage handler
         *
         * @see #sendMessage()
         */
        private function messageReceived(message:VoNetGroupMessage):void
        {
            _log("[" + message.sender + "] " + message.data);
             
            // Broadcast this message to any suscribers
            var event:NetGroupEvent = new NetGroupEvent(NetGroupEvent.POSTING);
            event.message = message.data;
            event.subject = message.subject;
            dispatchEvent(event);
        }
          
        /**
        * Simple 'trace' wrapper
        */
        private function _log(msg:Object):void
        {
            DemoHelper.log(msg as String);
        }
         
    }
}

Давайте создадим новый проект Flash в Flash Builder, который я назвал Demo_NetGroupChat. Поскольку в этом проекте будет использоваться менеджер, который мы только что написали, мы должны добавить ссылку на менеджер проекта. Это просто сделать, открыв свойства проекта и в Путь сборки Flex, нажав кнопку «Добавить проект …» и выбрав проект LibNetGroup.

Мы будем связывать экземпляр наших NetGroupManagerкак lanGroupMgrпеременный. Мы добавим кнопку подключения, которая будет вызывать метод подключения нашего менеджера, некоторые метки, связанные с общими свойствами наших менеджеров connectionState, joinedGroupи neighborCount.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
<s:HGroup>
    <s:Button id="btnConnect"
              label="connect"
              click="btnConnect_clickHandler(event)"
              visible="{!lanGroupMgr.connected}"
              />
    <s:Label text="{lanGroupMgr.connectionState}"/>
     
    <s:Label text="group joined" visible="{lanGroupMgr.joinedGroup}"/>
</s:HGroup>
 
<s:HGroup visible="{lanGroupMgr.joinedGroup}">
    <s:Label text="Neighbor count :" />
    <s:Label text="{lanGroupMgr.neighborCount}"/>
</s:HGroup>

Для обмена сообщениями нам просто нужен компонент TextInput, кнопка отправки и консоль textArea для отображения полученного сообщения:

1
2
3
4
5
6
7
8
<s:HGroup>
    <s:TextInput id="inputMsg" />
    <s:Button id="btnSend"
              enabled="{inputMsg.text != ''}"
              label="send"
              click="btnSend_clickHandler(event)"/>
</s:HGroup>
<s:TextArea id="console"/>

Это почти закончено.

Мы должны:

  • прослушивать новые сообщения, полученные путем добавления слушателя в наш creationCompleteобработчик,
  • написать btnConnect_clickHandlerобработчик для вызова метода connect нашего менеджера.
  • Отправка сообщения чата — это просто вызов sendMessageметода нашего менеджера с содержимым textInput, обернутым в объект.
  • Чтобы прочитать и отобразить полученное сообщение, в нашем messagePostedслушателе мы просто должны прочитать содержимое объекта в его messageсвойстве и добавить его в поле консоли.

Это все. Вот блок скриптов документа mxml:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import helpers.DemoHelper;
 
import mx.events.FlexEvent;
 
import netgroup.NetGroupEvent;
import netgroup.NetGroupManager;
 
[Bindable] protected var lanGroupMgr:NetGroupManager;
 
protected function application1_creationCompleteHandler(event:FlexEvent):void
{
    lanGroupMgr = new NetGroupManager();
    lanGroupMgr.addEventListener(NetGroupEvent.POSTING, onMessagePosted);
}
 
protected function onMessagePosted(event:NetGroupEvent):void
{
    var message:Object = event.message;
    console.appendText(DemoHelper.timestamp+' '+ message.contents +'\n');
}
 
 
protected function btnConnect_clickHandler(event:MouseEvent):void
{
    lanGroupMgr.connect();
}
 
 
protected function btnSend_clickHandler(event:MouseEvent):void
{
    // Creation of a message to send
    var msg:Object = {};
    msg.contents = inputMsg.text;
    lanGroupMgr.sendMessage('chat', msg);
    callLater(resetInput);
         
}
 
private function resetInput():void
{
    inputMsg.text = '';
}

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

Открыть другую копию в новом окне


Теперь мы создадим приложение, немного более сложное, чем чат, чтобы посмотреть, как обрабатывать сообщения различного типа и как изменять состояние приложения в зависимости от событий, полученных от других узлов сети. В той же группе NetGroup мы предоставим пользователям возможность выбора:

  • voteManager. В группе будет только один менеджер. VoiceManager отредактирует вопрос и доступные ответы, а затем отправит запрос на голосование всем участникам с данными тезисов. Менеджеры голосования будут получать и отображать результаты голосования в режиме реального времени каждый раз, когда клиент отвечает. Затем voiceManager отправит результаты голосования другим участникам группы, как только все подключенные клиенты ответят, или если менеджер вручную решит прекратить голосование.
  • voteClient. Клиент покажет экран «Пожалуйста, подождите», пока не получит вопрос, а затем покажет возможные ответы конечному пользователю. Когда пользователь выбирает свои ответы, клиент отправляет выбранные ответы менеджеру по голосованию. Наконец, когда менеджер голосования отправит результаты голосования, клиент покажет, сколько раз каждый ответ был выбран пользователями группы.

Чтобы было немного умнее, мы создадим только одно приложение. Когда пользователь «А» присоединяется к группе, он может выбрать, хочет ли он быть менеджером или клиентом. Если менеджер голосования уже задан пользователем «B» в группе, он предупреждает «A» о своем существовании, поэтому приложение «A» автоматически выберет клиент голосования и начнет ждать вопроса.


На следующих шагах мы перечислим каждый элемент, который нам нужен для создания этого приложения для голосования: очевидно, что NetGroupManagerмы создаем на предыдущих этапах, но также модель, событие, некоторые представления для отображения пользовательского интерфейса. И, наконец, контроллер для связи модели с представлениями.

Создайте новый проект во Flex Builder и назовите его «Demo_NetGroup_Votes». Добавьте ссылку на проект LibNetGroup, как мы уже сделали для быстрого теста чата. Таким образом, мы сможем использовать наши классы NetGroupManager.

В srcпапке давайте создадим несколько папок, которые мы будем заполнять на следующих шагах:

  • 'models ‘, этот пакет будет хранить нашу модель
  • 'events' этот пакет будет содержать наше пользовательское событие
  • 'controllers' , в который мы поместим контроллер нашего приложения для голосования
  • 'screens' который будет заполнен компонентами MXML, которые мы будем использовать в качестве представлений для нашего приложения

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

Начните с создания нового VoteDefinition.asкласса в modelsпакете. Мы добавим некоторые открытые свойства для хранения вопроса и до пяти ответов в виде строк. Конструктор этого класса будет читать универсальный объект и заполнять его свойства этими значениями.

Больше ничего нет, поэтому код очень прост и понятен:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package models
{
     
    /**
     * VoteDefinition is the data container that contains all the needed
     * informations to vote
     */
    public class VoteDefinition
    {
         
        [Bindable] public var question:String;
        [Bindable] public var answer1:String;
        [Bindable] public var answer2:String;
        [Bindable] public var answer3:String;
        [Bindable] public var answer4:String;
        [Bindable] public var answer5:String;
         
        // Some extra parameters to give ideas of how to expand the application
        public var timeLimit:int = 0; // If 0, then no time limit
        public var multipleAnswers:Boolean = true;
         
        public function VoteDefinition(obj:Object=null)
        {
            if (obj)
            {
                for (var prop:String in obj)
                {
                    this[prop] = obj[prop];
                }
            }
        }
         
    }
}

Наше приложение должно будет реагировать на несколько уведомлений. По мере расширения приложения вы сможете добавлять некоторые функции и события, но для нашего основного теста список пользовательских событий, которые нам понадобятся, будет очень коротким:

  • roleSet событие, когда приложение знает, должно ли оно вести себя как менеджер голосования или как клиент голосования
  • startVote когда голос должен быть показан конечному пользователю клиентом для голосования
  • stopVote , когда менеджер просит всех прекратить голосование взаимодействия
  • showResults когда все голоса были объединены и разосланы клиентам, чтобы они отобразили результаты
  • answersReceived Когда менеджер голосования получает ответ от клиента, чтобы обновить текущие результаты в режиме реального времени на экране менеджера.

Нам также необходимо хранить некоторую информацию, например данные о голосовании или результаты для отображения. Вместо того, чтобы использовать динамический объект, мы просто добавляем свойство данных, типизированное как Object, для хранения VoteDefinitionпереданного, индекса массива ответов или любых других соответствующих данных … (подробнее об этом позже)

Чтобы превратить это в код, создайте VoteEventкласс, расширяющийся flash.events.Eventв eventsпакете. Мы добавляем несколько типов событий, которые мы только что перечислили, и свойство data:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
package events
{
    import flash.events.Event;
     
    public class VoteEvent extends Event
    {
         
        static public const ROLE_SET:String = 'roleSet';
        static public const START_VOTE:String = 'startVote';
        static public const STOP_VOTE:String = 'stopVote';
        static public const SHOW_RESULTS:String = 'showResults';
        static public const ANSWERS_RECEIVED:String = 'answersReceived';
         
        public var data:Object;
         
        public function VoteEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)
        {
            super(type, bubbles, cancelable);
        }
    }
}

Теперь у нас есть данные, с которыми мы будем играть, событие для уведомления об изменениях. Но нам все еще нечего показать. Давайте перечислим представления, которые нам понадобятся:

  • основной контейнер, который будет общей частью приложения. Он инициализирует VoteControllerи запрашивает соединение с группой, а затем может переключиться на несколько состояний: init , roleChoice , voiceManager и voiceClient .
  • клиентские экраны, реализующие несколько состояний: waitVote , VoteInProgress , waitVoteEnd , voiceResults
  • экран диспетчера, реализующий меньшее количество состояний: editVote , voiceInProgress , voiceStopped .

Я не даю здесь полную информацию о состояниях каждого представления, потому что их имена и функции совершенно очевидны.

Эти представления будут использовать два пользовательских компонента:

  • базовый компонент, используемый для отображения результатов голосования
  • экран voiceResults для отображения результатов.

Контроллер будет связующим звеном между нашей моделью голосования, представлениями и коллегами. Поэтому этот контроллер будет единственным, который будет использовать NetGroupManagerранее созданную нами сборку.

Мы реализуем этот контроллер как синглтон и добавим метатеги, чтобы ссылаться на созданный нами ранее VoteEvent.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package controllers
{
    import events.VoteEvent;
     
    import flash.events.Event;
    import flash.events.EventDispatcher;
    import flash.events.IEventDispatcher;
     
    import models.VoteDefinition;
     
    import netgroup.NetGroupEvent;
    import netgroup.NetGroupManager;
     
     
    /**
     * Event sent once the netConnection and netGroup connections have been
     * successful, and the group is ready to be used.
     */
    [Event(name="connectedToGroup", type="netgroup.NetGroupEvent")]
     
    /**
     * Event sent when the role has been set, by the user of from the netGroup
     */
    [Event(name="roleSet", type="events.VoteEvent")]
     
     
    [Event(name="startVote", type="events.VoteEvent")]
     
    [Event(name="stopVote", type="events.VoteEvent")]
     
    [Event(name="showResults", type="events.VoteEvent")]
     
    /** answersReceived is sent when receiving an answer which isn't the last one */
    [Event(name="answersReceived", type="events.VoteEvent")]
     
     
    /**
     * VoteController manage the NetGroup connection and all data exchanges.
     * VoteController is implemented as a Singleton.
     *
     */
    public class VoteController extends EventDispatcher
    {
         
        /* Singleton management */
        static private var _instance:VoteController;
        static public function get instance():VoteController
        {
            if (!_instance) _instance = new VoteController();
            return _instance;
        }

Одной из важных функций VoteController является указание списка доступных тем сообщений, которые мы будем обмениваться в NetGroup, которую мы создадим для этого приложения:

  • setVoteManagerExistence (чтобы участники могли знать, что в группе уже есть менеджер голосования),
  • startVote,
  • stopVote,
  • submitAnswers,
  • показать результаты.

Контроллер также устанавливает роли, которые может выбрать пользователь приложения.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
/* Role definition */
static public const ROLE_CLIENT:String = 'client';
static public const ROLE_MANAGER:String = 'manager';
private var _role:String;
[Bindable]
}
public function set role(value:String):void
{
    if (_role != null) return; // Can be set only once
    _role = value;
    _handshakeVoteManager();
    dispatchEvent(new VoteEvent(VoteEvent.ROLE_SET));
}
 
 
 
static public const SUBJECT_SET_VOTE_MANAGER_EXISTENCE:String = 'setVoteManagerExistence';
static public const SUBJECT_START_VOTE:String = 'startVote';
static public const SUBJECT_STOP_VOTE:String = 'stopVote';
static public const SUBJECT_SUBMIT_ANSWERS:String = 'submitAnswers';
static public const SUBJECT_SHOW_RESULTS:String = 'showResults';

Если вы посмотрите на функцию установки ролей, вы заметите, что roleSetсобытие отправляется, и что мы вызываем _handshakeVoteManager()метод. Этот метод будет довольно простым: если мы voteManager, то мы говорим нашим коллегам, что мы здесь. Мы посмотрим через несколько минут, как наши коллеги будут слушать это сообщение.

1
2
3
4
5
6
7
8
private function _handshakeVoteManager():void
{
    if (role == ROLE_MANAGER)
    {
        // If I'm a vote manager, I warn this newcomer of my existence
        netGroupMgr.sendMessage(SUBJECT_SET_VOTE_MANAGER_EXISTENCE, {});
    }
}

Мы использовали netGroupMgr.sendMessage()функцию нашего NetGroupManager. Это означает, что мы должны инициализировать этот менеджер и установить соединение раньше. Это то, что мы делаем в конструкторе этого VoteController. Это также подходящее место для добавления прослушивателей netGroupManagerсобытий, чтобы знать, когда публикуется сообщение, когда присоединяется одноранговый узел, и, что более важно, если мы соединены в группу.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
protected var netGroupMgr:NetGroupManager;
 
public function VoteController(target:IEventDispatcher=null)
{
    super(target);
    if (_instance) return;
 
    netGroupMgr = new NetGroupManager();
    netGroupMgr.applicationName = "active.tutsplus.examples/";
    netGroupMgr.groupName = "sampleApp";
    netGroupMgr.addEventListener(NetGroupEvent.POSTING, onNetGroupMessage, false,0,true);
    netGroupMgr.addEventListener(NetGroupEvent.CONNECTED_TO_GROUP, onConnectionEstablished, false,0,true);
    netGroupMgr.addEventListener(NetGroupEvent.DISCONNECTED_FROM_GROUP, onConnectionLost, false,0,true);
    netGroupMgr.addEventListener(NetGroupEvent.NEIGHBOR_JOINED, onNeighborJoined, false,0,true);
     
    netGroupMgr.connect();
}

В этом конструкторе мы создали экземпляр нашего NetGroupManager, задали имя нашей группы и установили соединение. Мы также добавили несколько слушателей для основных групповых событий, которые наш менеджер отправит нам. Давайте посмотрим, как обрабатывать эти групповые события.

Когда соединение выполнено (помните, что мы имеем дело с успешным соединением NetGroupManager, что означает, что оно успешно подключилось к a NetConnectionзатем to a NetGroup), мы установим флаг «connected» в true и VoteEventотправим a, чтобы приложение знало что инициализация сделана.

01
02
03
04
05
06
07
08
09
10
11
12
13
[Bindable]
public var connected:Boolean = false;
 
protected function onConnectionEstablished(event:NetGroupEvent):void
{
    connected = true;
    dispatchEvent(new Event(NetGroupEvent.CONNECTED_TO_GROUP));
}
 
protected function onConnectionLost(event:NetGroupEvent):void
{
    connected = false;
}

Когда одноранговый участник присоединяется к группе, чтобы NetGroupManagerдать нам neighborJoinedсобытие, мы сообщаем этому одноранговому участнику, если мы — voiceManager. Таким образом, как только приложение подключится к группе, оно будет информировано voiceManager, если такой менеджер уже существует в группе.

1
2
3
4
protected function onNeighborJoined(event:NetGroupEvent):void
{
    _handshakeVoteManager();
}

Последний netGroupManagerпрослушиватель, который мы определили, это обратный вызов отправки. Он будет срабатывать каждый раз, когда мы получим сообщение от группы. Чтобы обработать эти сообщения, нам просто нужно выполнить переключение / регистр со значениями субъекта, которые мы уже установили ранее:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/* Current vote buffers */
public var currentVoteDefinition:VoteDefinition;
public var currentVoteAnswers:Array;
public var currentVoteUsers:int;
public var totalVoteClients:int;
 
/**
 * Callback for all the netGroup message we will receive.
 * Message routing, based on the message subject, will be done here. 
 */
protected function onNetGroupMessage(event:NetGroupEvent):void
{
    var message:Object = event.message;
    if (!message) return;
    var subject:String = event.subject as String;
   
    switch (subject)
    {
        case VoteController.SUBJECT_SET_VOTE_MANAGER_EXISTENCE:
            // Another peer is already a vote manager, so I'll be a
            // vote client if I haven't yet set my role
            if (role == null)
                role = ROLE_CLIENT;
            break;
         
        case SUBJECT_START_VOTE:
            // We just received a vote, send it !
            doStartVote(new VoteDefinition(message));
            break;
         
        case SUBJECT_SUBMIT_ANSWERS:
            if (role == ROLE_MANAGER)
            {
                registerClientAnswers(message as Array);
            }
            break;
             
        case SUBJECT_STOP_VOTE:
            dispatchEvent(new VoteEvent(VoteEvent.STOP_VOTE));
            break;
             
        case SUBJECT_SHOW_RESULTS:
            currentVoteDefinition = new VoteDefinition(message.voteDefinition);
            currentVoteAnswers = message.answers;
            currentVoteUsers = message.voteUserCnt;
            totalVoteClients = message.totalVoteClients;
             
            dispatchEvent(new VoteEvent(VoteEvent.SHOW_RESULTS));
             
            break;
    }
         
}

Вы помните _handshakeVoteManager()метод из шага 13, который вызываетnetGroupMgr.sendMessage(SUBJECT_SET_VOTE_MANAGER_EXISTENCE, {}); ?

В SUBJECT_SET_VOTE_MANAGER_EXISTENCEветке мы видим другую часть сообщения, когда узел его получает. При получении этой информации, если роль этого партнера еще не была установлена, мы заставляем ее быть клиентом, поскольку в группе уже существует менеджер. Поскольку метод handshakeVoteManager вызывается каждый раз, когда мы устанавливаем свою роль, и каждый раз, когда одноранговый участник присоединяется к группе, мы уверены, что каждый новый одноранговый узел будет знать о существовании любого менеджера.

stopVoteСообщение отправляет событие, более подробно об этом в шагах зрения.

Для startVote, submitAnswerи showResultsФилиалов, мы используем данные , содержащиеся в сообщении. Тип этих данных будет варьироваться в зависимости от предмета.

startVoteОтделение примет это сообщение и отправляет его в startVoteслучае с мнением:

01
02
03
04
05
06
07
08
09
10
/**
 * doStartVote will be called by submitVote for the VoteManager, and from
 * onNetGroupMessage for the voteClients, when receiving a SUBJECT_START_VOTE message
 */
protected function doStartVote(def:VoteDefinition):void
{
    var evt:VoteEvent = new VoteEvent(VoteEvent.START_VOTE);
    evt.data = def;
    dispatchEvent(evt);
}

showResultsРаботает почти так же, но данные хранятся в VoteControllerкачестве государственных буферов, и отправляется событие.

Мы увидим, что каждый клиент отправляет свои ответы в виде массива целых чисел. Если клиенты выбрали ответ два и четыре, сообщение будет [2,4]. Вот почему ответы субъекта при вызове обрабатывают переменную сообщения как массив registerClientAnswers.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
protected function registerClientAnswers(answers:Array):void
{
    // answers contains the position and the answers selected by the user, increment our counters
    for each (var ind:int in answers)
    {
        currentVoteAnswers[ind]++;
    }
     
    currentVoteUsers++;
     
    // notify the manager of the vote progression
    dispatchEvent(new VoteEvent(VoteEvent.ANSWERS_RECEIVED));
     
     
    if (currentVoteUsers >= totalVoteClients)
    {
        // We have received all the answers
        dispatchEvent(new VoteEvent(VoteEvent.STOP_VOTE));
        netGroupMgr.sendMessage(SUBJECT_STOP_VOTE, 'all answers received');
    }
     
}

Контроллер менеджера хранит информацию о том, сколько раз были выбраны все ответы, просто увеличивая различные значения массива ответов. Затем он отправляет событие, чтобы сообщить представлениям о получении нового ответа. Таким образом, менеджер будет отображать результаты в режиме реального времени. Наконец, если все ожидаемые нами голоса были получены, мы прекращаем голосование и уведомляем как локального пользователя о событии, так и одноранговых узлов с stopVoteсообщением. Как мы уже видели несколько строк ранее, это сообщение будет отправлять то же stopVoteсобытие.

Наш менеджер правильно инициализирует и обрабатывает сообщения, которые он получает от группы. Теперь нам нужно убедиться, что эти сообщения отправлены, и, следовательно, нам все еще нужно добавить несколько открытых методов, чтобы наши мнения могли голосовать и отправлять свои ответы.

01
02
03
04
05
06
07
08
09
10
11
12
public function submitVote(def:VoteDefinition):void
{
    if (role != ROLE_MANAGER) return;
     
    currentVoteDefinition = def;
    currentVoteAnswers = [null,0,0,0,0,0]; // no answers logged yet. 0 is the question and therefore cannot be answered
    currentVoteUsers = 0;
    totalVoteClients = netGroupMgr.neighborCount; // We store the number of answer we'll be waiting.
     
    doStartVote(def);
    netGroupMgr.sendMessage(SUBJECT_START_VOTE, def);
}

Чтобы начать голосование, представление должно будет только пройти VoteDefinition(т.е. тексты вопроса и ответов). Мы инициализируем наши буферы для хранения вопросов, помним, сколько ответов было получено и сколько мы будем ждать. Затем мы отправляем startVoteсообщение нашим коллегам. Сохраняя totalVoteClientsномер при начале голосования, я уверен, что дождусь правильного подсчета ответов, даже если новые участники присоединятся к группе во время голосования (эти участники должны будут ждать следующего голосования, чтобы принять участие).

Отправить ответ тривиально: мы уже говорили, что наши ответы — это простой массив позиций ответов, выбранных конечным пользователем. Мы просто отправляем это менеджеру:

1
2
3
4
public function submitAnswers(answers:Array /*of int*/):void
{
    netGroupMgr.sendMessage(SUBJECT_SUBMIT_ANSWERS, answers);
}

Наконец, когда голосование закончено, менеджер голосования отправит результаты голосования каждому пиру, чтобы каждый клиент голосования мог отобразить полные результаты.

01
02
03
04
05
06
07
08
09
10
11
public function parseResultsForBroadcast():void
{
    var msg:Object = {
        answers:currentVoteAnswers,
        voteDefinition:currentVoteDefinition,
        voteUserCnt:currentVoteUsers,
        totalVoteClients:totalVoteClients
    };
     
    netGroupMgr.sendMessage(SUBJECT_SHOW_RESULTS, msg);
}

Вот и все.Мы закончили модель, контроллер и событие. Нам просто нужно иметь некоторый графический интерфейс для отображения выбора для пользователя и прослушивания этих событий.


Хост-файл MXML будет основой наших представлений приложений. Он будет содержать общие части как менеджера, так и клиента. Как мы уже говорили ранее, в нем будет несколько состояний:

1
2
3
4
5
6
<s:states>
    <s:State name="init" />
    <s:State name="roleChoice" />
    <s:State name="voteManager" />
    <s:State name="voteClient" />
</s:states>

Состояние init будет значением по умолчанию, когда мы ожидаем, что контроллер отправит событие connectedToGroup, чтобы сообщить нам, что мы готовы к работе. Затем приложение войдет в состояние roleChoice , ожидая, что роль этого экземпляра установлена ​​как менеджер или клиент. Наконец, когда роль установлена, она переходит в состояние voiceManager или voiceClient , в зависимости от роли.

Итак, состояние init будет начинаться с

01
02
03
04
05
06
07
08
09
10
11
12
protected function _initializeHandler(event:FlexEvent):void
{
    // We make sure the VoteController has been instantiated, simply by asking for its singleton reference.
    var voteCtrl:VoteController = VoteController.instance;
    voteCtrl.addEventListener(NetGroupEvent.CONNECTED_TO_GROUP, initDone);
    voteCtrl.addEventListener(VoteEvent.ROLE_SET, onRoleSet);
}
 
protected function initDone(event:Event):void
{
    currentState = 'roleChoice';
}

И часть GUI будет очень очень простой:

1
2
3
<s:Group includeIn="init" >
    <s:Label text="Connecting..." visible="{!VoteController.instance.connected}"/>
</s:Group>

Как только приложение подключено к группе, мы увидели, что initDoneобработчик изменяет состояние компонента до 'roleChoice'. Давайте установим пользовательский интерфейс roleChoice: две кнопки для выбора той или иной роли.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
<s:VGroup
    includeIn="roleChoice"
    visible="{!VoteController.instance.role}"
    horizontalCenter="0" verticalCenter="0"
    horizontalAlign="center"
    >
     
    <s:Label text="Choose you role"/>
     
    <s:Button id="btnManager"
              label="Vote manager" width="250" height="40"
              click="btnRole_clickHandler(VoteController.ROLE_MANAGER)"
              />
     
    <s:Button id="btnClient"
              label="Client" width="250" height="40"
              click="btnRole_clickHandler(VoteController.ROLE_CLIENT)"
              />
     
</s:VGroup>

Эти кнопки вызывают btnRole_clickHandlerметод

1
2
3
4
protected function btnRole_clickHandler(role:String):void
{
    VoteController.instance.role = role;
}

Здесь важно то, что мы напрямую модифицируем roleсвойство контроллера. Мы видели, что контроллер отправит roleSetсобытие, и в _initializeHandlerэтом компоненте мы зарегистрировались для этого события. Это означает, что onRoleSetметод будет вызываться, когда пользователь нажимает на одну из этих двух кнопок или когда clientконтроллер вынужден выполнять роль приложения, потому что другая группа voteManagerуведомила группу о своем существовании.

01
02
03
04
05
06
07
08
09
10
11
12
13
protected function onRoleSet(event:VoteEvent):void
{
    switch(VoteController.instance.role)
    {
        case VoteController.ROLE_CLIENT:
            currentState = 'voteClient';
            break;
         
        case VoteController.ROLE_MANAGER:
            currentState = 'voteManager';
            break;
    }
}}

Все инициализация уже выполнена, и нам нужно создать представления для ролей менеджера и клиента. Из нашего компонента это будет так же просто, как включение одного или другого компонента в зависимости от состояния:

1
2
3
4
5
<!--- Vote Client UI -->
<screens:Client_Vote includeIn="voteClient" />
 
<!--- Vote Manager UI -->
<screens:Manager_Vote includeIn="voteManager" />

Теперь мы можем ввести внутренние детали обоих этих представлений.


И менеджер, и клиент должны будут отображать количество раз, когда был выбран ответ, поэтому сначала создайте для этого небольшой компонент.

Мы расширим индикатор выполнения, чтобы использовать его в качестве индикатора. В screensпакете создайте новый компонент MXML, расширяющий mx.controls.ProgressBar. Мы установим его значение по умолчанию для manualmode и поместим метку в center, а затем переопределим setProgressметод для построения текста метки:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version=»1.0″ encoding=»utf-8″?>
<mx:ProgressBar xmlns:fx="http://ns.adobe.com/mxml/2009"
                xmlns:s="library://ns.adobe.com/flex/spark"
                xmlns:mx="library://ns.adobe.com/flex/mx"
                 
                mode="manual" labelPlacement="center" label=""
                includeInLayout="{this.visible}"
                width="100%"
                >
     
    <fx:Script >
        <![CDATA[
            override public function setProgress(value:Number, total:Number):void
            {
                super.setProgress(value, total);
                this.label = value +'/'+total;
            }
        ]]>
    </fx:Script>
    <fx:Declarations />
 
</mx:ProgressBar>

В screensпакете создайте Manager_Voteрасширение компонента spark.components.Group.

Менеджер голос будет слушать какое — то событие из VoteController, так что давайте добавим creationCompleteслушатель и заполнить его с определениями прослушивателя событий, чтобы слушать startVote, stopVoteи answersReceivedсобытие. Мы уже видели в VoteControllerреализации, когда эти события отправляются.

1
2
3
4
5
6
7
protected function _creationCompleteHandler(event:FlexEvent):void
{
    var voteCtrl:VoteController = VoteController.instance;
    voteCtrl.addEventListener(VoteEvent.START_VOTE, onVoteStarted, false,0,true);
    voteCtrl.addEventListener(VoteEvent.ANSWERS_RECEIVED, onVoteUpdated, false,0,true);
    voteCtrl.addEventListener(VoteEvent.STOP_VOTE, onVoteStopped, false,0,true);
}

У менеджера есть несколько состояний

  • editVote : сначала конечный пользователь может редактировать голос,
  • voiceInProgress : когда идет голосование. Менеджер отобразит результаты в режиме реального времени и кнопку, чтобы иметь возможность остановить голосование в любое время.
  • voiceStopped : результаты отображаются, и пользователь имеет возможность начать новое голосование.
1
2
3
4
5
<s:states>
    <s:State name="editVote" enterState="editVote_enterStateHandler(event)"/>
    <s:State name="voteInProgress" stateGroups="_voting"/>
    <s:State name="voteStopped" stateGroups="_voting"/>
</s:states>

Состояние editVote вызывает этот метод, который устанавливает привязываемый логический флаг, который мы будем использовать в форме голосования:

1
2
3
4
5
[Bindable] protected var enabledEdit:Boolean = true;
protected function editVote_enterStateHandler(event:FlexEvent):void
{
    enabledEdit = true;
}

Теперь основная часть нашего интерфейса. По сути, редактирование голосования изменяет несколько текстовых вводов для вопросов и возможных ответов. Для каждого ответа мы имели калибр, что видимость будет установлена значением справедлив только для _voting состояния ( voteInProgress и voteStopped состояния).

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<s:TextInput id="labelVote0" editable="{enabledEdit}"
             text="Enter your question here" styleName="question" width="100%"/>
 
 
<s:TextInput id="labelVote1" editable="{enabledEdit}"
             text="Choice 1" styleName="answer" width="100%"/>
<screens:Jauge id="results1"
               visible="false" visible._voting="true" />
 
<s:TextInput id="labelVote2" editable="{enabledEdit}"
             text="Choice 2" styleName="answer" width="100%"/>
<screens:Jauge id="results2"
               visible="false" visible._voting="true" />
 
<s:TextInput id="labelVote3" editable="{enabledEdit}"
             text="Choice 3" styleName="answer" width="100%"/>
<screens:Jauge id="results3"
               visible="false" visible._voting="true" />
 
<s:TextInput id="labelVote4" editable="{enabledEdit}"
             text="Choice 4" styleName="answer" width="100%"/>
<screens:Jauge id="results4"
               visible="false" visible._voting="true" />
 
<s:TextInput id="labelVote5" editable="{enabledEdit}"
             text="Choice 5" styleName="answer" width="100%"/>
<screens:Jauge id="results5"
               visible="false" visible._voting="true" />
 
 
 
<s:Label text="tip : remove text from the field to hide it from the clients" />
 
 <s:HGroup>
             
    <s:Button id="btnSubmitVote"
              label.editVote="Start Vote"
              label.voteInProgress="vote in progress..."
              label.voteStopped="vote finished"
              width="200"
              enabled="{this.currentState == 'editVote'}"
              click="btnSubmitVote_clickHandler()"
              />
    <s:Button id="btnStopVote"
              label="Stop vote"
              includeIn="voteInProgress"
              click="onVoteStopped()"/>
    <s:Button id="btnNewVote"
              label="New vote"
              includeIn="voteStopped"
              click="currentState = 'editVote';"/>
</s:HGroup>

Вы видели, что во второй половине этого блока кода мы поместили три кнопки. Кнопка отправки голосования, чтобы начать голосование, которое доступно только для состояния editVote . Затем кнопки остановки голосования и новые кнопки голосования, которые включены только в соответствующие состояния.

Чтобы начать голосование, мы должны создать VoteDefinitionобъект, заполнить его текстами и вызвать submitVoteметод контроллера, который мы видели ранее.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
protected function btnSubmitVote_clickHandler():void
{
    enabledEdit = false; // We cannot modify the text while the vote is in progress
     
    // We collect all the informations and ask for a broadcast
    var def:VoteDefinition = new VoteDefinition();
    def.question = labelVote0.text;
    if (labelVote1.text != '') def.answer1 = labelVote1.text;
    if (labelVote2.text != '') def.answer2 = labelVote2.text;
    if (labelVote3.text != '') def.answer3 = labelVote3.text;
    if (labelVote4.text != '') def.answer4 = labelVote4.text;
    if (labelVote5.text != '') def.answer5 = labelVote5.text;
         
    VoteController.instance.submitVote(def);
}

Затем voteControllerбудет отправлено startVoteсобытие, и мы зарегистрировались для этого в нашем creationCompleteобработчике:

1
2
3
4
5
6
protected function onVoteStarted(event:VoteEvent):void
{
    currentState = 'voteInProgress';
    // We make sure the initial feedback is reset
    onVoteUpdated();
}

Сейчас мы находимся в состоянии voiceProgress . Это означает, что мы можем получать answersReceivedи stopVoteсобытия от контроллера, и пользователь может нажать кнопку остановки голосования. Часть с остановкой голосования очень проста: она просит контроллер остановиться и отправить результаты всем:

1
2
3
4
5
protected function onVoteStopped(event:Event=null):void
{
    currentState = 'voteStopped';
    VoteController.instance.parseResultsForBroadcast();
}

Для этого answerReceivedмы должны прочитать currentVoteAnswersбуферы из контроллера (который обновляется каждый раз при получении ответов и перед трансляцией этого события). Таким образом, нам просто нужно вызвать метод setProgress для каждого датчика, и мы получили обратную связь для голосования в реальном времени:

01
02
03
04
05
06
07
08
09
10
11
12
13
protected function onVoteUpdated(event:VoteEvent=null):void
{
    var ctrl:VoteController = VoteController.instance;
     
    // Update the number of answer received
    btnSubmitVote.label = 'vote in progress... ' + ctrl.currentVoteUsers +'/'+ctrl.totalVoteClients;
    var ar:Array = ctrl.currentVoteAnswers;
    results1.setProgress(ar[1], ctrl.totalVoteClients);
    results2.setProgress(ar[2], ctrl.totalVoteClients);
    results3.setProgress(ar[3], ctrl.totalVoteClients);
    results4.setProgress(ar[4], ctrl.totalVoteClients);
    results5.setProgress(ar[5], ctrl.totalVoteClients);
}

Новая кнопка голосования просто устанавливает текущее состояние для editVote . Таким образом, датчики больше не видны, поля могут быть отредактированы, а кнопка отправки голосования включена. Все готово, чтобы отредактировать голосование, отправить его и отобразить результаты.


screens.Client_VoteКомпонент построен на той же модели, что и менеджер; поскольку это расширяется, у Groupэтого будет несколько состояний:

  • waitVote, когда еще не было получено ни одного голоса,
  • voiceInProgress, когда голос получен и пользователь может ввести свои ответы.
  • waitVoteEnd , когда пользователь отправил свои ответы, но не все остальные клиенты сделали это, так что голосование все еще выполняется на других компьютерах (или вкладках браузера)
  • voiceResults , когда все ответили или когда менеджер вручную остановил голосование и отправил результаты всем участникам.
1
2
3
4
5
6
<s:states>
    <s:State name="waitingVote" />
    <s:State name="voteInProgress" stateGroups="_voting"/>
    <s:State name="waitingVoteEnd" stateGroups="_voting"/>
    <s:State name="voteResults" />
</s:states>

Как и менеджер, мы будем прослушивать события контроллера, поэтому мы объявляем прослушиватель этих тезисов нашим creationCompleteавтором:

1
2
3
4
5
6
7
protected function _creationCompleteHandler(event:FlexEvent):void
{
    var voteCtrl:VoteController = VoteController.instance;
    voteCtrl.addEventListener(VoteEvent.START_VOTE, onVoteStarted, false,0,true);
    voteCtrl.addEventListener(VoteEvent.STOP_VOTE, onVoteStopped, false,0,true);
    voteCtrl.addEventListener(VoteEvent.SHOW_RESULTS, onShowResults, false,0,true);
}

Пользовательский интерфейс для waitVote является базовым, так как нам пока ничего не нужно отображать:

1
2
3
4
5
6
7
<!--- Waiting vote info -->
<s:Group
    includeIn="waitingVote"
    horizontalCenter="0" verticalCenter="0"
    >
    <s:Label text="awaiting vote..." />
</s:Group>

Результаты голосования вызовут Vote_Resultкомпонент для отображения индикаторов и текстовых меток.

1
2
3
4
<!--- Vote results -->
<screens:Vote_Results
    id="results"
    includeIn="voteResults" />

Когда startVoteсобытие получено, VoteDefinitionобъект будет сохранен в привязываемом свойстве, и мы перейдем в состояние voiceInProgress . Если это не первый голос, который мы получили, пользовательский интерфейс голосования уже создан, и его флажки могут быть установлены, поэтому мы должны убедиться, что в этом случае все отключено.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
[Bindable] protected var currentVoteDefinition:VoteDefinition;
 
protected function onVoteStarted(event:VoteEvent):void
{
    var voteDefinition:VoteDefinition = event.data as VoteDefinition;
    if (voteDefinition == null) return;
    currentVoteDefinition = voteDefinition;
    // By default, no answer is checked
    if (labelVote1 != null)
    {
        labelVote1.selected = false;
        labelVote2.selected = false;
        labelVote3.selected = false;
        labelVote4.selected = false;
        labelVote5.selected = false;
    }
    currentState = 'voteInProgress';
}

Теперь пользовательский интерфейс формы голосования. Как и у менеджера, у нас есть компоненты для вопроса и каждого ответа. Для клиента каждый ответ является флажком, который активируется только во время голосования (т. Е. Мы находимся в состоянии voiceInProgress ). Метки вопроса и ответов привязаны к voteDefinition'sсвойствам. Наконец-то у нас есть кнопка отправки голосования, чтобы отправить наш выбор.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<!--- Vote -->
    <s:Group
        includeIn="_voting"
        horizontalCenter="0" verticalCenter="0"
        >
        <s:layout>
            <s:VerticalLayout />
        </s:layout>
         
        <s:Label id="labelVote0" text="{currentVoteDefinition.question}"
                 fontSize="20" width="100%" />
         
        <!-- In a real-world development, the results would be made of
        components and not repeated like this. I've chosen to use this very
        —>
         
        <s:CheckBox id="labelVote1"
                    enabled="{this.currentState == 'voteInProgress'}"
                    label="{currentVoteDefinition.answer1}"
                    visible="{currentVoteDefinition.answer1 != null}"
                    styleName="answer" fontSize="14" width="100%"
                    />
         
        <s:CheckBox id="labelVote2"
                    enabled="{this.currentState == 'voteInProgress'}"
                    label="{currentVoteDefinition.answer2}"
                    visible="{currentVoteDefinition.answer2 != null}"
                    styleName="answer" fontSize="14" width="100%"
                    />
         
        <s:CheckBox id="labelVote3"
                    enabled="{this.currentState == 'voteInProgress'}"
                    label="{currentVoteDefinition.answer3}"
                    visible="{currentVoteDefinition.answer3 != null}"
                    styleName="answer" fontSize="14" width="100%"
                    />
         
        <s:CheckBox id="labelVote4"
                    enabled="{this.currentState == 'voteInProgress'}"
                    label="{currentVoteDefinition.answer4}"
                    visible="{currentVoteDefinition.answer4 != null}"
                    styleName="answer" fontSize="14" width="100%"
                    />
         
        <s:CheckBox id="labelVote5"
                    enabled="{this.currentState == 'voteInProgress'}"
                    label="{currentVoteDefinition.answer5}"
                    visible="{currentVoteDefinition.answer5 != null}"
                    styleName="answer" fontSize="14" width="100%"
                    />
         
        <s:Button
            id="btnSubmitVote"
            label="Submit" width="250" height="40"
            label.waitingVoteEnd="No more answers allowed"
            enabled="{this.currentState == 'voteInProgress'}"
            click="btnSubmitVote_clickHandler(event)"
            />
    </s:Group>

Когда пользователь нажимает кнопку «подать голос», мы создадим массив с позициями выбранных ответов и вызовем submitAnswerметод нашего VoteController. Помните, что контроллер затем отправляет эти ответы нашим партнерам в группе. Сетевые операции выполняются только контроллером. Представления и модели не заботятся о том, как это делается, они просто вызывают submitVoteоткрытый API, который мы написали в нашем контроллере. Поскольку пользователь может ответить на голосование только один раз, мы немедленно обновляем наше состояние, чтобы убедиться, что голосование остановлено.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
protected function btnSubmitVote_clickHandler(event:MouseEvent):void
{
    // Collect our answers then send them
    var answers:Array = [];
    if (labelVote1.selected) answers.push(1);
    if (labelVote2.selected) answers.push(2);
    if (labelVote3.selected) answers.push(3);
    if (labelVote4.selected) answers.push(4);
    if (labelVote5.selected) answers.push(5);
     
    // Send our vote
    VoteController.instance.submitAnswers(answers);
     
    // We can no longer edit our vote
    onVoteStopped();
}
 
protected function onVoteStopped(event:VoteEvent=null):void
{
    currentState = 'waitingVoteEnd';
}

Наконец, когда менеджер голосования в группе отправляет результаты голосования, мы получаем showResultsсобытие от контроллера голосования, которое запускает наш onShowResultsобработчик, определенный в creationCompleteобработчике.

1
2
3
4
5
protected function onShowResults(event:VoteEvent):void
{
    currentState = 'voteResults';
    results.showResults();
}

В voteResultsсостоянии Vote_Resultsвиден только компонент.


Это screens.Vote_Resultsпростая перезапись интерфейса менеджера для датчиков и использование текстовых меток, а не TextInputкомпонентов. Когда контроллер получает showResultsсобытие, мы уже видели , что сохраняет все детали его currentVoteDefinitionи currentVoteAnswersсвойств. Компонент результата считывает эти свойства, чтобы установить значение индикаторов и текстовых меток.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<?xml version=»1.0″ encoding=»utf-8″?>
<s:Group xmlns:fx="http://ns.adobe.com/mxml/2009"
         xmlns:s="library://ns.adobe.com/flex/spark"
         xmlns:mx="library://ns.adobe.com/flex/mx"
         xmlns:screens="screens.*"
          
         horizontalCenter="0"
         verticalCenter="0"
         >
    <s:layout>
        <s:VerticalLayout/>
    </s:layout>
     
    <fx:Script>
        <![CDATA[
            import controllers.VoteController;
             
            import models.VoteDefinition;
             
            import mx.events.FlexEvent;
             
            [Bindable] protected var currentVote:VoteDefinition;
             
            public function showResults():void
            {
                var ctrl:VoteController = VoteController.instance;
                currentVote = ctrl.currentVoteDefinition;
                var ar:Array = ctrl.currentVoteAnswers;
                results1.setProgress(ar[1], ctrl.totalVoteClients);
                results2.setProgress(ar[2], ctrl.totalVoteClients);
                results3.setProgress(ar[3], ctrl.totalVoteClients);
                results4.setProgress(ar[4], ctrl.totalVoteClients);
                results5.setProgress(ar[5], ctrl.totalVoteClients);
            }
        ]]>
    </fx:Script>
    <fx:Declarations />
     
    <s:Label id="labelVote6"
                 text="{currentVote.question}" styleName="question" width="100%"/>
     
    <s:Label id="labelVote7"
                 text="{currentVote.answer1}" styleName="answer" width="100%"/>
    <screens:Jauge id="results6" />
     
    <s:Label id="labelVote8"
             text="{currentVote.answer2}" styleName="answer" width="100%"/>
    <screens:Jauge id="results7" />
 
    <s:Label id="labelVote9"
             text="{currentVote.answer3}" styleName="answer" width="100%"/>
    <screens:Jauge id="results8" />
     
    <s:Label id="labelVote10"
             text="{currentVote.answer4}" styleName="answer" width="100%"/>
    <screens:Jauge id="results9" />
     
    <s:Label id="labelVote11"
             text="{currentVote.answer5}" styleName="answer" width="100%"/>
    <screens:Jauge id="results10" />
     
</s:Group>

Вот и все, мы создали все и готовы к тестированию на нескольких компьютерах.


Теперь, когда все части собраны вместе, все готово, и не нужно настраивать соединение между несколькими компьютерами: несколько раз откройте этот демонстрационный фильм на некоторых компьютерах и посмотрите, как они автоматически обнаруживают своих пиров, получают голос от менеджера и как все получают результаты.

открыть окончательный результат в новом окне


Итак, что же RTMFP и новая netGroupподдержка в Flash Player 10.1 означают для нас, разработчиков Flash? Очевидно, что создание аудио / видео чата становится действительно легким. Но наличие нескольких компьютеров, автоматически соединяющихся между собой, без необходимости для конечного пользователя выполнять какие-либо настройки сети или вводить IP-адрес или URL-адрес сервера, действительно отлично подходит для многих других вещей. Подумайте о викторине для анимации киоска или стенда, где у каждого участника есть экран и пользовательский интерфейс, а у аниматора — интерфейс менеджера.

Другим примером может быть удаленный журнал, например инструмент мониторинга, автоматически получающий обновления со всех киосков или со всех компьютеров учеников.

Благодаря простому способу организации сетей с нулевой конфигурацией между флэш-клиентами открывается очень широкий спектр функций, которые мы сможем внедрить в наши приложения, будь то обучение, мониторинг, взаимодействие …

Если вы хотите пойти дальше, есть много отличных ресурсов в Интернете.

  • Первой точкой входа должна быть документация Actionscript, где netGroup.post()метод очень хорошо описан и дает все необходимое для получения первых шагов. Конечно, все остальные классы пакета flash.net, которые мы использовали в этой статье, стоит прочитать, например NetConnection, GroupSpecifierи NetStream.
  • Страница Adobe Labs Cirrus , откуда вы можете получить ключ разработчика.
  • Страница Википедии о многоадресной рассылке: http://en.wikipedia.org/wiki/Multicast
  • Список действительных адресов многоадресной рассылки IANA, включая зарезервированные, позволяет вам выбрать адрес многоадресной рассылки: http://www.iana.org/assignments/multicast-addresses/
  • Сессия Adobe Max 2009 года по RTMFP дала мне много информации, например, необходимость сообщения быть уникальным, чтобы его можно было считать новым при использовании NetGroup.post(). Более того, там действительно много информации о мультикастинге, которую стоит знать.