Advertisement
  1. Code
  2. JavaScript

Building a P2P Network Vote System Using RTMFP

Scroll to top
51 min read

Flash Player 10 brought us the RTMFP protocol, then the 10.1 update added the flash.net.NetGroup granularity. What does this mean for us? Short answer: easy peer to peer communications. Not so short answer: Zero-configuration networking to exchange message between several Flash Player clients..

We will have a look at what exactly RTMFP is, how it can be use in a LAN even without using Adobe's Cirrus Server, then we will start to implement our classes to use the netGroup features. First milestone will be a very basic and simple chat client to see that messages are exchanged, and then we will go on and develop a vote application to build something more close to real-world applications.


Final Result Preview

Let's take a look at the final result we will be working towards. Simply open this flash application several times (within multiple browsers, or multiple browser tabs, or multiple computers on the same LAN). You'll be able to have a single vote manager in the group, and as soon as a manager is set the others will automatically be set to clients. Then the manager can edit a question and its answers, then submit it to the group. The results will update in real time, and then will be displayed by all the computers/browsers of the group. Keep in mind that no network configuration has to be done, no server is involved. I guess you start to see the potential of these features for LAN applications interconnecting several computers.

Open another copy in a new window


Step 1: Real Time Media Flow Protocol?

RTMFP stands for Real Time Media Flow Protocol. This network protocol has been developed by Adobe and made public in Flash Player 10 and AIR 1.5. To paraphrase Adobe's Labs page, is a low latency protocol with peer-to-peer capabilities (amongst others). We won't go deep in details here about how it works, because everything is described in the Cirrus page on the Adobe Labs page.

This protocol enables automatic service discovery, which is quite like Apple's Bonjour protocol. From a Flash developer's point-of-view, it's a complicate stuff under the hood that enables automatic discovery of other clients of our 'service' in the network. Then our Flash clients will send messages (data or media) directly to each others. In Flash Player 10.0 these connections were straightforward: the publishing peer was sending the data to every other peer. Now in 10.1 we have multicast and every peer might automatically act as a relay to dispatch the data to the next peer.

What is multicast? In traditional network communications we often work in unicast mode, like HTTP request, a message between two actors over the network. To send data to several targets, you might first think of unicast broadcasting, which is sending the data for each target. If you have n targets, the data is send n times. Quite expensive. Then we might think about broadcast. To make it very simple, broadcast is sending the data to every node of the network. That means we are also sending it to computers which aren't interested by this message. It might do the job, but it does create extra bandwidth usage to network nodes which are not interested by our data. Then came the multicast, which is the kind of the way to transmit data which is used in RTMFP. Multicast send one single message, then the network nodes will take care of its distribution and replication when necessary. This is a very short summary, and Wikipedia's routing page does a great job for going further into this.

What's important for us is to know that something complicated and optimized is done under the hood, and that we'll need a valid multicast address. So what is a valid multicast address? The range of IP addresses we might use has been set by the IANA. For IPv4, we might choose any one in the 224.0.0.0 to 239.255.255.255 range, but some addresses has been reserved: all the 224.0.0.0 to 224.0.0.255 should not be used. But some addresses outside this range are also reserved. To get the official list you just have to look at IANA's official list to see if the address you're planning to use is reserved. In the next steps we will use 239.252.252.1 as our multicast address. Knowing if your address is a valid multicast one might be helpful for you to debug your project, as a common mistake is to use traditional unicast address like 192.168.0.3. To prepare yourself to the future, valid multicast addresses when you'll be using IPv6 must be in the range ff00::/8.

One final note: Adobe's Cirrus Server enable peer discovery outside of your LAN (i.e. across the Internet). To be able to use Cirrus server, you just have to register to have a Cirrus developer key. When using RTMFP without Cirrus server, we'll have all the peer-to-peer features, but limited to the local network.


Step 2: OK, Now I Want to See Some Code!

So Flash Player now supports RTMFP. Great. But how do we use it? The connection magic happens in the flash.net.NetConnection class. And the group support added by Flash Player 10.1 is done through the flash.net.NetGroup class.

In the next steps we will look at the connection process and which keywords are involved, and then we will merge this together. So let's start by listing what we will need to make a connection:


Store an Instance of NetConnection

Obviously we need an instance of NetConnection, which is included in version 4.1 of the Flex SDK. This NetConnection object exposes a connect() public method. According to this method documentation, connect() waits for a RTMFP URL to create peer-to-peer connection and IP Multicast communication. For a simple LAN peer-to-peer discovery, using "rtmfp:" is enough. For using connect with the Cirrus server, we just have to add the server and devkey strings to this address. This give a really simple connect method in our manager:

1
2
/**

3
 * Using a serverless rtmfp does work for local subnet peer discovery. 

4
 * For internet usage with Cirrus server, something like 

5
 * rtmfp://cirrus.adobe.com/"; will be more useful

6
 */
7
public var _rtmfpServer:String   = "rtmfp:";
8
9
/**

10
 * If you are using a Cirrus server, you should add your developer key here

11
 */
12
public var _cirrusDevKey:String = "";
13
14
protected var netConnection:NetConnection = null;
15
16
public function connect():void
17
{
18
    netConnection = new NetConnection();
19
    netConnection.addEventListener(NetStatusEvent.NET_STATUS, netConnection_netStatusHandler, false,0,true);
20
    netConnection.connect(_rtmfpServer + _cirrusDevKey);
21
}

I am assuming throughout this tutorial that you will import the necessary classes yourself, or that your IDE will do it for you.

Once we have called the netConnection.connect() method, we have to wait and listen to a NetStatusEvent.NET_STATUS event with its info.code property of "NetConnection.Connect.Success". This success info tell us the netConnection is ready, so we will be able to create a netGroup in our netConnection_onConnectSuccess() method.

1
2
/**

3
 * Callback for NetStatusEvent.NET_STATUS events on my netConnection 

4
 * object, set in the connect() method

5
 *

6
 * @param event 

7
 */
8
private function netConnection_netStatusHandler(event:NetStatusEvent):void
9
{
10
    
11
    switch(event.info.code)
12
    {
13
        // Handle connection success

14
        case NetConnectionCodes.CONNECT_SUCCESS:
15
            netConnection_onConnectSuccess();
16
            break;
17
    //...

Creating the NetGroup

The netGroup will be the 'lobby' in which our peers communicate. Therefore it needs to be specified. That's what the flash.net.GroupSpecifier class is intended for. Its constructor waits for a group name which will identify our group. We will compose this name with an applicationName and a groupName suffix. This way it will be easy to create several groups in the same applications without modifying the netGroup manager we are creating. The netGroup class exposes several useful parameters:

  • multicastEnabled, which is used for streams (when used with a netStream.play() or netStream.publish() methods). In our case we won't work with media, but I still keep this property here to because it is very useful for media; it's false by default.
  • objectReplicationEnabled is a Boolean - false by default - which specifies if the replication is enabled. For sending simple message that might be exactly the same but at different times (like a notification for a new occurrence of the same event), I found very useful to explicitly set it to true, otherwise some peers might be a little too smart and would think they already dispatched this messages, and therefore will not replicate it to the next peer, so that not every peer of the group get the new occurrence of the message.
  • postingEnabled, false by default, set if posting is allowed in the group. Obviously we will set it to true, as every peer will send its update via this mechanism.
  • routingEnabled enables direct routing in the netGroup.
  • ipMulticastMemberUpdatesEnabled is a flag that enables IP multicast sockets. This improves P2P performance in a LAN context. As our example will be LAN-based, we will obviously set it to true.
  • serverChannelEnabled help peer discovery by creating a channel on the Cirrus server. As we won't use Cirrus in our example, we'll ignore it by now.

Once we've set all these properties to match our need, we set the multicast address we'll use to interconnect data in our group. This is done by calling the groupSpecifier.addIPMulticastAddress("239.252.252.1:20000") method. Now everything is set-up, we just have to create an instance of NetGroup with this parameter to make the group connection happens:

1
2
/**

3
 * applicationName is the common part of the groupSpecifier name we will be using 

4
 */
5
public var applicationName:String    = "active.tutsplus.examples.netGroup/";
6
7
/**

8
 * groupName is added to applicationName to be able to manage several 

9
 * groups for the same application easily. 

10
 */
11
public var groupName:String      = "demoGroup";
12
13
/** 

14
 * Callback for NetConnectionCodes.CONNECT_SUCCESS net status for my 

15
 * netConnection object.

16
 */
17
private function netConnection_onConnectSuccess():void
18
{
19
    var groupSpecifier:GroupSpecifier;
20
    
21
    groupSpecifier = new GroupSpecifier(applicationName + groupName);
22
    groupSpecifier.multicastEnabled     = true;
23
    groupSpecifier.objectReplicationEnabled = true;
24
    groupSpecifier.postingEnabled       = true;
25
    groupSpecifier.routingEnabled       = true;
26
    groupSpecifier.ipMulticastMemberUpdatesEnabled = true; 
27
   
28
    groupSpecifier.addIPMulticastAddress("239.252.252.1:20000"); 
29
    
30
    netGroup = new NetGroup(netConnection, groupSpecifier.groupspecWithAuthorizations());
31
    netGroup.addEventListener(NetStatusEvent.NET_STATUS, netGroup_statusHandler);
32
}

Then the netGroup dispatch a NetStatusEvent.NET_STATUS event with its code property of "NetGroup.Connect.Success" when it's ready to be used. By now we can start using the netGroup.post(object) method to send messages to our peers.

We've talked a little bit about the NetStatusEvent which give us the connection success info. But it's also useful for retrieving everything happening to our netConnection or netGroup. Therefore we will add some values in a switch/case tree to get the relevant values of the NetStatusEvent for our netConnection:

1
2
switch(event.info.code)
3
{
4
    // Handle connection success

5
    case NetConnectionCodes.CONNECT_SUCCESS:
6
        netConnection_onConnectSuccess();
7
        break;
8
    
9
    // Handle every case of disconnection

10
    case NetConnectionCodes.CONNECT_CLOSED:
11
    case NetConnectionCodes.CONNECT_FAILED:
12
    case NetConnectionCodes.CONNECT_REJECTED:
13
    case NetConnectionCodes.CONNECT_APPSHUTDOWN:
14
    case NetConnectionCodes.CONNECT_INVALIDAPP:
15
        netConnection_onDisconnect();
16
        break;
17
    
18
    case "NetGroup.Connect.Success": 
19
        netGroup_onConnectSuccess();
20
        break;
21
    
22
    default:
23
        break;
24
    
25
}

Our netGroup's netStatusEvent listener will have similar tests to listen to group-specific actions: messages posted, peer (or neighbor in the netGroup API) joined or left, so we will have this simple routing method (some properties there will be implemented later, like or custom NetGroupEvent class, our VoNetGroupMessage, but you will have a look at the codes received, and you can note that we already have a direct count of currently connected peers by reading netGroup.neighborCount.

1
2
private function netGroup_statusHandler(event:NetStatusEvent):void
3
{
4
    switch(event.info.code)
5
    {
6
        
7
        case "NetGroup.Connect.Rejected":
8
        case "NetGroup.Connect.Failed": 
9
            disconnect();
10
            break;
11
        
12
        case "NetGroup.SendTo.Notify": 
13
            break;
14
        case "NetGroup.Posting.Notify":
15
            messageReceived(new VoNetGroupMessage(event.info.message));
16
            break;
17
        
18
        case "NetGroup.Neighbor.Connect":
19
            dispatchEvent(new NetGroupEvent(NetGroupEvent.NEIGHBOR_JOINED));
20
            // no break here to continue on the same segment than the disconnect part

21
        case "NetGroup.Neighbor.Disconnect":
22
            neighborCount = netGroup.neighborCount; 
23
            break;
24
        
25
        case "NetGroup.LocalCoverage.Notify":
26
        case "NetGroup.MulticastStream.PublishNotify": 
27
        case "NetGroup.MulticastStream.UnpublishNotify":
28
        case "NetGroup.Replication.Fetch.SendNotify":
29
        case "NetGroup.Replication.Fetch.Failed":
30
        case "NetGroup.Replication.Fetch.Result":
31
        case "NetGroup.Replication.Request":
32
        default:
33
            break;
34
    }
35
}

We have highlighted the main keywords and methods to use a NetGroup. In the next step we'll formalize this quite a bit, writing our own NetGroupManager class to simplify this API for a basic message-exchange usage.


Step 3: Building our own NetGroupManager Utilities

Because we don't want to worry about the inner working once it has been done, we will build a utility class to handle of the network communication. This manager will use a NetGroupEvent to dispatch its update, and a NetGroupMessage object to unify the structure and get some helpers. This will give us a generic manager, written once, ready to be used in any project.

First, as a debug tool, we'll create a little helper. In Flash Builder, create a new Flex Library Project, name it LibHelpers, create a new 'helpers' package and paste this DemoHelper class in this package:

1
2
package helpers
3
{
4
    public class DemoHelper
5
    {
6
        
7
        /** 

8
        * Centralisation of all trace methods, and prefix this with a little 

9
        * timestamp

10
        */
11
        static public function log(msg:String):void
12
        {
13
            trace(timestamp +'\t' + msg);
14
        }
15
        
16
        
17
        /**

18
         * Useful function for logging, formatting the current time in a easy

19
         * to read line.

20
         * @return a ':' delimited time string like 12:34:56:789  

21
         * 

22
         */
23
        static public function get timestamp():String
24
        {
25
            var now:Date = new Date();
26
            var arTimestamp:Array = [now.getHours(), now.getMinutes(), now.getSeconds(), now.getMilliseconds()];
27
            return arTimestamp.join(':');
28
        }
29
        
30
    }
31
}

It give us something more useful than a simple trace(). Calling DemoHelper.put('some string'); will output the message, with a timestamp as a prefix. It's also very handy to group all the traces functions in this central place to be able to easily turn them off.

Important warning : As flash.net.netGroup has been added in flash Player 10.1, make sure you have updated Flash Builder to work with the Flex 4.1 version.

Now you can create a new Flex Library Project named LibNetGroup. Create a 'netgroup' package in this project, in which we will add our files.


Step 4: A custom NetGroupEvent to Dispatch Application-Level Updates

First, let's start by our custom event. Our manager will be used to let the project know when the group is ready to be used (connectedToGroup event), when we're disconnected, when a new neighbor has joined and when a message has been received. Obviously many others events might be added, but let's keep it simple for now, and you'll be able to extend it later. Rather than extend Flash's DynamicObject, I choose to simply have two fields for storing optional data when the event carries the details of a received message.

So it gave us a simple event called netgroup.NetGroupEvent:

1
2
package netgroup
3
{
4
    import flash.events.Event;
5
    
6
    public class NetGroupEvent extends Event
7
    {
8
        
9
        static public const POSTING:String                 = 'posting';
10
        static public const CONNECTED_TO_GROUP:String      = 'connectedToGroup';
11
        static public const DISCONNECTED_FROM_GROUP:String = 'disconnectedFromGroup';
12
        static public const NEIGHBOR_JOINED:String         = 'neighborJoined';
13
        
14
        public var message:Object;
15
        public var subject:String;
16
        
17
        
18
        public function NetGroupEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)
19
        {
20
            super(type, bubbles, cancelable);
21
        }
22
    }
23
}

Step 5: Our own Message Format to Bypass Pitfalls

Next easy step, we'll set-up a message format. A message will have a subject, a sender and some data. The subject is useful for routing received message in a real application, the data will be some kind of generic object. Remember the netGroup.post() method we briefly mentioned in step 2. It takes an object as argument. So we'll have some serialization methods to create a VoNetMessage from an object, and create an object from a VoNetMessage instance. This serialization method will be implemented by a simple getter method, and we'll add here some salt.

Do you remember peer to peer automatically handle the routing of the messages. If a peer gets a message it has already forwarded, it will consider this message as old and will not send it again. But what will happen when our application will send small simple notification like a single string 'endOfVote' to notify clients the vote has ended. If we start a new vote, then stop it, this very simple message will be send one more time. Exactly the same contents. But we need to make sure the peers are aware this is yet a new message. The easiest way to make sure of this is to add some salt in our message, when serializing it before sending it. In this example I use a simple random number, but you might want to add a timestamp, or start creating some unique messages IDs. You're the boss. The most important thing is to be aware of this pitfall. Then you're up to handle it the way you prefer.

Here comes our netgroup.VoNetGroupMessage in its simplest form:

1
2
package netgroup
3
{
4
    /**

5
     * VoNetGroupMessage is a little helper that force the subject/data/sender

6
     * fields before sending a generic object over the air. 

7
     * 

8
     */
9
    public class VoNetGroupMessage extends Object
10
    {
11
         
12
        public var subject:String;
13
        public var data:Object;
14
        public var sender:String; // the sender's nearID

15
        
16
        public function get object():Object
17
        {
18
            var o:Object = {};
19
            o.subject   = subject;
20
            o.data      = data;
21
            o.sender    = sender;
22
            o.salt = Math.random()*99999; // We add some salt to make sure we do not send the same message 2 times

23
          
24
            return o;
25
        }
26
        
27
        public function VoNetGroupMessage(message:Object=null)
28
        {
29
            if (message == null) message = {};
30
            subject = message.subject;
31
            data    = message.data;
32
            sender  = message.sender;
33
        }
34
    }
35
}

To send a message to the group, it will be as simple as instantiating a VoNetGroupMessage, populating its subject and data properties, then passing its object getter in the netGroup.post() method. When a message will be received from the group, instantiating it with the object received as the constructor's argument will automatically populate the properties, ready to be read, like this :

1
2
var message:VoNetGroupMessage = new VoNetGroupMessage(objectReceived)

Step 6: Wiring it all Together: the NetGroupManager Class

The manager will be the 'black box' that our application will interact with to communicate with the peers. We don't want that our business logic have to take care of how the network communication is done, we just want to send and receive messages. So let's start by defining the public API we want for our manager:

  • It will dispatch events about what happens, therefore it will extends EventDispatcher
  • It will be easy to set up to use our own rtmpServer value, developerKey if needed, applicationName and groupName. No need to edit the class to use it for another application.
  • A connect() method to connect to our peers.
  • A disconnect() method
  • A sendMessage() method.

Let's look at this in details


NetGroupManager will Dispatch Meaningful Events

It will extend EventDispatcher, and will dispatch the events we have prepared in NetGroupEvent. We had the event meta-tags to make their usage easier:

1
2
/** 

3
* Event sent every time a message is received. 

4
*/
5
[Event(name="posting", type="netgroup.NetGroupEvent")]
6
7
/** 

8
 * Event sent once the netConnection and netGroup connections have been 

9
 * successful, and the group is ready to be used. 

10
 */
11
[Event(name="connectedToGroup", type="netgroup.NetGroupEvent")]
12
13
/** 

14
 * Event sent the netGroup connection is closed. 

15
 */
16
[Event(name="disconnectedFromGroup", type="netgroup.NetGroupEvent")]
17
18
/** 

19
 * Event sent the netGroup connection is closed. 

20
 */
21
[Event(name="neighborJoined", type="netgroup.NetGroupEvent")]

It will also have some public bindable properties to have a look at the group state. Be careful, I have let them as public properties to make the code smaller and easily bindable, but these shouldn't be kept externally editable in a real world project.

1
2
/* ------------------------------------------------------------------ */
3
/* ---  Useful public properties for managing the group          --- */
4
/* ------------------------------------------------------------------ */
5
static public const STATE_DISCONNECTED:String   = 'disconnected';
6
static public const STATE_CONNECTING:String     = 'connecting';
7
static public const STATE_CONNECTED:String      = 'connected';
8
   
9
private var _neighborCount:int;
10
[Bindable(event='propertyChange')] 
11
/** neighborCount is a public exposure of the netGroup.neighborCount property */
12
public function get neighborCount():int { return _neighborCount; }
13
public function set neighborCount(value:int):void
14
{
15
    if (value == neighborCount) return;
16
    _neighborCount = value;
17
    dispatchEvent(new Event('propertyChange'));
18
}
19
20
[Bindable] public var connectionState:String    = STATE_DISCONNECTED;
21
22
[Bindable] public var connected:Boolean         = false; 
23
[Bindable] public var joinedGroup:Boolean       = false;

NetGroupManager will be Easy to Configure

Let's give it some public properties as arguments: rtmfpServer, cirrusDevKey, applicationName and groupName. This should sound familiar if you have read the previous steps.

1
2
/* ------------------------------------------------------------------ */
3
/* ---  Connection parameters                                     --- */
4
/* ------------------------------------------------------------------ */
5
6
/**

7
 * Using a serverless rtmfp does work for local subnet peer discovery. 

8
 * For internet usage with Cirrus server, something like 

9
 * rtmfp://cirrus.adobe.com/"; will be more useful

10
 */
11
public var _rtmfpServer:String   = "rtmfp:";
12
13
/**

14
 * If you are using a Cirrus server, you should add your developer key here

15
 */
16
public var _cirrusDevKey:String = "";
17
18
/**

19
 * applicationName is the common part of the groupSpecifier name we will be using 

20
 */
21
public var applicationName:String    = "active.tutsplus.examples.netGroup/";
22
23
/**

24
 * groupName is added to applicationName to be able to manage several 

25
 * groups for the same application easily. 

26
 */
27
public var groupName:String      = "demoGroup";

NetGroupManager Will be our API to Send Messages to our Peers...

It will have a few method : connect(), disconnect() and sendMessage(). The connect method is almost already known, as all the important stuff in there have been seen in the previous step. The disconnect method is simply a wrapper of the netConnection.close() method.

The sendMessage method will use everything we have explained in the VoNetGroupMessage structure, therefore there's no surprise in its implementation: it instantiates a VoNetGroupMessage object, populate its fields, then serialize it using its object getter and use it for the netGroup.post() method.

1
2
public function sendMessage(subject:String, messageContents:Object):void
3
{
4
    if (!connected) 
5
        throw "Error trying to send a message without being connected";
6
    
7
    // Create the message to send, setting my ID on it and filling it 

8
    // with the contents

9
    var message:VoNetGroupMessage = new VoNetGroupMessage();
10
    message.subject = subject;
11
    message.data    = messageContents;
12
    message.sender  = netConnection.nearID;
13
    
14
    netGroup.post(message.object);
15
}

... and NetGroupManager will be our Central Place to Read Message Received From Peers.

We already had a quick look of the NetStatusEvent values in the previous steps; the only missing part is the messageReceived handler, called when a NetGroup.Posting.Notify status has been received. In the NetStatusEvent listener, we instantiate the message using the object we receive, then pass it to the messageReceived method:

1
2
private function netGroup_statusHandler(event:NetStatusEvent):void
3
{
4
5
    switch(event.info.code)
6
    {
7
        // {... }

8
        case "NetGroup.Posting.Notify": //

9
            messageReceived(new VoNetGroupMessage(event.info.message));
10
            break;
11
        // other cases...

This messageReceived handler will dispatch the message. The business layer which might have subscribed to this event will then handle the inner details of the message, the job of the netGroupManager, which is handling peer connections, sending and receiving data, is done.

1
2
/**

3
 * NetGroup callback for a netGroup.post(..) command.

4
 * 

5
 * @param message Object, the structure of this object being defined in the sendMessage handler

6
 * 

7
 * @see #sendMessage()

8
 */
9
private function messageReceived(message:VoNetGroupMessage):void
10
{
11
    // Broadcast this message to any suscribers

12
    var event:NetGroupEvent = new NetGroupEvent(NetGroupEvent.POSTING);
13
    event.message = message.data;
14
    event.subject = message.subject;
15
    dispatchEvent(event);
16
}

As a milestone, here comes the full code of our netgroup.NetGroupManager class:

1
2
package netgroup
3
{
4
	import flash.events.Event;
5
	import flash.events.EventDispatcher;
6
	import flash.events.IEventDispatcher;
7
	import flash.events.NetStatusEvent;
8
	import flash.net.GroupSpecifier;
9
	import flash.net.NetConnection;
10
	import flash.net.NetGroup;
11
	import flash.net.NetStream;
12
	import flash.sampler.getGetterInvocationCount;
13
	import flash.utils.getTimer;
14
	
15
	import helpers.DemoHelper;
16
	
17
	import mx.events.DynamicEvent;
18
	
19
	import org.osmf.net.NetConnectionCodes;
20
	import org.osmf.net.NetStreamCodes;
21
	
22
    /** 

23
    * Event sent every time a message is received. 

24
    */
25
    [Event(name="posting", type="netgroup.NetGroupEvent")]
26
    
27
    /** 

28
     * Event sent once the netConnection and netGroup connections have been 

29
     * successful, and the group is ready to be used. 

30
     */
31
    [Event(name="connectedToGroup", type="netgroup.NetGroupEvent")]
32
    
33
    /** 

34
     * Event sent the netGroup connection is closed. 

35
     */
36
    [Event(name="disconnectedFromGroup", type="netgroup.NetGroupEvent")]
37
    
38
    /** 

39
     * Event sent the netGroup connection is closed. 

40
     */
41
    [Event(name="neighborJoined", type="netgroup.NetGroupEvent")]
42
    
43
    
44
     
45
    
46
    /**

47
     * 

48
     * 

49
     */
50
	public class NetGroupManager extends EventDispatcher
51
	{
52
        static public const STATE_DISCONNECTED:String   = 'disconnected';
53
        static public const STATE_CONNECTING:String     = 'connecting';
54
        static public const STATE_CONNECTED:String      = 'connected';
55
        
56
        
57
        /* ------------------------------------------------------------------ */
58
        /* ---  Connection parameters                                     --- */
59
        /* ------------------------------------------------------------------ */
60
        
61
        /**

62
         * Using a serverless rtmfp does work for local subnet peer discovery. 

63
         * For internet usage with Cirrus server, something like 

64
         * rtmfp://cirrus.adobe.com/"; will be more usefull

65
         */
66
        public var _rtmfpServer:String   = "rtmfp:";
67
        
68
        /**

69
         * If you are using a Cirrus server, you should add your developer key here

70
         */
71
        public var _cirrusDevKey:String = "";
72
        
73
        /**

74
         * applicationName is the common part of the groupSpecifier name we will be using 

75
         */
76
        public var applicationName:String    = "active.tutsplus.examples.netGroup/";
77
        
78
        /**

79
         * groupName is added to applicationName to be able to manage several 

80
         * groups for the same application easily. 

81
         */
82
        public var groupName:String      = "demoGroup";
83
        
84
        /* ------------------------------------------------------------------ */
85
        /* ---  Usefull public properties for managing the group          --- */
86
        /* ------------------------------------------------------------------ */
87
        private var _neighborCount:int;
88
        [Bindable(event='propertyChange')] 
89
        /** neighborCount is a public exposure of the netGroup.neighborCount property */
90
        public function get neighborCount():int { return _neighborCount; }
91
        public function set neighborCount(value:int):void
92
        {
93
            if (value == neighborCount) return;
94
            _neighborCount = value;
95
            dispatchEvent(new Event('propertyChange'));
96
        }
97
        
98
        [Bindable] public var connectionState:String    = STATE_DISCONNECTED;
99
        
100
        [Bindable] public var connected:Boolean         = false; 
101
        [Bindable] public var joinedGroup:Boolean       = false;
102
        
103
		protected var netConnection:NetConnection       = null;
104
		protected var netStream:NetStream               = null;
105
		protected var netGroup:NetGroup                 = null;
106
		
107
        
108
         
109
       
110
		public function NetGroupManager(target:IEventDispatcher=null)
111
		{
112
			super(target);
113
		}
114
		
115
	
116
		
117
		public function connect():void
118
		{
119
			_log("Connecting to '" + _rtmfpServer);
120
            connectionState = STATE_CONNECTING;
121
            
122
			netConnection = new NetConnection();
123
			netConnection.addEventListener(NetStatusEvent.NET_STATUS, netConnection_netStatusHandler, false,0,true);
124
			netConnection.connect(_rtmfpServer + _cirrusDevKey);
125
		}
126
		
127
        
128
        /** 

129
         * disconnect will be automatically called when a netGroup connection or

130
         * a netStream connection will be rejected or have failed.

131
         * 

132
         * @see #netConnection_onDisconnect() 

133
         */
134
        public function disconnect():void
135
        {
136
            if(netConnection) netConnection.close();
137
        }
138
        
139
        
140
        public function sendMessage(subject:String, messageContents:Object):void
141
        {
142
            if (!connected) 
143
                throw "Error trying to send a message without being connected";
144
            
145
            // Create the message to send, setting my ID on it and filling it 

146
            // with the contents

147
            var message:VoNetGroupMessage = new VoNetGroupMessage();
148
            message.subject = subject;
149
            message.data    = messageContents;
150
            message.sender  = netConnection.nearID;
151
            
152
            netGroup.post(message.object);
153
        }
154
        
155
        
156
        
157
        
158
        
159
		/** 

160
		 * Callback for NetConnectionCodes.CONNECT_SUCCESS net status for my 

161
         * netConnection object.

162
         */
163
		private function netConnection_onConnectSuccess():void
164
		{
165
			var groupSpecifier:GroupSpecifier;
166
			
167
			connected = true;
168
            connectionState = STATE_CONNECTED;
169
			_log("Connected");
170
			
171
			groupSpecifier = new GroupSpecifier(applicationName + groupName);
172
			groupSpecifier.multicastEnabled     = true;
173
            groupSpecifier.objectReplicationEnabled = true;
174
			groupSpecifier.postingEnabled       = true;
175
            groupSpecifier.routingEnabled       = true;
176
			
177
            // Server channel might help peer finding each other when using a Cirrus server

178
            //groupSpecifier.serverChannelEnabled = true;

179
            
180
            // ipMulticastMemberUpdatesEnabled is new in Flash player 10.1

181
            groupSpecifier.ipMulticastMemberUpdatesEnabled = true; 
182
            
183
            /* 

184
            The multicast IP you enter has to be in the 224.0.0.0 to 

185
            239.255.255.255 range, or ff00::/8 if you're using IPv6

186
            Some IP are reserved and should not be used, see 

187
            http://www.iana.org/assignments/multicast-addresses/ for this list.

188
            */
189
            groupSpecifier.addIPMulticastAddress("239.252.252.1:20000"); 
190
            
191
            netGroup = new NetGroup(netConnection, groupSpecifier.groupspecWithAuthorizations());
192
            netGroup.addEventListener(NetStatusEvent.NET_STATUS, netGroup_statusHandler);
193
            
194
            /* netStream will not be used in this example, but I kept the code 

195
            here to give you an entry point to go further. */ 
196
            // netStream = new NetStream(netConnection, groupSpecifier.groupspecWithAuthorizations());

197
			// netStream.addEventListener(NetStatusEvent.NET_STATUS, NetConnectionStatusHandler);

198
            
199
            /* 

200
            You might enable the following line to have a look at what's your

201
            current groupSpecifier looks like:

202
              sample ouput :

203
              10:58:45:529	Join 'G:01010103010c2c0e6163746976652e74757473706c75732e6578656d706c65732e6e657447726f75702f64656d6f47726f7570011b00070aee0000014e20'

204
            */
205
			// _log("Join '" + groupSpecifier.groupspecWithAuthorizations() + "'");

206
		}
207
		
208
        
209
        /** 

210
         * Callback for NetConnectionCodes.CONNECT_SUCCESS net status for my 

211
         * netConnection object.

212
         */
213
		private function netStream_onConnectSuccess():void
214
		{
215
			netStream.client = this;
216
            // We're not using media for now, but it's the right place to use 

217
            // netStream.attachAudio(..) and netStream.attachCamera if you want 

218
            // to create a video chat or broadcast some media

219
            netStream.publish("mystream");
220
		}
221
		
222
        
223
		private function netGroup_onConnectSuccess():void
224
		{
225
			joinedGroup = true;
226
            
227
            dispatchEvent(new NetGroupEvent(NetGroupEvent.CONNECTED_TO_GROUP));
228
		}
229
        
230
        
231
		private function netConnection_onDisconnect():void
232
		{
233
			_log("Disconnected\n");
234
            connectionState = STATE_DISCONNECTED;
235
			netConnection   = null;
236
			netStream       = null;
237
			netGroup        = null;
238
			connected       = false;
239
			joinedGroup     = false;
240
            
241
            dispatchEvent(new NetGroupEvent(NetGroupEvent.DISCONNECTED_FROM_GROUP));
242
		}
243
		
244
        
245
        /**

246
         * Callback fot NetStatusEvent.NET_STATUS events on my netConnection 

247
         * object, set in the connect() method

248
         *

249
         * @param event 

250
         */
251
        private function netConnection_netStatusHandler(event:NetStatusEvent):void
252
        {
253
            _log("netConnection status:" + event.info.code);
254
            
255
            switch(event.info.code)
256
            {
257
                // Handle connection success

258
                case NetConnectionCodes.CONNECT_SUCCESS:
259
                    netConnection_onConnectSuccess();
260
                    break;
261
                
262
                // Handle very case of disconnection

263
                case NetConnectionCodes.CONNECT_CLOSED:
264
                case NetConnectionCodes.CONNECT_FAILED:
265
                case NetConnectionCodes.CONNECT_REJECTED:
266
                case NetConnectionCodes.CONNECT_APPSHUTDOWN:
267
                case NetConnectionCodes.CONNECT_INVALIDAPP:
268
                    netConnection_onDisconnect();
269
                    break;
270
                
271
                case "NetGroup.Connect.Success": 
272
                    netGroup_onConnectSuccess();
273
                    break;
274
                
275
                default:
276
                    break;
277
                
278
            }
279
        }
280
        
281
   
282
        
283
        private function netGroup_statusHandler(event:NetStatusEvent):void
284
        {
285
            _log("netGroup status:" + event.info.code);
286
            switch(event.info.code)
287
            {
288
                
289
                case "NetGroup.Connect.Rejected":
290
                case "NetGroup.Connect.Failed": 
291
                    disconnect();
292
                    break;
293
                
294
                case "NetGroup.SendTo.Notify": 
295
                    messageReceived(new VoNetGroupMessage(event.info.message));
296
                    break;
297
                case "NetGroup.Posting.Notify": //

298
                    messageReceived(new VoNetGroupMessage(event.info.message));
299
                    break;
300
                
301
                case "NetGroup.Neighbor.Connect":
302
                    dispatchEvent(new NetGroupEvent(NetGroupEvent.NEIGHBOR_JOINED));
303
                    // no break here to continue on the same segment than the disconnect part

304
                case "NetGroup.Neighbor.Disconnect":
305
                    neighborCount = netGroup.neighborCount; 
306
                    break;
307
                
308
                    
309
                case "NetGroup.LocalCoverage.Notify": //

310
                    
311
                case "NetGroup.MulticastStream.PublishNotify": // event.info.name

312
                case "NetGroup.MulticastStream.UnpublishNotify": // event.info.name

313
                case "NetGroup.Replication.Fetch.SendNotify": // event.info.index

314
                case "NetGroup.Replication.Fetch.Failed": // event.info.index

315
                case "NetGroup.Replication.Fetch.Result": // event.info.index, event.info.object

316
                case "NetGroup.Replication.Request": // event.info.index, event.info.requestID

317
                
318
                default:
319
                    break;
320
               
321
            }
322
        }
323
        
324
        
325
        
326
        private function netStream_statusHandler(event:NetStatusEvent):void
327
        {
328
            switch(event.info.code)
329
            { 
330
                /*

331
                The netStream usefull event codes are kept here, even if in 

332
                this demo you're will not be using them. This gives you a brief 

333
                overview of what you receive.

334
                */
335
                case "NetStream.Connect.Success": 
336
                    netStream_onConnectSuccess();
337
                    break;
338
                
339
                case "NetStream.Connect.Rejected":
340
                case "NetStream.Connect.Failed":
341
                    disconnect();
342
                    break;
343
                
344
                case "NetStream.MulticastStream.Reset":
345
                case "NetStream.Buffer.Full":
346
                    break;
347
                
348
            }
349
        }
350
        
351
		
352
        
353
        /**

354
         * NetGroup callback for a netGroup.post(..) command.

355
         * 

356
         * @param message Object, the structure of this object being defined in the sendMessage handler

357
         * 

358
         * @see #sendMessage()

359
         */
360
		private function messageReceived(message:VoNetGroupMessage):void
361
		{
362
			_log("[" + message.sender + "] " + message.data);
363
            
364
            // Broadcast this message to any suscribers

365
            var event:NetGroupEvent = new NetGroupEvent(NetGroupEvent.POSTING);
366
            event.message = message.data;
367
            event.subject = message.subject;
368
            dispatchEvent(event);
369
		}
370
		 
371
		/**

372
        * Simple 'trace' wrapper

373
        */
374
        private function _log(msg:Object):void
375
        {
376
            DemoHelper.log(msg as String);
377
        }
378
		
379
	}
380
}

Step 7: Quick Test Creating a Basic Chat Using the NetGroupManager

Let's create a new Flash project in Flash Builder, which I named Demo_NetGroupChat. As this project will use the manager we just wrote, we have to add a reference to the manager project. This is simply done by opening the project properties and in the Flex Build Path clicking the "Add a project..." button and selecting the LibNetGroup project.

We will bind an instance of our NetGroupManager as lanGroupMgr variable. We will add a connect button which will call the connect method of our manager, some labels binded to the public properties of our managers connectionState, joinedGroup and neighborCount.

1
2
<s:HGroup>
3
	<s:Button id="btnConnect"
4
			  label="connect" 
5
			  click="btnConnect_clickHandler(event)"
6
			  visible="{!lanGroupMgr.connected}"
7
			  />
8
	<s:Label text="{lanGroupMgr.connectionState}"/>
9
	
10
	<s:Label text="group joined" visible="{lanGroupMgr.joinedGroup}"/>
11
</s:HGroup>
12
13
<s:HGroup visible="{lanGroupMgr.joinedGroup}">
14
	<s:Label text="Neighbor count :" />
15
	<s:Label text="{lanGroupMgr.neighborCount}"/>
16
</s:HGroup>

To exchange messages we just need a TextInput component, a submit button, and a textArea console to show the message received:

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

That's almost finished.

We have to:

  • listen to new posts received by adding a listener in our creationComplete handler,
  • write btnConnect_clickHandler handler to call the connect method of our manager.
  • Sending a chat message is then simply calling the sendMessage method of our manager with the textInput content wrapped in an object.
  • To read and display the message received, in our messagePosted listener we just have to read the content of the object in its message property, and append it to the console field.

That's all. Here comes the script block of the mxml document:

1
2
import helpers.DemoHelper;
3
4
import mx.events.FlexEvent;
5
6
import netgroup.NetGroupEvent;
7
import netgroup.NetGroupManager;
8
9
[Bindable] protected var lanGroupMgr:NetGroupManager;
10
11
protected function application1_creationCompleteHandler(event:FlexEvent):void
12
{
13
	lanGroupMgr = new NetGroupManager();
14
	lanGroupMgr.addEventListener(NetGroupEvent.POSTING, onMessagePosted);
15
}
16
17
protected function onMessagePosted(event:NetGroupEvent):void
18
{
19
	var message:Object = event.message;
20
	console.appendText(DemoHelper.timestamp+' '+ message.contents +'\n');
21
}
22
23
24
protected function btnConnect_clickHandler(event:MouseEvent):void
25
{
26
	lanGroupMgr.connect();
27
}
28
29
30
protected function btnSend_clickHandler(event:MouseEvent):void
31
{
32
	// Creation of a message to send

33
	var msg:Object = {};
34
	msg.contents = inputMsg.text;
35
	lanGroupMgr.sendMessage('chat', msg);
36
	callLater(resetInput);
37
		
38
}
39
40
private function resetInput():void
41
{
42
	inputMsg.text = '';
43
}

Here comes the source and demo of this test application. To have something to test, you should open this URL several times, ideally on several computers on the same network. If you don't have several computers right now, you can open multiple tabs in multiple browsers (although the real interest of the feature won't be as obvious as on on a network, it's handy for quick testing).

Open another copy in a new window


Step 8: Building a Sample Peer Application: PeerVote

We will now build an application a little more complex than a chat, to see how to handle different kind of messages, and how to modify the application state depending on the events received from the other peers on the network. In the same NetGroup, we will allow users to have the choice to be:

  • voteManager. There will be only one manager in a group at once. The voteManager will edit a question and the available answers, then send a vote request to all peers with theses data. The voteManagers will receive and display the vote results in real time, each time a client answers. Then the voteManager will dispatch the vote results to the other peers of the group as soon as all the clients connected have answered, or if the manager manually decided to stop the vote.
  • voteClient. A client will display a 'please wait' screen until it receive a question, then will show the possible answers to the end-user. When the user selects his answers, the client will send the selected answers to the vote manager. At last when the vote manager dispatch the vote results, the client will display how many time each answer has been selected by users of the group.

To make things a little smarter, we will build only one application. When a user 'A' joins the group, he might choose if he want to be a manager or a client. If a vote manager is already set by the user 'B' in the group, it warns 'A' about his existence, therefore the application 'A' will automatically choose to be a vote client and will start to wait for a question.


Step 9: Listing the Basic Bricks of our Application

In the next steps we will list every item we need to build this vote application: obviously the NetGroupManager we build in the previous steps, but also a model, an event, some views to display an UI. And finally a controller to link the model to the views.

Create a new project in Flex builder and give it the name "Demo_NetGroup_Votes". Add a reference to the LibNetGroup project, like we already did for the quick chat test. This way we will be able to use our NetGroupManager classes.

In the src folder, let's create some folders that we will fill in the forthcoming steps:

  • 'models', this package will store our model
  • 'events', this package will contain our custom event
  • 'controllers', in which we will put the controller of our vote application
  • 'screens' which will be fill with the MXML components we will use as views for our application

Step 10: A Simple Model: VoteDefinition

The vote application will send a "vote definition", containing information about the vote, to the clients. These clients will have to read this question and display it. In order to make things clean, we will create a very simple model so that everything as a name and a type. Although our question might be simplified to a string list for this test, using a model gives an idea of how to build it in a way that makes it easily expandable, easy to read and debug.

Start by create a new VoteDefinition.as class in the models package. We will add some public properties to store the question and up to five answers, as Strings. The constructor of this class will read a generic Object and fill its properties with theses values.

There's nothing more, so the code is very simple and straightforward:

1
package models
2
{
3
    
4
    /**

5
     * VoteDefinition is the data container that contains all the needed 

6
     * informations to vote

7
     */
8
    public class VoteDefinition 
9
    {
10
        
11
        [Bindable] public var question:String;
12
        [Bindable] public var answer1:String;
13
        [Bindable] public var answer2:String;
14
        [Bindable] public var answer3:String;
15
        [Bindable] public var answer4:String;
16
        [Bindable] public var answer5:String;
17
        
18
        // Some extra parameters to give ideas of how to expand the application

19
        public var timeLimit:int = 0; // If 0, then no time limit

20
        public var multipleAnswers:Boolean = true;
21
        
22
        public function VoteDefinition(obj:Object=null)
23
        {
24
            if (obj)
25
            {
26
                for (var prop:String in obj)
27
                {
28
                    this[prop] = obj[prop];
29
                }
30
            }
31
        }
32
        
33
    }
34
}

Step 11: A Simple Event, VoteEvent, Specifying the States Transitions

Our application will have to react to several notifications. As you extend the application you will be able to add some functionalities, and events, but for our basic test, the list of custom events we will need is really short:

  • roleSet event, when the application know if it should behave as a vote manager or as a vote client
  • startVote, when a vote has to be displayed to the end user by a vote client
  • stopVote, when the manager ask everyone to stop the vote interactions
  • showResults, when all the votes have been merged and dispatched to the clients, for them to display the results
  • answersReceived, when the vote manager receive the answer from a client, in order to update the current results in real time on the manager screen.

We also need to store some information, like the vote details, or the results to display. Rather than using a dynamic object, we just add a data property, typed as Object, to store the VoteDefinition transmitted, an array of answers index, or any other relevant data... (more on this later)

To turn this into code, create a VoteEvent class extending flash.events.Event in the events package. We add the several event types we have just listed, and the data property:

1
package events
2
{
3
    import flash.events.Event;
4
    
5
    public class VoteEvent extends Event
6
    {
7
        
8
        static public const ROLE_SET:String     = 'roleSet';
9
        static public const START_VOTE:String   = 'startVote';
10
        static public const STOP_VOTE:String    = 'stopVote';
11
        static public const SHOW_RESULTS:String = 'showResults';
12
        static public const ANSWERS_RECEIVED:String = 'answersReceived';
13
        
14
        public var data:Object;
15
        
16
        public function VoteEvent(type:String, bubbles:Boolean=false, cancelable:Boolean=false)
17
        {
18
            super(type, bubbles, cancelable);
19
        }
20
    }
21
}

Step 12: Listing our Views

We have now the data we will play with, the event to notify the changes. But we still haven't anything to display. Let's list the views we will need:

  • a main container, which will be the common part of the application. It will initialize the VoteController and ask for a connection to the group, and then might switch to a few states: init, roleChoice, voteManager and voteClient.
  • the Client screens, implementing a few states: waitingVote, voteInProgress, waitingVoteEnd, voteResults
  • the Manager screen, implementing fewer states: editVote, voteInProgress, voteStopped.

I don't give here the full details of the states of each view because their names and function and quite obvious.

These views will use two custom components:

  • a basic gauge component, used to display vote results
  • the voteResults screen to display the results.

Step 13: The Vote Logic: VoteController.as

The controller will be link between our vote model, views and peers. Therefore this controller will be the only one that will use the NetGroupManager we have build before.

We will implement this controller as a Singleton, and add the meta-tags to reference the VoteEvent we have created before.

1
package controllers
2
{
3
    import events.VoteEvent;
4
    
5
    import flash.events.Event;
6
    import flash.events.EventDispatcher;
7
    import flash.events.IEventDispatcher;
8
    
9
    import models.VoteDefinition;
10
    
11
    import netgroup.NetGroupEvent;
12
    import netgroup.NetGroupManager;
13
    
14
    
15
    /** 

16
     * Event sent once the netConnection and netGroup connections have been 

17
     * successful, and the group is ready to be used. 

18
     */
19
    [Event(name="connectedToGroup", type="netgroup.NetGroupEvent")]
20
    
21
    /** 

22
     * Event sent when the role has been set, by the user of from the netGroup

23
     */
24
    [Event(name="roleSet", type="events.VoteEvent")]
25
    
26
    
27
    [Event(name="startVote", type="events.VoteEvent")]
28
    
29
    [Event(name="stopVote", type="events.VoteEvent")]
30
    
31
    [Event(name="showResults", type="events.VoteEvent")]
32
    
33
    /** answersReceived is sent when receiving an answer which isn't the last one */
34
    [Event(name="answersReceived", type="events.VoteEvent")]
35
    
36
    
37
    /**

38
     * VoteController manage the NetGroup connection and all data exchanges.

39
     * VoteController is implemented as a Singleton.

40
     * 

41
     */
42
    public class VoteController extends EventDispatcher
43
    {
44
        
45
        /* Singleton management */
46
        static private var _instance:VoteController;
47
        static public function get instance():VoteController
48
        {
49
            if (!_instance) _instance = new VoteController();
50
            return _instance;
51
        }

One essential function of the VoteController is to specify the list of available message subjects we will exchange in the NetGroup we will create for this application:

  • setVoteManagerExistence (to let the peers know there's already a vote manager in the group),
  • startVote,
  • stopVote,
  • submitAnswers,
  • showResults.

The controller will also set the roles the application user can choose.

1
2
/* Role definition */
3
static public const ROLE_CLIENT:String  = 'client'; 
4
static public const ROLE_MANAGER:String = 'manager'; 
5
private var _role:String;
6
[Bindable]
7
public function get role():String { return _role; }
8
public function set role(value:String):void
9
{
10
    if (_role != null) return; // Can be set only once

11
    _role = value;
12
    _handshakeVoteManager(); 
13
    dispatchEvent(new VoteEvent(VoteEvent.ROLE_SET));
14
}
15
16
17
18
static public const SUBJECT_SET_VOTE_MANAGER_EXISTENCE:String = 'setVoteManagerExistence';
19
static public const SUBJECT_START_VOTE:String       = 'startVote';
20
static public const SUBJECT_STOP_VOTE:String        = 'stopVote';
21
static public const SUBJECT_SUBMIT_ANSWERS:String   = 'submitAnswers';
22
static public const SUBJECT_SHOW_RESULTS:String     = 'showResults';

If you look at the role setter function, you will notice that the roleSet event is dispatched, and that we call a _handshakeVoteManager() method. This method will be quite simple: if we're a voteManager, then we tell our peers that we're here. We will look in a few moments how our peers will listen for this message.

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

7
        netGroupMgr.sendMessage(SUBJECT_SET_VOTE_MANAGER_EXISTENCE, {});
8
    }
9
}

We have used netGroupMgr.sendMessage(), a function of our NetGroupManager. This means we have to initialize this manager and start a connection before. This is what we do in the constructor of this VoteController. This is also the right place to add the listeners for netGroupManager events to know when a message is posted, when a peer joined, and more basically if we are connected in a group.

1
2
protected var netGroupMgr:NetGroupManager;
3
4
public function VoteController(target:IEventDispatcher=null)
5
{
6
    super(target);
7
    if (_instance) return;
8
9
    netGroupMgr = new NetGroupManager();
10
    netGroupMgr.applicationName = "active.tutsplus.examples/";
11
    netGroupMgr.groupName       = "sampleApp";
12
    netGroupMgr.addEventListener(NetGroupEvent.POSTING, onNetGroupMessage, false,0,true);
13
    netGroupMgr.addEventListener(NetGroupEvent.CONNECTED_TO_GROUP, onConnectionEstablished, false,0,true);
14
    netGroupMgr.addEventListener(NetGroupEvent.DISCONNECTED_FROM_GROUP, onConnectionLost, false,0,true);
15
    netGroupMgr.addEventListener(NetGroupEvent.NEIGHBOR_JOINED, onNeighborJoined, false,0,true);
16
    
17
    netGroupMgr.connect();
18
}

In this constructor we have created an instance of our NetGroupManager, set the name of our group and started a connection. We also added some listeners for the basic group events that our manager will send us. Let's see how to handle theses group events.

When the connection is done (remember that we're dealing with the connection success of the NetGroupManager, which means it has successfully connected to a NetConnection then to a NetGroup), we will set a "connected" flag to true and dispatch a VoteEvent to let the application know that the initialization is done.

1
2
[Bindable] 
3
public var connected:Boolean = false;
4
5
protected function onConnectionEstablished(event:NetGroupEvent):void
6
{
7
    connected = true;
8
    dispatchEvent(new Event(NetGroupEvent.CONNECTED_TO_GROUP));
9
}
10
11
protected function onConnectionLost(event:NetGroupEvent):void
12
{
13
    connected = false;
14
}

When a peer joins the group, so the NetGroupManager give us a neighborJoined event, we tell this peer if we're the voteManager. This way as soon as the application connects to the group, it will be informed by the voteManager if such a manager already exists in the group.

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

The last netGroupManager listener we have defined is the posting callback. It will be triggered every time we receive a message from the group. To handle these messages, we then just have to perform a switch/case with the subject values we have already set before:

1
2
/* Current vote buffers */
3
public var currentVoteDefinition:VoteDefinition;
4
public var currentVoteAnswers:Array;
5
public var currentVoteUsers:int;
6
public var totalVoteClients:int;
7
8
/**

9
 * Callback for all the netGroup message we will receive. 

10
 * Message routing, based on the message subject, will be done here.  

11
 */
12
protected function onNetGroupMessage(event:NetGroupEvent):void
13
{
14
    var message:Object = event.message;
15
    if (!message) return;
16
    var subject:String = event.subject as String;
17
  
18
    switch (subject) 
19
    {
20
        case VoteController.SUBJECT_SET_VOTE_MANAGER_EXISTENCE:
21
            // Another peer is already a vote manager, so I'll be a 

22
            // vote client if I haven't yet set my role

23
            if (role == null)
24
                role = ROLE_CLIENT;
25
            break;
26
        
27
        case SUBJECT_START_VOTE:
28
            // We just received a vote, send it !

29
            doStartVote(new VoteDefinition(message));
30
            break;
31
        
32
        case SUBJECT_SUBMIT_ANSWERS:
33
            if (role == ROLE_MANAGER)
34
            {
35
                registerClientAnswers(message as Array);
36
            }
37
            break;
38
            
39
        case SUBJECT_STOP_VOTE:
40
            dispatchEvent(new VoteEvent(VoteEvent.STOP_VOTE));
41
            break;
42
            
43
        case SUBJECT_SHOW_RESULTS:
44
            currentVoteDefinition = new VoteDefinition(message.voteDefinition);
45
            currentVoteAnswers    = message.answers;
46
            currentVoteUsers      = message.voteUserCnt;
47
            totalVoteClients      = message.totalVoteClients;
48
            
49
            dispatchEvent(new VoteEvent(VoteEvent.SHOW_RESULTS));
50
            
51
            break;
52
    }
53
        
54
}

Do you remember the _handshakeVoteManager() method from Step 13 that calls netGroupMgr.sendMessage(SUBJECT_SET_VOTE_MANAGER_EXISTENCE, {});?

In the SUBJECT_SET_VOTE_MANAGER_EXISTENCE branch we see the other part of the message, when a peer receives it. When receiving this information, if the role of this peer hasn't already been set, then we force it to be a client, as a manager already exists in the group. As the handshakeVoteManager method is called every time we set our role and every time a peer joins the group, we are sure that every new peer will aware of the existence of any manager.

The stopVote message dispatches an event, more on this in the view steps.

For the startVote, submitAnswer and showResults branchs, we use the data contained in the message. The type of this data will vary depending on the subject.

The startVote branch will take this message and dispatch it in a startVote event to the views:

1
2
/** 

3
 * doStartVote will be called by submitVote for the VoteManager, and from 

4
 * onNetGroupMessage for the voteClients, when receiving a SUBJECT_START_VOTE message

5
 */
6
protected function doStartVote(def:VoteDefinition):void
7
{
8
    var evt:VoteEvent = new VoteEvent(VoteEvent.START_VOTE);
9
    evt.data = def;
10
    dispatchEvent(evt);
11
}

The showResults works almost the same way, but the data is kept in the VoteController as public buffers, and the event is dispatched.

We will see that each client send its answers as an Array of integers. If the clients have selected the answer two and four, the message will be [2,4]. This is why the subject answers handle the message variable as an Array when calling registerClientAnswers.

1
2
protected function registerClientAnswers(answers:Array):void
3
{
4
    // answers contains the position and the answers selected by the user, increment our counters

5
    for each (var ind:int in answers)
6
    {
7
        currentVoteAnswers[ind]++;
8
    }
9
    
10
    currentVoteUsers++;
11
    
12
    // notify the manager of the vote progression

13
    dispatchEvent(new VoteEvent(VoteEvent.ANSWERS_RECEIVED));
14
    
15
    
16
    if (currentVoteUsers >= totalVoteClients)
17
    {
18
        // We have received all the answers

19
        dispatchEvent(new VoteEvent(VoteEvent.STOP_VOTE));
20
        netGroupMgr.sendMessage(SUBJECT_STOP_VOTE, 'all answers received');
21
    } 
22
    
23
}

The manager's controller store how many time every answers has been selected simply by incrementing the different values of an array of answers number. It then dispatch a event to let the views know that a new answer has been received. This way the manager will display the results in real time. At last if all the votes we are waiting have been received, we stop the vote, and notify both the local user with an event and the peers with a stopVote message. As we have seen a few lines before, this message will dispatch the same stopVote event.

Our manager initializes correctly and handles the messages it receive from the group. Now we need to make sure these messages are sent, and therefore we still need to add a few public methods for our views to be able to start a vote and submit their answers.

1
2
public function submitVote(def:VoteDefinition):void
3
{
4
    if (role != ROLE_MANAGER) return;
5
    
6
    currentVoteDefinition = def;
7
    currentVoteAnswers = [null,0,0,0,0,0]; // no answers logged yet. 0 is the question and therefore cannot be answered

8
    currentVoteUsers = 0;
9
    totalVoteClients = netGroupMgr.neighborCount; // We store the number of answer we'll be waiting.

10
    
11
    doStartVote(def);
12
    netGroupMgr.sendMessage(SUBJECT_START_VOTE, def);
13
}

To start a vote, the view will only have to pass the VoteDefinition (i.e. the texts of the question and answers). We will initialize our buffers to store the questions, remember how many answers have been received and how many we will wait for. Then we send the startVote message to our peers. By storing the totalVoteClients number when starting a vote, I'm sure I'll wait for the correct answer count, even if new peers join the group during the vote (these peers will have to wait for the next vote to participate).

The submit answer is trivial: we already said our answers are a simple array of the position of the answers selected by the end-user. We just send that to the manager:

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

At last, when a vote is finished, the vote manager will send the vote results to every peer, so that every vote client might display the full results.

1
2
public function parseResultsForBroadcast():void
3
{
4
    var msg:Object = {
5
        answers:currentVoteAnswers, 
6
        voteDefinition:currentVoteDefinition,
7
        voteUserCnt:currentVoteUsers, 
8
        totalVoteClients:totalVoteClients
9
    };
10
    
11
    netGroupMgr.sendMessage(SUBJECT_SHOW_RESULTS, msg);
12
}

That's it. We have finished the model, the controller and the event. We just need to have some graphic interface to display the choice to the user and to listen for these events.


Step 14: The Application Common Part Between Manager and Client

The host MXML file will be the core of our application views. It will contain the common parts of both manager and client. As we already said before, it will have a few states:

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

init state will be the default one, when we wait the controller to dispatch the connectedToGroup event to let us know we are ready to work. Then the application will enter the roleChoice state, waiting that the role of this instance is set to manager or client. At last when the role is set it will enter either the voteManager or the voteClient state, depending on the role.

So the init state will start with

1
2
protected function _initializeHandler(event:FlexEvent):void
3
{
4
    // We make sure the VoteController has been instantiated, simply by asking for its singleton reference.

5
    var voteCtrl:VoteController = VoteController.instance;
6
    voteCtrl.addEventListener(NetGroupEvent.CONNECTED_TO_GROUP, initDone);
7
    voteCtrl.addEventListener(VoteEvent.ROLE_SET, onRoleSet);
8
}
9
10
protected function initDone(event:Event):void
11
{
12
    currentState = 'roleChoice';
13
}

And the GUI part will be very very basic:

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

Once the application is connected to the group, we've seen that the initDone handler modify the state of the component to 'roleChoice'. Let's set the user interface of roleChoice: two buttons to choose one role or another.

1
2
<s:VGroup
3
    includeIn="roleChoice"
4
    visible="{!VoteController.instance.role}"
5
    horizontalCenter="0" verticalCenter="0"
6
    horizontalAlign="center"
7
    >
8
    
9
    <s:Label text="Choose you role"/>
10
    
11
    <s:Button id="btnManager" 
12
              label="Vote manager" width="250"  height="40"
13
              click="btnRole_clickHandler(VoteController.ROLE_MANAGER)"
14
              />
15
    
16
    <s:Button id="btnClient" 
17
              label="Client" width="250"  height="40"
18
              click="btnRole_clickHandler(VoteController.ROLE_CLIENT)"
19
              />
20
    
21
</s:VGroup>

These buttons call the btnRole_clickHandler method

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

The important thing here, is that we directly modify the role property of the controller. We have seen that the controller will dispatch a roleSet event, and in the _initializeHandler of this component we have registered for this event. This means that the onRoleSet method will be called when the user click on one of this two buttons, or when the application's role is forced to client by the controller because another voteManager has notify its existence to the group.

1
2
protected function onRoleSet(event:VoteEvent):void
3
{
4
    switch(VoteController.instance.role)
5
    {
6
        case VoteController.ROLE_CLIENT:
7
            currentState = 'voteClient';
8
            break;
9
        
10
        case VoteController.ROLE_MANAGER:
11
            currentState = 'voteManager';
12
            break;
13
    }
14
}}

All the initialization is done by now, and we have to create the views for the manager and client roles. From our component it will be as simple as including one component or another depending on the state:

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

Now we can enter the inner details of both of these views.


Step 15: Preparing the Gauge to Display Results

Both the manager and the client will need to display the number of times an answer has been selected, so first create a little component for this.

We will extend a progress bar to use it as a gauge. In the screens package, create a new MXML component, extending mx.controls.ProgressBar. We will set its default value for manual mode and place the label in the center, then override the setProgress method to build the label's text:

1
2
<?xml version="1.0" encoding="utf-8"?>
3
<mx:ProgressBar xmlns:fx="http://ns.adobe.com/mxml/2009" 
4
                xmlns:s="library://ns.adobe.com/flex/spark" 
5
                xmlns:mx="library://ns.adobe.com/flex/mx"
6
                
7
                mode="manual" labelPlacement="center" label="" 
8
                includeInLayout="{this.visible}"
9
                width="100%"
10
                >
11
    
12
    <fx:Script >
13
        <![CDATA[

14
            override public function setProgress(value:Number, total:Number):void

15
            {

16
                super.setProgress(value, total);

17
                this.label = value +'/'+total;

18
            }

19
        ]]> 
20
    </fx:Script>
21
    <fx:Declarations />
22
23
</mx:ProgressBar>

Step 16:- Manager_Vote, a User Interface to Edit Votes and View Results

In the screens package, create a Manager_Vote component extending spark.components.Group.

The vote manager will have to listen to some event of the VoteController, so let's add a creationComplete listener and fill it with the event listener's definitions to listen to startVote, stopVote and answersReceived events. We have already seen in the VoteController implementation when these events are dispatched.

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

The manager has a few states

  • editVote : first the end-user can edit a vote,
  • voteInProgress : when a vote is in progress. The manager will have the results displayed in real time, and a button to be able to stop the vote whenever he wants.
  • voteStopped : the results are displayed, and the user has the option to start a new vote.
1
2
<s:states>
3
    <s:State name="editVote"        enterState="editVote_enterStateHandler(event)"/>
4
    <s:State name="voteInProgress"  stateGroups="_voting"/>
5
    <s:State name="voteStopped"     stateGroups="_voting"/>
6
</s:states>

The editVote state triggers this method, which set a bindable Boolean flag we will use in the vote form:

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

Now the main part of our UI. Basically, editing a vote is modifying the few text inputs for the questions and the possible answers. For each answer we had a gauge, which visibility will be set to true only for the _voting state (voteInProgress and voteStopped states).

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

In the second half of this code block, you have seen we put three buttons. The submit vote button to start a vote, which is only enabled for the editVote state. Then the stop vote and new vote buttons which are only included in their relevant states.

To start a vote, we have to create a VoteDefinition object, fill it with the texts and call the submitVote method of the controller we have seen before.

1
2
protected function btnSubmitVote_clickHandler():void
3
{
4
    enabledEdit = false; // We cannot modify the text while the vote is in progress

5
    
6
    // We collect all the informations and ask for a broadcast

7
    var def:VoteDefinition = new VoteDefinition();
8
    def.question = labelVote0.text; 
9
    if (labelVote1.text != '') def.answer1 = labelVote1.text; 
10
    if (labelVote2.text != '') def.answer2 = labelVote2.text; 
11
    if (labelVote3.text != '') def.answer3 = labelVote3.text; 
12
    if (labelVote4.text != '') def.answer4 = labelVote4.text; 
13
    if (labelVote5.text != '') def.answer5 = labelVote5.text; 
14
        
15
    VoteController.instance.submitVote(def);
16
}

The voteController will then dispatch a startVote event, and we have registered for that in our creationComplete handler:

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

6
    onVoteUpdated();
7
}

Now we're in the voteProgress state. This mean we might receive answersReceived and stopVote events from the controller, and the user may press the stop vote button. The stop vote part is really easy: it ask the controller to stop and send results to everyone:

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

For the answerReceived, we have to read the currentVoteAnswers buffers from the controller (which is updated every time answers are received, and before broadcasting this event). This way we just have to call the setProgress method of every gauge and here we are, we have our real-time vote feedback:

1
2
protected function onVoteUpdated(event:VoteEvent=null):void
3
{
4
    var ctrl:VoteController = VoteController.instance;
5
    
6
    // Update the number of answer received

7
    btnSubmitVote.label = 'vote in progress... ' + ctrl.currentVoteUsers +'/'+ctrl.totalVoteClients;
8
    var ar:Array = ctrl.currentVoteAnswers;
9
    results1.setProgress(ar[1], ctrl.totalVoteClients);
10
    results2.setProgress(ar[2], ctrl.totalVoteClients);
11
    results3.setProgress(ar[3], ctrl.totalVoteClients);
12
    results4.setProgress(ar[4], ctrl.totalVoteClients);
13
    results5.setProgress(ar[5], ctrl.totalVoteClients);
14
}

The new vote button simply set the current state to editVote. This way the gauges aren't visible any longer, the fields can be edited, and the submit vote button is enabled. Everything is ready to edit a vote, send it, and display the results.


Step 17: Client_Vote, a View to Display Choices to the End-User

The screens.Client_Vote component is built on the same model as the manager; as it extends Group it will have a few states:

  • waitingVote when no vote has been received yet,
  • voteInProgress when a vote has been received and the user can enter his answers
  • waitingVoteEnd, when the user submitted his answers but not every other clients has done so, so that the vote is still running on other computers (or browsers tabs)
  • voteResults, when everyone has answered or when the manager manually stopped the vote, and sent the results to all the peers
1
2
<s:states>
3
    <s:State name="waitingVote" />
4
    <s:State name="voteInProgress" stateGroups="_voting"/>
5
    <s:State name="waitingVoteEnd" stateGroups="_voting"/>
6
    <s:State name="voteResults" />
7
</s:states>

Like the manager, we will listen for the controller's events, so we declare theses listener in our creationComplete hander:

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

The user interface for the waitingVote is basic, as we haven't anything yet to display:

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

The vote results will call a Vote_Result component to display the gauges and text labels.

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

When a startVote event is received, the VoteDefinition object will be stored in a bindable property, and we will enter the voteInProgress state. If this is not the first vote we received, the vote UI is already created, and its checkboxes might be selected, so we make sure everything is unchecked in this case.

1
2
[Bindable] protected var currentVoteDefinition:VoteDefinition;
3
4
protected function onVoteStarted(event:VoteEvent):void
5
{
6
    var voteDefinition:VoteDefinition = event.data as VoteDefinition;
7
    if (voteDefinition == null) return;
8
    currentVoteDefinition = voteDefinition;
9
    // By default, no answer is checked 

10
    if (labelVote1 != null)
11
    {
12
        labelVote1.selected = false;
13
        labelVote2.selected = false;
14
        labelVote3.selected = false;
15
        labelVote4.selected = false;
16
        labelVote5.selected = false;
17
    }
18
    currentState = 'voteInProgress';
19
}

Now the UI of the voting form. Like the manager we have components for the question and each answer. For the client each answer is a checkbox, which is only enabled while the vote is in progress (i.e. we are in the voteInProgress state). The labels of the question and answers are binded to the voteDefinition's properties. At last we have a submit vote button to send our choices.

1
2
<!--- Vote -->
3
    <s:Group 
4
        includeIn="_voting" 
5
        horizontalCenter="0" verticalCenter="0"
6
        >
7
        <s:layout>
8
            <s:VerticalLayout />
9
        </s:layout>
10
        
11
        <s:Label id="labelVote0" text="{currentVoteDefinition.question}" 
12
                 fontSize="20" width="100%" />
13
        
14
        <!-- In a real-world development, the results would be made of 

15
        components and not repeated like this. I've chosen to use this very 

16
        basic checkbox repetition to focus on the network dialogs. -->
17
        
18
        <s:CheckBox id="labelVote1" 
19
                    enabled="{this.currentState == 'voteInProgress'}" 
20
                    label="{currentVoteDefinition.answer1}"
21
                    visible="{currentVoteDefinition.answer1 != null}"
22
                    styleName="answer" fontSize="14" width="100%"
23
                    />
24
        
25
        <s:CheckBox id="labelVote2" 
26
                    enabled="{this.currentState == 'voteInProgress'}" 
27
                    label="{currentVoteDefinition.answer2}"
28
                    visible="{currentVoteDefinition.answer2 != null}"
29
                    styleName="answer" fontSize="14" width="100%" 
30
                    />
31
        
32
        <s:CheckBox id="labelVote3" 
33
                    enabled="{this.currentState == 'voteInProgress'}" 
34
                    label="{currentVoteDefinition.answer3}"
35
                    visible="{currentVoteDefinition.answer3 != null}"
36
                    styleName="answer" fontSize="14" width="100%"
37
                    />
38
        
39
        <s:CheckBox id="labelVote4" 
40
                    enabled="{this.currentState == 'voteInProgress'}" 
41
                    label="{currentVoteDefinition.answer4}"
42
                    visible="{currentVoteDefinition.answer4 != null}"
43
                    styleName="answer" fontSize="14" width="100%"
44
                    />
45
        
46
        <s:CheckBox id="labelVote5" 
47
                    enabled="{this.currentState == 'voteInProgress'}" 
48
                    label="{currentVoteDefinition.answer5}"
49
                    visible="{currentVoteDefinition.answer5 != null}"
50
                    styleName="answer" fontSize="14" width="100%"
51
                    />
52
        
53
        <s:Button 
54
            id="btnSubmitVote"
55
            label="Submit"  width="250" height="40"
56
            label.waitingVoteEnd="No more answers allowed"
57
            enabled="{this.currentState == 'voteInProgress'}"
58
            click="btnSubmitVote_clickHandler(event)"
59
            />
60
    </s:Group>

When the user presses the "submit vote" button, we will create an array with the positions of the selected answers and call the submitAnswer method of our VoteController. Remember that the controller then sends these answers to our peers in the group. The network operations are only done by the controller. The views and models don't care about how this is done, they just call the submitVote public API we have written in our controller. As a user can answer vote only one time, we immediately update our state to make sure that the vote is stopped.

1
2
protected function btnSubmitVote_clickHandler(event:MouseEvent):void
3
{
4
    // Collect our answers then send them

5
    var answers:Array = [];
6
    if (labelVote1.selected) answers.push(1);
7
    if (labelVote2.selected) answers.push(2);
8
    if (labelVote3.selected) answers.push(3);
9
    if (labelVote4.selected) answers.push(4);
10
    if (labelVote5.selected) answers.push(5);
11
    
12
    // Send our vote

13
    VoteController.instance.submitAnswers(answers);
14
    
15
    // We can no longer edit our vote

16
    onVoteStopped();
17
}
18
19
protected function onVoteStopped(event:VoteEvent=null):void
20
{
21
    currentState = 'waitingVoteEnd';
22
}

At last, when the vote manager in the group sends the vote results, we get a showResults event from the vote controller which triggers our onShowResults handler defined in the creationComplete handler.

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

In the voteResults state, only the Vote_Results component is visible.


Step 18: VoteResults: Displaying the Last Vote with Labels and Gauges

The screens.Vote_Results is a simple rewrite of the manager interface for the gauges and using text labels rather than TextInput components. When the controller receives a showResults event, we've seen it stores all the details in its currentVoteDefinition and currentVoteAnswers properties. The result component read these properties to set the value of the gauges and text labels.

1
2
<?xml version="1.0" encoding="utf-8"?>
3
<s:Group xmlns:fx="http://ns.adobe.com/mxml/2009" 
4
         xmlns:s="library://ns.adobe.com/flex/spark" 
5
         xmlns:mx="library://ns.adobe.com/flex/mx"
6
         xmlns:screens="screens.*"
7
         
8
         horizontalCenter="0"
9
         verticalCenter="0"
10
         >
11
    <s:layout>
12
        <s:VerticalLayout/>
13
    </s:layout>
14
    
15
    <fx:Script>
16
        <![CDATA[

17
            import controllers.VoteController;

18
            

19
            import models.VoteDefinition;

20
            

21
            import mx.events.FlexEvent;

22
            

23
            [Bindable] protected var currentVote:VoteDefinition;

24
            

25
            public function showResults():void

26
            {

27
                var ctrl:VoteController = VoteController.instance;

28
                currentVote = ctrl.currentVoteDefinition;

29
                var ar:Array = ctrl.currentVoteAnswers;

30
                results1.setProgress(ar[1], ctrl.totalVoteClients);

31
                results2.setProgress(ar[2], ctrl.totalVoteClients);

32
                results3.setProgress(ar[3], ctrl.totalVoteClients);

33
                results4.setProgress(ar[4], ctrl.totalVoteClients);

34
                results5.setProgress(ar[5], ctrl.totalVoteClients);

35
            }

36
        ]]>
37
    </fx:Script>
38
    <fx:Declarations />
39
    
40
    <s:Label id="labelVote6" 
41
                 text="{currentVote.question}" styleName="question" width="100%"/>
42
    
43
    <s:Label id="labelVote7" 
44
                 text="{currentVote.answer1}" styleName="answer" width="100%"/>
45
    <screens:Jauge id="results6" />
46
    
47
    <s:Label id="labelVote8" 
48
             text="{currentVote.answer2}" styleName="answer" width="100%"/>
49
    <screens:Jauge id="results7" />
50
51
    <s:Label id="labelVote9" 
52
             text="{currentVote.answer3}" styleName="answer" width="100%"/>
53
    <screens:Jauge id="results8" />
54
    
55
    <s:Label id="labelVote10" 
56
             text="{currentVote.answer4}" styleName="answer" width="100%"/>
57
    <screens:Jauge id="results9" />
58
    
59
    <s:Label id="labelVote11" 
60
             text="{currentVote.answer5}" styleName="answer" width="100%"/>
61
    <screens:Jauge id="results10" />
62
    
63
</s:Group>

That's all, we have built everything and we are ready to test in on several computers.


Step 19: Deploy and Test

Now that every part have been put together, you're done, there's no configuration to do to make it work for interconnecting several computers : open you this demo movie multiple times on some computers and see how they automatically discover their peers, receive the vote from the manager and how everyone get the results.

open the final result in a new window


Step 20: Where to go From Here?

So what does RTMFP and the new netGroup support in Flash Player 10.1 means for us, Flash developers? Obviously creating a audio/video chat becomes really easy. But having several computers automatically connect between each other without having the end-user to do any network configuration or enter any IP address or server URL is really great for many other things. Think about quiz for kiosk or stand animations where each participant has his screen and UI, and the animator has a manager interface.

Another case might be a remote log, like a monitoring tool, automatically receiving the updates from all the kiosks, or all the student computers.

Having an easy way to implement zero-configuration networking between flash clients open a very wide range of features we will be able to bring into our applications, might it be for learning, monitoring, interacting...

If you want to go further, there are plenty of excellent resources on the Internet.

  • The first entry point should be the Actionscript documentation, where netGroup.post() method is very well covered and gives everything you need to get the first steps. Of course all the other classes of the flash.net package we have used in this article are worth reading, like NetConnection, GroupSpecifier and NetStream.
  • Adobe Labs Cirrus page, from where you can get a developer key.
  • Wikipedia page about multicast: http://en.wikipedia.org/wiki/Multicast
  • IANA's list of valid multicast addresses, included the reserved ones, to let you choose you multicast address: http://www.iana.org/assignments/multicast-addresses/
  • Adobe's 2009 Max session about RTMFP gave me a lot of information, like the need for a message to be unique to be considered new when using NetGroup.post(). Moreover, there's really a lot of information there about media multicasting that is worth knowing.
Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.