Reactor Chat Tutorial, Part 3
Chat Example 3 adds the following new features to Chat Example 2:
- Private chat
- A "total messages sent" counter
- An external configuration file
Here's what the new chat looks like:
Once again, in order to focus on Reactor code rather than user interface code, the code for this chat does not use any component libraries such as the Flex framework or Flash's components. Hence, the private chat feature is brute force: to set a private chat recipient, you must type the recipient's name in the "private chat recipient" text field.
The private chat feature is implemented with a simple client-to-client message, sent via the Client class's sendMessage() method.
1 2 | privateChatRecipient.sendMessage("PRIVATE_MESSAGE", outgoingMessages.text); |
All clients listen for PRIVATE_CHAT messages by registering with the MessageManager:
1 2 | connection.getMessageManager().addMessageListener("PRIVATE_MESSAGE", privateMessageListener); |
The message counter is implemented with a room attribute, MESSAGE_COUNTER, that is incremented by one every time a message is sent. To increment the attribute's value mathematically on the server, the client sends an expression and asks for it to be evaluated server-side:
1 2 3 4 | // The %v means "the attribute's current value" // The last "true" means "evaluate the specified value // on the server before assignment" chatRoom.setAttribute("MESSAGE_COUNTER", "%v+1", true, false, true); |
By incrementing MESSAGE_COUNTER on the server rather than on the client, the application avoids an important synchronization problem: Suppose two clients simultaneously send a message, and both want to increment the counter entirely with client code. If the counter's existing value were, say, 12, both clients would check the value of MESSAGE_COUNTER, find it to be 12, add one to that, and tell the server to set the new value of MESSAGE_COUNTER to 13. But two messages were sent, so the actual value should be 14. The "server-side increment" approach avoids the miscount by performing the increment on the server's current value of MESSAGE_COUNTER, which is guaranteed to be accurate. Instead of saying "set MESSAGE_COUNTER to 13," the clients say "add one to MESSAGE_COUNTER's current server-side value."
Clients listen for changes to MESSAGE_COUNTER by registering for the AttributeEvent.UPDATE event.
1 2 3 4 5 6 7 8 9 10 11 12 | // Event registration chatRoom.addEventListener(AttributeEvent.UPDATE, updateRoomAttributeListener); // Event listener protected function updateRoomAttributeListener (e:RoomEvent):void { var changedAttr:Attribute = e.getChangedAttr(); if (changedAttr.name == "MESSAGE_COUNTER") { messageCounter.text = "Total Messages: " + parseInt(e.getChangedAttr().value); } } |
The Code
Here's the complete code for the chat.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 | package { import flash.display.Sprite; import flash.events.KeyboardEvent; import flash.text.TextField; import flash.text.TextFieldType; import flash.ui.Keyboard; import net.user1.reactor.*; public class UnionChatPart3 extends Sprite { // ============================================================================= // APPLICATION VARIABLES // ============================================================================= // Union objects protected var reactor:Reactor; protected var chatRoom:Room; // User-interface objects protected var incomingMessages:TextField; protected var outgoingMessages:TextField; protected var userlist:TextField; protected var nameInput:TextField; protected var privateRecipientInput:TextField; protected var messageCounter:TextField; // ============================================================================= // APPLICATION STARTUP // ============================================================================= // Constructor public function UnionChatPart3 () { // Create the user interface buildUI(); // Make the Reactor object, and load configuration settings from // an external file. The connection will open automatically after the // settings have loaded. reactor = new Reactor("config.xml"); // Register for the READY event reactor.addEventListener(ReactorEvent.READY, readyListener); } // ============================================================================= // CONNECTION READY TASKS // ============================================================================= // Method invoked when the connection is ready protected function readyListener (e:ReactorEvent):void { // Assign the current user an auto-generated username setUserName("Guest" + reactor.self().getClientID()); // Register for private-chat messages from other users reactor.getMessageManager().addMessageListener("PRIVATE_MESSAGE", privateMessageListener); // Display a welcome message displayMessage("Connected to Union"); // Create the settings for the chat room. We want the // room to exist until the server shuts down, so set // dieOnEmpty to false. var settings:RoomSettings = new RoomSettings(); settings.removeOnEmpty = false; // Create the chat room (if the room already exists, the // request will be ignored, but the Room reference will // still be valid) chatRoom = reactor.getRoomManager().createRoom( "chatRoomThree", settings); // Register for regular chat messages chatRoom.addMessageListener("CHAT_MESSAGE", chatMessageListener); // Register for room events chatRoom.addEventListener(RoomEvent.JOIN, joinRoomListener); chatRoom.addEventListener(RoomEvent.ADD_OCCUPANT, addClientListener); chatRoom.addEventListener(RoomEvent.REMOVE_OCCUPANT, removeClientListener); chatRoom.addEventListener(RoomEvent.UPDATE_CLIENT_ATTRIBUTE, updateClientAttributeListener); chatRoom.addEventListener(AttributeEvent.UPDATE, updateRoomAttributeListener); // Join the chat room chatRoom.join(); } // ============================================================================= // UI CREATION // ============================================================================= // Create the user interface protected function buildUI ():void { // Incoming chat messages incomingMessages = makeTextField(0, 0, 300, 200); incomingMessages.wordWrap = true; // Outgoing chat messages outgoingMessages = makeTextField(0, 210, 399, 20); outgoingMessages.type = TextFieldType.INPUT; outgoingMessages.addEventListener(KeyboardEvent.KEY_UP, outgoingKeyUpListener); // The list of users userlist = makeTextField(310, 0, 89, 200); // Input field for setting user name nameInput = makeTextField(0, 240, 399, 20); nameInput.type = TextFieldType.INPUT; nameInput.addEventListener(KeyboardEvent.KEY_UP, nameKeyUpListener); // Displays the total messages sent in the chat messageCounter = makeTextField(0, 274, 120, 20, false, false); messageCounter.text = "Total Messages: ??"; messageCounter.textColor = 0xFFFFFF; // Instructions for using private chat var privateRecipientLabel:TextField = makeTextField(130, 274, 180, 20, false, false); privateRecipientLabel.text = "To private chat, enter user name here:"; privateRecipientLabel.textColor = 0xFFFFFF; // Input field for setting private-chat recipient privateRecipientInput = makeTextField(310, 270, 89, 20); privateRecipientInput.type = TextFieldType.INPUT; addChild(incomingMessages); addChild(outgoingMessages); addChild(userlist); addChild(nameInput); addChild(messageCounter); addChild(privateRecipientLabel); addChild(privateRecipientInput); } // Helper function to build text fields protected function makeTextField (tx:Number = 0, ty:Number = 0, twidth:Number = 0, theight:Number = 0, border:Boolean = true, background:Boolean = true):TextField { var textField:TextField = new TextField(); textField.x = tx; textField.y = ty; textField.width = twidth; textField.height = theight; textField.border = border; textField.background = background; return textField; } // ============================================================================= // UI EVENT LISTENERS // ============================================================================= // Keyboard listener for outgoingMessages text field protected function outgoingKeyUpListener (e:KeyboardEvent):void { var privateChatRecipient:IClient; // When the user presses the ENTER key... if (e.keyCode == Keyboard.ENTER) { // If there's a private-chat recipient specified, // attempt to find the matching client if (privateRecipientInput.text != "") { privateChatRecipient = reactor.getClientManager().getClientByAttribute("USERNAME", privateRecipientInput.text); if (privateChatRecipient == null) { displayMessage("Cound not send message to: '" + privateRecipientInput.text + "'. No such user."); return; } } // If there's no private-chat recipient, // send the message to everyone in the room if (privateChatRecipient == null) { chatRoom.sendMessage("CHAT_MESSAGE", true, null, outgoingMessages.text); // Add one to the room's MESSAGE_COUNTER attribute chatRoom.setAttribute("MESSAGE_COUNTER", "%v+1", true, false, true); } else { // There's a valid private-chat recipient, so send // the message to that client only privateChatRecipient.sendMessage("PRIVATE_MESSAGE", outgoingMessages.text); displayMessage("You told " + getUserName(privateChatRecipient) + ": " + outgoingMessages.text); } outgoingMessages.text = ""; } } // Keyboard listener for nameInput protected function nameKeyUpListener (e:KeyboardEvent):void { // When the user presses the ENTER key... if (e.keyCode == Keyboard.ENTER) { // Assign the new user name setUserName(nameInput.text); nameInput.text = ""; } } // ============================================================================= // UNION MESSAGE LISTENERS // ============================================================================= // Method invoked when a regular chat message is received protected function chatMessageListener (fromClient:IClient, messageText:String ):void { displayMessage(getUserName(fromClient) + " says: " + messageText); } // Method invoked when a private message is received protected function privateMessageListener (fromClient:IClient, messageText:String ):void { displayMessage("Private message from: " + getUserName(fromClient) + ": " + messageText); } // ============================================================================= // ROOM EVENT LISTENERS // ============================================================================= // Method invoked when the room's client list and // attributes are synchronized and ready for use protected function joinRoomListener (e:RoomEvent):void { updateUserList(); } // Method invoked when a client joins the room protected function addClientListener (e:RoomEvent):void { if (e.getClient().isSelf()) { displayMessage("You joined the chat."); } else { if (chatRoom.getSyncState() != SynchronizationState.SYNCHRONIZING) { // Show a "guest joined" message only when the room isn't performing // its initial occupant-list synchronization. displayMessage(getUserName(e.getClient()) + " joined the chat."); } } updateUserList(); } // Method invoked when a client leave the room protected function removeClientListener (e:RoomEvent):void { displayMessage(getUserName(e.getClient()) + " left the chat."); updateUserList(); } // Method invoked when a client // changes one of its shared attributes protected function updateClientAttributeListener (e:RoomEvent):void { var changedAttr:Attribute = e.getChangedAttr(); // If the attribute that changed was USERNAME... if (changedAttr.name == "USERNAME") { // Display a message and update the user list if (changedAttr.oldValue != null) { displayMessage(changedAttr.oldValue + "'s name changed to " + getUserName(e.getClient())); updateUserList(); } } } // Method invoked when any of the room's // shared room attributes change protected function updateRoomAttributeListener (e:AttributeEvent):void { var changedAttr:Attribute = e.getChangedAttr(); // If the attribute that changed was MESSAGE_COUNTER... if (changedAttr.name == "MESSAGE_COUNTER") { // Display the new message count messageCounter.text = "Total Messages: " + parseInt(e.getChangedAttr().value); } } // ============================================================================= // USERNAME MANAGEMENT // ============================================================================= // Returns the specified client's username protected function getUserName (client:IClient):String { return client.getAttribute("USERNAME"); } // Assigns a new username to the current client protected function setUserName (userName:String):void { var self:IClient; // Check if the desired new name is valid if (userName == null || userName.length == 0) { return; } self = reactor.self(); // Set the shared attribute so other clients will // be notified of the changed user name. self.setAttribute("USERNAME", userName); } // ============================================================================= // UI CONTROL // ============================================================================= // Displays the client list on screen protected function updateUserList ():void { userlist.text = ""; for each (var client:IClient in chatRoom.getOccupants()) { userlist.appendText(getUserName(client) + "\n"); } } // Displays a message in the incoming text field protected function displayMessage (message:String):void { incomingMessages.appendText(message + "\n"); incomingMessages.scrollV = incomingMessages.maxScrollV; } } } |
Here's the chat's configuration file:
1 2 3 4 5 6 7 | <?xml version="1.0"?> <config> <connections> <connection host="tryunion.com" port="80"/> </connections> <logLevel>DEBUG</logLevel> </config> |