Menu
Home
Log in / Register
 
Home arrow Computer Science arrow Building Applications with Scala
Source

The chat protocol

Now we will need to define our protocol. For this functionality, we will need three Actors. The Actors that we create will be as follows:

  • ChatRoom: This will have a reference for all users in the chat room
  • ChatUser: This will have one instance per user (active browser)
  • ChatBotAdmin: This simple Bot Admin will provide stats about the chat room

ChatUserActor will need to join JoinChatRoom object in order to start chatting. ChatUserActor will also need to send messages to ChatMessage class to the ChatRoomActor that will broadcast messages to all users. The ChatBotAdmin will get a report from GetStats object from ChatRoomActor.

Let's start coding this protocol. First, we will need to define the messages that will be exchanged between these Actors, as shown in the following piece of code:

package actors

case class ChatMessage(name:String,text: String)

case class Stats(users:Set[String])

object JoinChatRoom

object Tick

object GetStats

As you can see here, we have a ChatMessage class with a name and a text. This will be the message each user will send on the chat. Then, we will have a stats class, which has a set of users-this will be all the users logged into the chat application.

Finally, we have some action messages, such as JoinChatRoom, Tick, and GetStats. So, JoinChatRoom will be sent by ChatUserActor to ChatRoomActor in order to join the chat. Tick will be a scheduled message that will happen from time to time in order to make ChatBotAdmin send stats about the chat room to all logged users. GetStats is the message that ChatBotAdminActor will send to ChatRoomActor in order to get information about who is in the room.

Let's code our three actors now.

The ChatRoomActor.scala file should look something like this:

package actors

import akka.actor.Props

import akka.actor.Terminated

import akka.actor.ActorLogging

import akka.event.LoggingReceive

import akka.actor.Actor

import akka.actor.ActorRef

import play.libs.Akka

import akka.actor.ActorSystem

class ChatRoomActor extends Actor with ActorLogging { var users = Set[ActorRef]() def receive = LoggingReceive { case msg: ChatMessage => users foreach { _ ! msg } case JoinChatRoom => users += sender context watch sender case GetStats =>

val stats:String = "online users[" + users.size + "] - users["

+ users.map( a => a.hashCode().mkString("|") + "]"

sender ! stats

case Terminated(user) =>

users -= user

}

}

object ChatRoomActor {

var room:ActorRef = null def apply(system:ActorSystem) = { this.synchronized {

if (room==null) room = system.actorOf(Props[ChatRoomActor]) room

}

}

}

ChatRoomActor has a var called users, which is a set of ActorRef. ActorRef is a generic reference to any actor. We have the receive function with three cases: ChatMessage, JoinChatRoom, and GetStats.

A JoinChatRoom will be sent by the ChatUserActor method in order to join the room. As you can see, we are getting the ActorRef method from the sender Actor using the sender() function, and we are adding this reference to the set of users. In this way, the set of ActorRef represents the online logged-in users in the chat room right now.

The other case is with the ChatMessage method. Basically, we will broadcast the message to all users in the chat. We do this because we have the reference for all actors in users. Then, we will call the foreach function in order to iterate all users one by one, and then we will send the message using FireAndForget "!" to each user Actor represented by the operator underscore _.

The GetStats case creates a string with all chat room stats. For now, the stats are just the number of online users, which is computed by calling the size function on the users object. We are also showing all the hash codes that identify all Actors logged in, just for fun.

That's our ChatRoomActor implementation. As you can see, it is hard to talk about one Actor without describing the other, as the protocol will always be kind of coupled. You might also be wondering why we have a companion object for the ChatRoomActor method.

This object is to provide an easy way to create Actor instances. We are creating a single room for our design; we don't want to have multiple chat rooms, so that's why we will need to control the creation of the room Actor.

If the room is null, we will create a new room; otherwise, we will return the cached instance of the room that we already got in the memory. We will need an instance of the Actor system in order to create actors, so that's why we are receiving the system on the apply function. The apply function will be called when someone writes a code like

ChatRoomActor(mySystem).

Now, let's move to the ChatUserActor implementation.

The ChatUserActor.scala file should look something like this:

package actors import akka.actor.ActorRef import akka.actor.Actor import akka.actor.ActorLogging import akka.event.LoggingReceive import akka.actor.ActorSystem import akka.actor.Props

class ChatUserActor(room:ActorRef, out:ActorRef) extends Actor with ActorLogging {

override def preStart() = { room ! JoinChatRoom

}

def receive = LoggingReceive {

case ChatMessage(name, text) if sender == room =>

val result:String = name + ":" + text

out ! result

case (text:String) =>

room ! ChatMessage(text.split(":")(0), text.split(":")(1)) case other =>

log.error("issue - not expected: " + other)

}

}

object ChatUserActor {

def props(system:ActorSystem)(out:ActorRef) = Props(new ChatUserActor(ChatRoomActor(system), out))

}

This Actor is a little bit easier than the previous one. ChatUserActor receives, as a parameter, the room actor reference and also an out actor. The room will be an instance of the room that the user will use to communicate with other users. The ActorRef method called out is the Play framework Actor responsible for sending the answer back to the controllers and UI.

We pretty much just have two cases: one where we receive a ChatMessage and the other is the ChatUserActors method in the chat room. So, we will just need to send back to the UI using the out Actor. That's why there is a fire and forget message for the out Actor with a result. Using a new Actor model can be dangerous, please read more at

http://doc.akka.io/docs/akka/current/scala/actors.html.

There is another case that just receives a string that will be the message from that Actor itself. Remember that each Actor represents a user and a browser full of duplex connections via WebSockets. Don't worry about WebSockets now; we will cover it in more detail later in this chapter.

For this case function, we are sending the ChatMessage method to the room. We will split the messages in two parts: the username and the text, which is split by :.

Here, we also have a companion object for the sake of good practice. So, you can call ChatUserActor, passing the Actor system and a curried parameter for the out actor.

Now, we will move to the last Actor: the Bot Admin Actor, which should look something like this:

package actors import akka.actor.ActorRef import akka.actor.Actor import akka.actor.ActorLogging import akka.event.LoggingReceive import akka.actor.ActorSystem import akka.actor.Props import scala.concurrent.duration._

class ChatBotAdminActor(system:ActorSystem) extends Actor with ActorLogging {

import play.api.libs.concurrent.Execution. Implicits.defaultContext

val room:ActorRef = ChatRoomActor(system)

val cancellable = system.scheduler.schedule(0 seconds,

10 seconds, self , Tick) override def preStart() = { room ! JoinChatRoom

}

def receive = LoggingReceive { case ChatMessage(name, text) => Unit

case (text:String) => room ! ChatMessage(text.split(":")(0),

text.split(":")(1))

case Tick =>

val response:String = "AdminBot:" + ActorHelper.get (GetStats, room) sender() ! response case other =>

log.error("issue - not expected: " + other)

}

}

object ChatBotAdminActor { var bot:ActorRef = null def apply(system:ActorSystem) = { this.synchronized {

if (bot==null) bot = system.actorOf(Props (new ChatBotAdminActor(system))) bot

}

}

}

As you can see, this Actor receives the reference of the chat room as a parameter. Using the Actor system, it gets the reference of the chat room Actor. This Actor receives an ActorSystem message by now.

Using the Actor system variable called system, we will also schedule a Tick for this Actor for every ten seconds. This time, the window interval will be the time in which the bot will notify the chat room about the current status.

We will also override the preStart function. Akka will call this function when the Actor is created on the actor system. This implementation will send a message to the room, which is

JoinChatRoom.

Like all Actors, there is the receive function implementation. First case with ChatMessage is returning Unit. If you want to make this bot respond to people, remove Unit and write the proper Scala code as you wish.

In the second case, we will have the String message that will be sent to the chat room. Finally, after this case, we will have the Tick method, which will appear every ten seconds. So, we will use the ActorHelper to get the stats from the room, and then we will send a string message with the information about the room. This will trigger the second case and broadcast the message to the whole room.

Finally, we have a companion object. We don't want to have two instances of the bot, which is why we will control this object creation by design. We're done with the actors implementations. Next, we will need to work a new controller for the chat actors.

 
Source
Found a mistake? Please highlight the word and press Shift + Enter  
< Prev   CONTENTS   Next >
 
Subjects
Accounting
Business & Finance
Communication
Computer Science
Economics
Education
Engineering
Environment
Geography
Health
History
Language & Literature
Law
Management
Marketing
Mathematics
Political science
Philosophy
Psychology
Religion
Sociology
Travel