Akka框架:构建并发、分布式消息驱动应用的利器
1. Akka框架简介
Akka(http://akka.io/)是一个用于在Scala、Java和.NET中构建并发、分布式和弹性消息驱动应用程序的框架。使用Akka构建应用程序具有以下优势:
-
高性能
:在普通硬件上,Akka每秒可处理多达5000万条消息,每GB内存可容纳约250万个Actor。
-
设计弹性
:Akka系统的本地和远程Actor具有自我修复特性。
-
分布式与弹性扩展
:Akka提供了集群、负载均衡、分区和分片等机制,可按需扩展或缩减Actor。
Akka框架为并发、异步和分布式编程提供了良好的抽象,如Actor、流和Future。许多知名公司都在生产环境中成功使用了Akka,如BBC、亚马逊、eBay、思科、《卫报》、暴雪、Gilt、惠普、汇丰银行、Netflix等。Akka是一个真正的响应式框架,因为在向Actor发送和接收消息时,一切都是无锁、非阻塞IO和异步的。
2. Actor模型介绍
并发编程的关键是避免共享可变状态。共享状态通常需要锁和同步,这会降低代码的并发性并增加复杂性。而Actor不共享任何状态,它们有自己的内部状态,但不会共享这些内部状态。
Actor具有位置透明性,它们可以在本地或远程系统以及集群中运行,也可以混合使用本地和远程Actor,这非常适合扩展性,并且能完美融入云环境。Actor可以在任何地方运行,包括本地机器、云、裸金属数据中心和Linux容器。
3. 什么是Actor
Actor可以替代线程、回调监听器、单例服务、企业Java Bean(EJB)、路由器、负载均衡器或池以及有限状态机(FSM)。Actor模型的概念并不新鲜,它由Carl Hewitt在1973年创建。该模型在电信行业的可靠技术(如Erlang)中被广泛使用,Erlang和Actor模型在爱立信和Facebook等公司取得了巨大成功。
Actor的工作方式简单,主要包括以下几个方面:
-
代码组织单元
:处理、存储和通信。
-
管理内部状态
。
-
拥有邮箱
。
-
使用消息与其他Actor通信
。
-
可以在运行时改变行为
。
4. 消息交换和邮箱
Actor之间通过消息进行通信,有两种模式:“ask”和“fire and forget”。这两种方法都是异步和非阻塞IO。当一个Actor向另一个Actor发送消息时,它实际上是将消息发送到目标Actor的邮箱。
消息按时间顺序在Actor邮箱中排队。Akka中有不同的邮箱实现,默认是基于先进先出(FIFO)的。不过,如果需要,也可以更改邮箱算法,更多详细信息可查看官方文档(http://doc.akka.io/docs/akka/2.4.9/scala/mailboxes.html#mailboxes-scala)。Actor存在于Actor系统中,一个集群中可以有多个Actor系统。
Akka将Actor状态封装在邮箱中,并将其与Actor行为解耦。Actor行为是Actor内部的代码。需要将Actor和Akka视为一种协议,即需要定义有多少个Actor以及每个Actor在代码、职责和行为方面的具体操作。Actor系统包含Actor和监管者,监管者是Akka提供容错和弹性的机制之一,它们负责管理Actor实例,可根据需要重启、终止或创建更多Actor。
Actor模型非常适合并发和可扩展性,但和计算机科学中的其他事物一样,也有一些权衡和缺点。例如,使用Actor需要新的思维方式和不同的思考方法。一旦确定了协议,可能很难在协议之外重用Actor。一般来说,与面向对象的类或函数式编程中的函数相比,Actor更难组合。
5. 使用Akka编码Actor
以下是使用Akka框架和Scala编写的Actor代码示例:
import akka.actor._
case object HelloMessage
class HelloWorldActor extends Actor {
def receive = {
case HelloMessage => sender() ! "Hello World"
case a:Any => sender() ! "I don't know: " + a + " - Sorry!"
}
}
object SimpleActorMainApp extends App{
val system = ActorSystem("SimpleActorSystem")
val actor = system.actorOf(Props[HelloWorldActor])
import scala.concurrent.duration._
import akka.util.Timeout
import akka.pattern.ask
import scala.concurrent.Await
implicit val timeout = Timeout(20 seconds)
val future = actor ? HelloMessage
val result = Await.result(future,
timeout.duration).asInstanceOf[String]
println("Actor says: " + result )
val future2 = actor ? "Cobol"
val result2 = Await.result(future2,
timeout.duration).asInstanceOf[String]
println("Actor says: " + result2 )
system.terminate()
}
如果在控制台使用sbt运行这段Akka代码,会看到类似以下的输出:
$ sbt run
下面详细分析这段代码。定义了一个名为
HelloWorldActor
的Scala类,为了使该类成为一个Actor,需要继承
Actor
。Actor默认是响应式的,即它们等待接收消息并对其做出反应。需要在事件循环中编写行为代码,在Akka中,这是通过在Scala中使用模式匹配器编写
receive
函数来实现的。
模式匹配器将定义Actor可以执行的操作,需要编写所有希望该Actor处理的可能消息类型。通常在Akka中使用Scala对象作为消息,但实际上可以传递几乎所有类型的消息,甚至可以发送带有参数的样例类。
有了协议(即Actor和它们可以交换的消息)后,需要创建一个Actor系统并启动应用程序。使用
ActorSystem
对象创建Actor系统,Actor系统需要有一个名称,该名称可以是任何包含字母[a - z, A - Z, 0 - 9]且不以’-‘或’_’开头的字符串。
创建系统后,可以使用
actorOf
函数创建Actor,需要使用一个特殊的对象
Props
并传递Actor类。这是因为Akka管理Actor状态,不应该自己尝试管理Actor实例,否则可能会破坏引用透明性,导致代码无法正常工作。
在这段代码中,使用了“ask”模式向Actor发送消息并希望知道Actor的返回结果。虽然Akka的所有操作都是异步和非阻塞的,但有时需要立即得到答案,这时就需要阻塞。为了立即得到答案,需要定义一个超时时间并使用
Await
对象。当使用
?
(“ask”模式)向Actor发送消息时,Akka会返回一个
Future
,可以将
Future
和超时时间传递给
Await
,如果在超时时间内得到答案,就可以获得Actor的响应。
需要注意的是,这里进行了阻塞操作,因为是在Actor系统外部希望立即得到答案。当Actor在Actor系统内部与其他Actor通信时,永远不应该阻塞,所以要谨慎使用
Await
。
代码中
Actor
的
receive
函数中的
sender()
方法用于获取发送消息的Actor的引用,通过
sender() !
方法可以将答案发送回调用者。
sender()
函数是Akka用于处理向其他Actor或函数调用者发送响应消息的抽象。
“ask”模式是向Actor发送消息的一种方式,还有另一种模式是“FireAndForget”(“!”)。“FireAndForget”会发送消息但不会阻塞等待答案,即返回
Unit
。以下是使用“FireAndForget”消息交换的代码示例:
import akka.actor._
object Message
class PrinterActor extends Actor {
def receive = {
case a:Any =>
println("Print: " + a)
}
}
object FireAndForgetActorMainApp extends App{
val system = ActorSystem("SimpleActorSystem")
val actor = system.actorOf(Props[PrinterActor])
val voidReturn = actor ! Message
println("Actor says: " + voidReturn )
system.terminate()
}
如果使用
$ sbt run
运行这段代码,会看到如下输出。这里有一个
PrinterActor
方法,它可以接受几乎任何类型的消息并在控制台打印。然后创建一个Actor系统,使用“FireAndForget”模式(即“!”)向Actor发送消息,会收到
Unit
,最后使用
terminate
选项等待Actor系统关闭。
6. Actor路由
Akka提供路由功能,从业务角度来看,这很有用,因为可以根据业务逻辑和行为将消息路由到正确的Actor。在架构方面,可以将其用作负载均衡,并将消息路由到更多Actor以实现容错和可扩展性。
Akka提供了以下几种路由选项:
| 路由选项 | 说明 |
| ---- | ---- |
| RoundRobin | 随机将消息发送到池中的不同Actor |
| SmallestMailbox | 将消息发送到消息较少的Actor |
| Consistent Hashing | 根据哈希ID对Actor进行分区 |
| ScatterGather | 将消息发送给所有Actor,第一个回复的Actor获胜 |
| TailChopping | 随机选择一个路由发送消息,如果在一秒内没有收到回复,则选择一个新的路由并再次发送,以此类推 |
以下是一个使用路由的代码示例:
import akka.actor._
import akka.routing.RoundRobinPool
class ActorUpperCasePrinter extends Actor {
def receive = {
case s:Any =>
println("Msg: " + s.toString().toUpperCase() + " - " +
self.path)
}
}
object RoutingActorApp extends App {
val system = ActorSystem("SimpleActorSystem")
val actor:ActorRef = system.actorOf(
RoundRobinPool(5).props(Props[ActorUpperCasePrinter]),name =
"actor")
try{
actor ! "works 1"
actor ! "works 2"
actor ! "works 3"
actor ! "works 4"
actor ! "works 5"
actor ! "works 6"
}catch{
case e:RuntimeException => println(e.getMessage())
}
system.terminate()
}
如果在sbt中使用
$ sbt run
运行这段代码,会得到相应的输出。这里有一个
ActorUpperCasePrinter
函数,它会打印接收到的任何消息,并将其转换为大写,最后还会打印Actor的地址(
self.path
)。Actor以类似文件系统的层次结构组织。
有多种使用Akka的方式,它支持代码或配置(
application.conf
文件)。这里创建了一个具有五个路由的轮询池Actor,并将目标Actor(即
PrinterActor
)传递给路由器。可以看到,当使用“FireAndForget”模式发送消息时,每个消息都会被发送到不同的Actor。
7. 持久化
Akka默认在内存中工作,但也可以使用持久化功能。虽然持久化在Akka中仍处于实验阶段,但它是稳定的。在生产环境中,可以使用高级持久化插件,如Apache Cassandra。为了开发和学习目的,这里将在文件系统中使用Google leveldb。Akka提供了多种持久化选项,如视图和持久化Actor。
以下是一个使用Google leveldb和文件系统的持久化Actor示例:
import akka.actor._
import akka.persistence._
import scala.concurrent.duration._
class PersistenceActor extends PersistentActor{
override def persistenceId = "sample-id-1"
var state:String = "myState"
var count = 0
def receiveCommand: Receive = {
case payload: String =>
println(s"PersistenceActor received ${payload} (nr =
${count})")
persist(payload + count) { evt =>
count += 1
}
}
def receiveRecover: Receive = {
case _: String =>
println("recover...")
count += 1
}
}
object PersistentViewsApp extends App {
val system = ActorSystem("SimpleActorSystem")
val persistentActor =
system.actorOf(Props(classOf[PersistenceActor]))
import system.dispatcher
system.scheduler.schedule(Duration.Zero, 2.seconds,
persistentActor, "scheduled")
}
如果使用
$ sbt run
运行这段代码,然后停止并再次运行,会发现每次停止和启动时数据都会被存储和恢复。
要使Actor具有持久化支持,需要继承
PersistentActor
并提供一个
persistenceID
。这里需要实现两个
receive
函数,一个用于命令(即消息),另一个用于恢复。当Actor接收到消息时,命令的
receive
循环将被激活;当Actor启动时,恢复的
receive
函数将被激活,它会从数据库中读取持久化数据。
这个Actor有一个计数器,用于统计接收到的每条消息,并在控制台打印收到的每条消息。要使用此功能,还需要配置
application.conf
文件,该文件应如下所示:
akka {
system = "SimpleActorSystem"
remote {
log-remote-lifecycle-events = off
netty.tcp {
hostname = "127.0.0.1"
port = 0
}
}
}
akka.cluster.metrics.enabled=off
akka.persistence.journal.plugin =
"akka.persistence.journal.leveldb"
akka.persistence.snapshot-store.plugin =
"akka.persistence.snapshot-store.local"
akka.persistence.journal.leveldb.dir = "target/persistence/journal"
akka.persistence.snapshot-store.local.dir =
"target/persistence/snapshots"
# DO NOT USE THIS IN PRODUCTION !!!
# See also https://github.com/typesafehub/activator/issues/287
akka.persistence.journal.leveldb.native = false
这里定义了一个简单的Akka系统(本地模式),并为Google leveldb配置了持久化。需要提供持久化的路径,并且该路径必须在操作系统中存在。
由于使用了额外的功能,还需要更改
build.sbt
文件以导入所需的所有JAR包,
build.sbt
文件应如下所示:
// rest of the build.sbt file ...
val akkaVersion = "2.4.9"
libraryDependencies += "com.typesafe.akka" %% "akka-actor" %
akkaVersion
libraryDependencies += "com.typesafe.akka" %% "akka-kernel" %
akkaVersion
libraryDependencies += "com.typesafe.akka" %% "akka-remote" %
akkaVersion
libraryDependencies += "com.typesafe.akka" %% "akka-cluster" %
akkaVersion
libraryDependencies += "com.typesafe.akka" %% "akka-contrib" %
akkaVersion
libraryDependencies += "com.typesafe.akka" %% "akka-persistence" %
akkaVersion
libraryDependencies += "org.iq80.leveldb" % "leveldb" % "0.7"
libraryDependencies += "org.iq80.leveldb" % "leveldb-api" % "0.7"
libraryDependencies += "org.fusesource.leveldbjni" % "leveldbjni" %
"1.8"
libraryDependencies += "org.fusesource.leveldbjni" % "leveldbjni-
linux64" % "1.8"
libraryDependencies += "org.fusesource" % "sigar" % "1.6.4"
libraryDependencies += "org.scalatest" % "scalatest_2.11" % "2.2.6"
这样就可以持久化Actor的状态了。
8. 创建聊天应用程序
在对Akka有了更深入的了解后,将继续开发应用程序。Akka与Play框架有很好的集成,现在将结合Akka和Play框架使用Actor来构建一个简单的聊天功能。需要更改代码以添加新的UI,并使用Akka测试工具包测试Actor。
Play框架已经在类路径中包含了Akka,所以不需要担心这一点。但需要在
build.sbt
文件中添加Akka测试工具包依赖,以使这些类出现在类路径中。
build.sbt
文件应如下所示:
// rest of the build.stb ...
libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-testkit" % "2.4.4" % Test,
// rest of the deps ...
)
// rest of the build.stb ...
然后在控制台中输入
$ activator
、
$ reload
和
$ compile
,这将强制sbt下载新的依赖项。
接下来,需要创建一个名为
Actors
的包,该包应位于
ReactiveWebStore/app/
目录下。首先创建一个
ActorHelper
工具对象,用于实现前面提到的“ask”模式的通用函数,它是一个Actor辅助通用“ask”模式工具。
ActorHelper.scala
文件应如下所示:
package actors
object ActorHelper {
import play.api.libs.concurrent.
Execution.Implicits.defaultContext
import scala.concurrent.duration._
import akka.pattern.ask
import akka.actor.ActorRef
import akka.util.Timeout
import scala.concurrent.Future
import scala.concurrent.Await
def get(msg:Any,actor:ActorRef):String = {
implicit val timeout = Timeout(5 seconds)
val result = (actor ? msg).mapTo[String].map { result =>
result.toString }
Await.result(result, 5.seconds)
}
}
ActorHelper
只有一个
get
函数,该函数可以从任何Actor获取任何消息的答案。这里设置了一个5秒的超时时间,如果在这个时间内没有得到结果,将抛出异常。在代码中,还将Actor的结果映射为
String
,通过调用结果
Future
的
toString
函数实现。虽然代码不多,但有很多导入语句,这使得代码更简洁,并且可以用更少的代码和导入语句从Actor获取答案。
9. 聊天协议
为了实现聊天功能,需要定义一个协议,涉及三个Actor:
-
ChatRoom
:持有聊天室中所有用户的引用。
-
ChatUser
:每个用户(活跃浏览器)有一个实例。
-
ChatBotAdmin
:这个简单的机器人管理员将提供聊天室的统计信息。
ChatUserActor
需要发送
JoinChatRoom
对象以加入聊天,还需要将
ChatMessage
类的消息发送给
ChatRoomActor
,后者将消息广播给所有用户。
ChatBotAdmin
将从
ChatRoomActor
获取
GetStats
对象的报告。
首先,需要定义这些Actor之间交换的消息,代码如下:
package actors
case class ChatMessage(name:String,text: String)
case class Stats(users:Set[String])
object JoinChatRoom
object Tick
object GetStats
这里有一个
ChatMessage
类,包含用户名和文本,这是每个用户在聊天时发送的消息。还有一个
Stats
类,包含一组用户,即所有登录到聊天应用程序的用户。最后,有一些操作消息,如
JoinChatRoom
、
Tick
和
GetStats
。
JoinChatRoom
由
ChatUserActor
发送给
ChatRoomActor
以加入聊天;
Tick
是一个定时消息,用于让
ChatBotAdmin
定期向所有登录用户通知聊天室的当前状态;
GetStats
是
ChatBotAdminActor
发送给
ChatRoomActor
以获取聊天室中在线用户信息的消息。
接下来是三个Actor的代码实现:
ChatRoomActor.scala
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
有一个
users
变量,它是一个
ActorRef
的集合。
receive
函数有四个匹配情况:
-
ChatMessage
:将消息广播给所有在线用户。
-
JoinChatRoom
:将发送者的
ActorRef
添加到
users
集合中,并监视发送者。
-
GetStats
:创建一个包含聊天室统计信息的字符串并发送给请求者。
-
Terminated
:当一个用户离开时,将其从
users
集合中移除。
ChatRoomActor
的伴生对象用于提供一种简单的方式来创建Actor实例,确保只创建一个聊天室实例。
ChatUserActor.scala
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))
}
ChatUserActor
接收
room
和
out
两个
ActorRef
作为参数。
room
是用户用于与其他用户通信的聊天室实例,
out
是Play框架中负责将答案发送回控制器和UI的Actor。
receive
函数有三个匹配情况:
-
ChatMessage
:当接收到来自聊天室的消息时,将消息格式化为字符串并发送给
out
Actor。
-
String
:将接收到的字符串消息拆分为用户名和文本,然后发送给聊天室。
-
other
:记录意外消息的错误信息。
ChatUserActor
的伴生对象提供了一个
props
函数,用于创建
ChatUserActor
的
Props
对象。
ChatBotAdminActor.scala
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
}
}
}
ChatBotAdminActor
接收
ActorSystem
作为参数,通过它获取聊天室的引用。使用
system.scheduler
为该Actor每10秒安排一个
Tick
消息,这个时间间隔是机器人通知聊天室当前状态的时间。
receive
函数有四个匹配情况:
-
ChatMessage
:目前返回
Unit
,如果需要让机器人回复用户,可以修改此处的代码。
-
String
:将接收到的字符串消息拆分为用户名和文本,然后发送给聊天室。
-
Tick
:使用
ActorHelper
从聊天室获取统计信息,并将包含信息的字符串消息发送给发送者。
-
other
:记录意外消息的错误信息。
ChatBotAdminActor
的伴生对象用于确保只创建一个机器人实例。
至此,完成了Actor的实现,接下来需要为聊天Actor开发一个新的控制器。
下面是一个简单的mermaid流程图,展示了聊天协议中消息的流动:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([ChatUserActor]):::startend -->|JoinChatRoom| B(ChatRoomActor):::process
A -->|ChatMessage| B
C(ChatBotAdminActor):::process -->|GetStats| B
B -->|Broadcast ChatMessage| A
B -->|Stats| C
C -->|Tick| C
C -->|Stats Broadcast| A
通过以上步骤,我们可以利用Akka和Play框架构建一个简单的聊天应用程序,并且利用Actor模型的特性实现并发、分布式的消息处理和管理。
Akka框架:构建并发、分布式消息驱动应用的利器
10. 开发新控制器
为了让聊天功能完整可用,需要开发一个新的控制器来处理用户请求和与Actor进行交互。以下是具体的实现步骤和代码示例。
首先,创建一个新的控制器类,命名为
ChatController
,该类位于
controllers
包下。
ChatController.scala
文件的代码如下:
package controllers
import javax.inject._
import akka.actor._
import actors._
import play.api.mvc._
@Singleton
class ChatController @Inject()(system: ActorSystem, cc: ControllerComponents) extends AbstractController(cc) {
val chatRoom = ChatRoomActor(system)
def chatSocket = WebSocket.accept[String, String] { request =>
ActorFlow.actorRef { out =>
ChatUserActor.props(system)(out)
}
}
def getStats = Action {
val stats = ActorHelper.get(GetStats, chatRoom)
Ok(stats)
}
}
在上述代码中:
-
ChatController
类使用
@Singleton
注解确保只有一个实例。它接收
ActorSystem
和
ControllerComponents
作为依赖注入。
-
chatRoom
变量通过
ChatRoomActor(system)
获取聊天室的引用。
-
chatSocket
方法用于处理WebSocket连接。当有用户连接时,会创建一个
ChatUserActor
实例,并将其与WebSocket的输出流关联起来。
-
getStats
方法用于获取聊天室的统计信息。它使用
ActorHelper
的
get
方法从
chatRoom
获取统计信息,并以
Ok
响应返回给客户端。
11. 配置路由
接下来,需要在
routes
文件中配置路由,使得客户端可以访问控制器的方法。
routes
文件的配置如下:
# Chat routes
GET /chat/stats controllers.ChatController.getStats
GET /chat/socket controllers.ChatController.chatSocket
这里定义了两个路由:
-
/chat/stats
:用于获取聊天室的统计信息,会调用
ChatController
的
getStats
方法。
-
/chat/socket
:用于建立WebSocket连接,会调用
ChatController
的
chatSocket
方法。
12. 前端UI实现
为了让用户能够方便地使用聊天功能,需要实现一个简单的前端UI。以下是一个使用HTML、JavaScript和WebSocket的示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Simple Chat</title>
</head>
<body>
<h1>Simple Chat</h1>
<div id="messages"></div>
<input type="text" id="messageInput" placeholder="Type your message">
<button onclick="sendMessage()">Send</button>
<button onclick="getStats()">Get Stats</button>
<script>
const socket = new WebSocket('ws://localhost:9000/chat/socket');
const messagesDiv = document.getElementById('messages');
const messageInput = document.getElementById('messageInput');
socket.onmessage = function(event) {
const message = document.createElement('p');
message.textContent = event.data;
messagesDiv.appendChild(message);
};
function sendMessage() {
const message = messageInput.value;
if (message) {
socket.send(message);
messageInput.value = '';
}
}
function getStats() {
fetch('/chat/stats')
.then(response => response.text())
.then(data => {
const statsMessage = document.createElement('p');
statsMessage.textContent = 'Stats: ' + data;
messagesDiv.appendChild(statsMessage);
});
}
</script>
</body>
</html>
在这个前端UI中:
- 使用
WebSocket
对象与后端的
/chat/socket
路由建立连接。
-
onmessage
事件处理函数用于接收服务器发送的消息,并将其显示在页面上。
-
sendMessage
函数用于将用户输入的消息发送到服务器。
-
getStats
函数使用
fetch
API调用
/chat/stats
路由,获取聊天室的统计信息并显示在页面上。
13. 测试Actor
为了确保Actor的正确性,可以使用Akka测试工具包进行测试。以下是一个简单的测试示例,测试
ChatRoomActor
的功能:
package actors
import akka.actor._
import akka.testkit._
import org.scalatest._
class ChatRoomActorSpec extends TestKit(ActorSystem("TestSystem"))
with WordSpecLike
with MustMatchers
with BeforeAndAfterAll {
override def afterAll(): Unit = {
TestKit.shutdownActorSystem(system)
}
"A ChatRoomActor" must {
"add a user when JoinChatRoom message is received" in {
val chatRoom = system.actorOf(Props[ChatRoomActor])
val testProbe = TestProbe()
testProbe.send(chatRoom, JoinChatRoom)
testProbe.send(chatRoom, GetStats)
testProbe.expectMsgPF() {
case stats: String if stats.contains("online users[1]") => ()
}
}
"broadcast a chat message to all users" in {
val chatRoom = system.actorOf(Props[ChatRoomActor])
val testProbe1 = TestProbe()
val testProbe2 = TestProbe()
testProbe1.send(chatRoom, JoinChatRoom)
testProbe2.send(chatRoom, JoinChatRoom)
val chatMessage = ChatMessage("User1", "Hello!")
testProbe1.send(chatRoom, chatMessage)
testProbe2.expectMsg(chatMessage)
}
}
}
在这个测试中:
- 使用
TestKit
和
TestProbe
来模拟Actor之间的交互。
- 测试用例
add a user when JoinChatRoom message is received
验证当收到
JoinChatRoom
消息时,用户会被添加到聊天室。
- 测试用例
broadcast a chat message to all users
验证聊天室会将聊天消息广播给所有在线用户。
14. 总结
通过以上步骤,我们完成了一个基于Akka和Play框架的简单聊天应用程序的开发。整个过程涵盖了从Actor的定义、消息协议的设计、控制器的开发、前端UI的实现到Actor的测试等多个方面。
以下是整个开发过程的步骤总结表格:
| 步骤 | 描述 | 代码文件 |
| ---- | ---- | ---- |
| 1 | 定义消息协议 |
actors/MessageProtocol.scala
|
| 2 | 实现Actor |
actors/ChatRoomActor.scala
、
actors/ChatUserActor.scala
、
actors/ChatBotAdminActor.scala
|
| 3 | 开发控制器 |
controllers/ChatController.scala
|
| 4 | 配置路由 |
conf/routes
|
| 5 | 实现前端UI |
public/index.html
|
| 6 | 测试Actor |
test/actors/ChatRoomActorSpec.scala
|
同时,下面的mermaid流程图展示了整个聊天应用程序的架构和交互流程:
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([用户浏览器]):::startend -->|WebSocket连接| B(ChatController):::process
B -->|创建ChatUserActor| C(ChatUserActor):::process
C -->|JoinChatRoom| D(ChatRoomActor):::process
C -->|ChatMessage| D
E(ChatBotAdminActor):::process -->|GetStats| D
D -->|Broadcast ChatMessage| C
D -->|Stats| E
E -->|Tick| E
E -->|Stats Broadcast| C
B -->|返回Stats| A
通过这个示例,我们可以看到Akka框架在构建并发、分布式消息驱动应用方面的强大能力。它提供了Actor模型、路由、持久化等多种功能,能够帮助开发者高效地开发出具有高并发、高可扩展性和容错性的应用程序。同时,Akka与Play框架的集成也使得开发Web应用变得更加简单和高效。在实际应用中,可以根据具体需求对代码进行扩展和优化,例如添加更多的聊天功能、优化性能、增强安全性等。
超级会员免费看
11

被折叠的 条评论
为什么被折叠?



