Getting started with Scala, Akka and Sbt: the chat example
October 20th, 2011 | Posted by in SystemScala is a general purpose programming language designed to express common programming patterns in a concise, elegant, and type-safe way. It smoothly integrates features of object-oriented and functional languages. Scala programs run on the Java VM, are byte code compatible with Java so you can make full use of existing Java libraries or existing application code.
Akka is the platform for the next generation event-driven, scalable and fault-tolerant architectures on the JVM.
Sbt is a build tool for Scala and Java projects that aims to do the basics well.
In this tutorial, I will show you how to quickly setup your environment to build and run the akka-sample-chat, a sample that comes with the akka distribution, that implements a simple chat system.
- - Linux with bash
- - A Java runtime version 1.6 or later
- - less than 1 hour
Setting up your local environment
Required environment
charless@linux-bash:~$ cd $WORKDIR
charless@linux-bash:~/Work$ JAVA_HOME=/opt/jdk1.6.0_24/
charless@linux-bash:~/Work$ $JAVA_HOME/bin/java -version
java version "1.6.0_24"
Java(TM) SE Runtime Environment (build 1.6.0_24-b07)
Java HotSpot(TM) Server VM (build 19.1-b02, mixed mode)
Install SBT
SBT is a build tool for Scala projects that aims to do the basics well. It requires Java 1.6 or later.
- Download sbt-launch.jar
- Create the launcher
- Run sbt
Let’s begin by getting the sbt jar:
charless@linux-bash:~$ cd bin
charless@linux-bash:~/bin$ curl -C - -O http://typesafe.artifactoryonline.com/typesafe/ivy-releases/org.scala-tools.sbt/sbt-launch/0.11.0/sbt-launch.jar
(...)
charless@linux-bash:~/bin$ ls
sbt-launch.jar
To create the launcher, create a file named sbt in ~/bin with the content of the following gist
#!/bin/bash
SBT_HOME="$(cd "$(cd "$(dirname "$0")"; pwd -P)"; pwd)"JAVA_OPTS="-Dfile.encoding=UTF8 -Xmx1536M -Xss1M -XX:+CMSClassUnloadingEnabled -XX:MaxPermSize=256m"
java $JAVA_OPTS -jar "$SBT_HOME/sbt-launch.jar" "$@"By using shell commands:
(...)
charless@linux-bash:~/bin$ chmod ugo+rx sbt
Open a new terminal and type the following command to test sbt (this may take some minutes as the first run downloads all dependencies):
charless@linux-bash:/tmp$ ~/bin/sbt
(....)
[info] Set current project to default-8c3933 (in build file:/tmp/)
Ctrl-D
charless@linux-bash:/tmp$
If you have an error like this one
:: problems summary :: :::: WARNINGS [NOT FOUND ] commons-codec#commons-codec;1.2!commons-codec.jar (0ms) ==== Maven2 Local: tried file:///home/charless/.m2/repository/commons-codec/commons-codec/1.2/commons-codec-1.2.jar
Make sure to delete the directory of the uncomplete artifact from your maven local directory (“~/.m2/path/to/uncomplete/artifact”) and the”~/.ivy2″ directory; restart then the “sbt” command.
For the example:
rm -rf /home/charless/.m2/repository/commons-codec/commons-codec/1.2 rm -rf ~/.ivy2
Create the chat application
- Create the project akka-chat-sample
- Run the chat application with
sbt console
By default, sbt works purely by convention. sbt will find the following automatically:
- - Sources in the base directory
- - Sources in
src/main/scalaorsrc/main/java - - Tests in
src/test/scalaorsrc/test/java - - Data files in
src/main/resourcesorsrc/test/resources - - jars in
lib
You can run the project with sbt run or enter the Scala REPL with sbt console. sbt console sets up your project’s classpath so you can try out live Scala examples based on your project’s code.
For our example, we will build the following project in $WORKDIR/akka-sample-chat:
-
build.sbt
-
src/main/scala/ChatServer.scala
First, prepare the project:
charless@linux-bash:~/Work$ cd akka-sample-chat/
charless@linux-bash:~/Work/akka-sample-chat$ mkdir -p src/main/scala
Create a file named build.sbt in the $WORKDIR/akka-sample-chat directory with the content of the following gist
name := "AkkaSampleChat"
version := "1.0"
scalaVersion := "2.9.1"
resolvers += "Typesafe Repository" at "http://repo.typesafe.com/typesafe/releases/"
libraryDependencies ++= Seq( "se.scalablesolutions.akka" % "akka-actor" % "1.2", "se.scalablesolutions.akka" % "akka-stm" % "1.2", "se.scalablesolutions.akka" % "akka-remote" % "1.2")Then, create a file named ChatServer.scala in the $WORKDIR/akka-sample-chat/src/main/scala directory with the content of the following gist
/** * Copyright (C) 2009-2010 Scalable Solutions AB <http://scalablesolutions.se>. */
package sample.chat
import scala.collection.mutable.HashMap
import akka.actor.{Actor, ActorRef} import akka.stm._ import akka.config.Supervision.{OneForOneStrategy,Permanent} import Actor._ import akka.event.EventHandler
/****************************************************************************** Akka Chat Client/Server Sample Application How to run the sample:
1. Fire up two shells. For each of them: - Step down into to the root of the Akka distribution. - Set 'export AKKA_HOME=<root of distribution>. - Run 'sbt console' to start up a REPL (interpreter). 2. In the first REPL you get execute: - scala> import sample.chat._ - scala> import akka.actor.Actor._ - scala> val chatService = actorOf[ChatService].start() 3. In the second REPL you get execute: - scala> import sample.chat._ - scala> ClientRunner.run 4. See the chat simulation run. 5. Run it again to see full speed after first initialization. 6. In the client REPL, or in a new REPL, you can also create your own client - scala> import sample.chat._ - scala> val myClient = new ChatClient("<your name>") - scala> myClient.login - scala> myClient.post("Can I join?") - scala> println("CHAT LOG:nt" + myClient.chatLog.log.mkString("nt"))
That’s it. Have fun.
******************************************************************************/
/** * ChatServer's internal events. */ sealed trait Event case class Login(user: String) extends Event case class Logout(user: String) extends Event case class GetChatLog(from: String) extends Event case class ChatLog(log: List[String]) extends Event case class ChatMessage(from: String, message: String) extends Event
/** * Chat client. */ class ChatClient(val name: String) { val chat = Actor.remote.actorFor("chat:service", "localhost", 2552)
def login = chat ! Login(name) def logout = chat ! Logout(name) def post(message: String) = chat ! ChatMessage(name, name + ": " + message) def chatLog = (chat !! GetChatLog(name)).as[ChatLog].getOrElse(throw new Exception("Couldn't get the chat log from ChatServer")) }
/** * Internal chat client session. */ class Session(user: String, storage: ActorRef) extends Actor { private val loginTime = System.currentTimeMillis private var userLog: List[String] = Nil
EventHandler.info(this, "New session for user [%s] has been created at [%s]".format(user, loginTime))
def receive = { case msg @ ChatMessage(from, message) => userLog ::= message storage ! msg
case msg @ GetChatLog(_) => storage forward msg } }
/** * Abstraction of chat storage holding the chat log. */ trait ChatStorage extends Actor
/** * Memory-backed chat storage implementation. */ class MemoryChatStorage extends ChatStorage { self.lifeCycle = Permanent
private var chatLog = TransactionalVector[Array[Byte]]()
EventHandler.info(this, "Memory-based chat storage is starting up...")
def receive = { case msg @ ChatMessage(from, message) => EventHandler.debug(this, "New chat message [%s]".format(message)) atomic { chatLog + message.getBytes("UTF-8") }
case GetChatLog(_) => val messageList = atomic { chatLog.map(bytes => new String(bytes, "UTF-8")).toList } self.reply(ChatLog(messageList)) }
override def postRestart(reason: Throwable) { chatLog = TransactionalVector() } }
/** * Implements user session management. * <p/> * Uses self-type annotation (this: Actor =>) to declare that it needs to be mixed in with an Actor. */ trait SessionManagement { this: Actor =>
val storage: ActorRef // needs someone to provide the ChatStorage val sessions = new HashMap[String, ActorRef]
protected def sessionManagement: Receive = { case Login(username) => EventHandler.info(this, "User [%s] has logged in".format(username)) val session = actorOf(new Session(username, storage)) session.start() sessions += (username -> session)
case Logout(username) => EventHandler.info(this, "User [%s] has logged out".format(username)) val session = sessions(username) session.stop() sessions -= username }
protected def shutdownSessions() { sessions.foreach { case (_, session) => session.stop() } } }
/** * Implements chat management, e.g. chat message dispatch. * <p/> * Uses self-type annotation (this: Actor =>) to declare that it needs to be mixed in with an Actor. */ trait ChatManagement { this: Actor => val sessions: HashMap[String, ActorRef] // needs someone to provide the Session map
protected def chatManagement: Receive = { case msg @ ChatMessage(from, _) => getSession(from).foreach(_ ! msg) case msg @ GetChatLog(from) => getSession(from).foreach(_ forward msg) }
private def getSession(from: String) : Option[ActorRef] = { if (sessions.contains(from)) Some(sessions(from)) else { EventHandler.info(this, "Session expired for %s".format(from)) None } } }
/** * Creates and links a MemoryChatStorage. */ trait MemoryChatStorageFactory { this: Actor => val storage = Actor.actorOf[MemoryChatStorage] this.self.startLink(storage) // starts and links ChatStorage }
/** * Chat server. Manages sessions and redirects all other messages to the Session for the client. */ trait ChatServer extends Actor { self.faultHandler = OneForOneStrategy(List(classOf[Exception]),5, 5000) val storage: ActorRef
EventHandler.info(this, "Chat server is starting up...")
// actor message handler def receive: Receive = sessionManagement orElse chatManagement
// abstract methods to be defined somewhere else protected def chatManagement: Receive protected def sessionManagement: Receive protected def shutdownSessions()
override def postStop() { EventHandler.info(this, "Chat server is shutting down...") shutdownSessions() self.unlink(storage) storage.stop() } }
/** * Class encapsulating the full Chat Service. * Start service by invoking: * <pre> * val chatService = Actor.actorOf[ChatService].start() * </pre> */ class ChatService extends ChatServer with SessionManagement with ChatManagement with MemoryChatStorageFactory { override def preStart() { remote.start("localhost", 2552); remote.register("chat:service", self) //Register the actor with the specified service id } }
/** * Test runner starting ChatService. */ object ServerRunner {
def main(args: Array[String]) { ServerRunner.run() }
def run() { actorOf[ChatService].start() } }
/** * Test runner emulating a chat session. */ object ClientRunner {
def main(args: Array[String]) { ClientRunner.run() }
def run() {
val client1 = new ChatClient("jonas") client1.login val client2 = new ChatClient("patrik") client2.login
client1.post("Hi there") println("CHAT LOG:nt" + client1.chatLog.log.mkString("nt"))
client2.post("Hello") println("CHAT LOG:nt" + client2.chatLog.log.mkString("nt"))
client1.post("Hi again") println("CHAT LOG:nt" + client1.chatLog.log.mkString("nt"))
client1.logout client2.logout } }
By using shell commands:
(...)
charless@linux-bash:~/Work/akka-sample-chat$ cd src/main/scala/
charless@linux-bash:~/Work/akka-sample-chat/src/main/scala$ curl -C - -O https://raw.github.com/gist/1299742/db527df6babc07827293f5ffefcd57a47ea8a39b/ChatServer.scala
(...)
charless@linux-bash:~/Work/akka-sample-chat/src/main/scala$ ls
ChatServer.scala
charless@linux-bash:~/Work/akka-sample-chat/src/main/scala$ cd ../../..
charless@linux-bash:~/Work/akka-sample-chat$
Launch the sbt console from the $WORKDIR/akka-sample-chat project directory and type the following commands:
import sample.chat._import akka.actor.Actor._val chatService = actorOf[ChatService].start()
To do so, open a new terminal and type the following:
charless@linux-bash:~$ cd $PROJECTDIR
charless@linux-bash:~/Work/akka-sample-chat$ ~/bin/sbt console
[info] Set current project to AkkaSampleChat (in build file:/home/charless/Work/akka-sample-chat/)
(...)
Welcome to Scala version 2.9.1.final (OpenJDK Server VM, Java 1.6.0_22).
scala› import sample.chat._
import sample.chat._
scala› import akka.actor.Actor._
import akka.actor.Actor._
scala› val chatService = actorOf[ChatService].start()
[INFO] [10/19/11 11:45 PM] [run-main] [ChatService] Chat server is starting up...
(...)
[INFO] [10/19/11 11:45 PM] [run-main] [MemoryChatStorage] Memory-based chat storage is starting up...
(...)
scala›
In another terminal, launch the sbt console form the $WORKDIR/akka-sample-chat project directory and run the chat simulation run:
charless@linux-bash:~$ cd $PROJECTDIR
charless@linux-bash:~/Work/akka-sample-chat$ ~/bin/sbt console
[info] Set current project to AkkaSampleChat (in build file:/home/charless/Work/akka-sample-chat/)
(...)
Welcome to Scala version 2.9.1.final (OpenJDK Server VM, Java 1.6.0_22).
scala› import sample.chat._
import sample.chat._
scala› ClientRunner.run
[GENERIC] [10/19/11 11:57 PM] [RemoteClientStarted(akka.remote.netty.NettyRemoteSupport@acd5d4,localhost/127.0.0.1:2552)]
[GENERIC] [10/19/11 11:57 PM] [RemoteClientConnected(akka.remote.netty.NettyRemoteSupport@acd5d4,localhost/127.0.0.1:2552)]
CHAT LOG:
jonas: Hi there
CHAT LOG:
jonas: Hi there
patrik: Hello
CHAT LOG:
jonas: Hi there
patrik: Hello
jonas: Hi again
scala›
Run it again to see full speed after first initialization, and the chat log growing up.
Conclusion
In this article, we have seen how to install sbt and use it to build and test the akka-sample-chat application.
In a next article, we will see how to work on an akka project by using Eclipse.
Share and Enjoy
You can follow any responses to this entry through the RSS 2.0 You can leave a response, or trackback.
