16、Akka框架:构建并发、分布式消息驱动应用的利器

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应用变得更加简单和高效。在实际应用中,可以根据具体需求对代码进行扩展和优化,例如添加更多的聊天功能、优化性能、增强安全性等。

内容概要:本文是一份针对2025年中国企业品牌传播环境撰写的《全网媒体发稿白皮书》,聚焦企业媒体发稿的策略制定、渠道选择与效果评估难题。通过分析当前企业面临的资源分散、内容同质、效果难量化等核心痛点,系统性地介绍了新闻媒体、央媒、地方官媒和自媒体四大渠道的特点与适用场景,并深度融合“传声港”AI驱动的新媒体平台能力,提出“策略+工具+落地”的一体化解决方案。白皮书详细阐述了传声港在资源整合、AI智能匹配、舆情监测、合规审核及全链路效果追踪方面的技术优势,构建了涵盖曝光、互动、转化与品牌影响力的多维评估体系,并通过快消、科技、零售等行业的实战案例验证其有效性。最后,提出了按企业发展阶段和营销节点定制的媒体组合策略,强调本土化传播与政府关系协同的重要性,助力企业实现品牌声量与实际转化的双重增长。; 适合人群:企业市场部负责人、品牌方管理者、公关传播从业者及从事数字营销的相关人员,尤其适用于初创期至成熟期不同发展阶段的企业决策者。; 使用场景及目标:①帮助企业科学制定媒体发稿策略,优化预算分配;②解决渠道对接繁琐、投放不精准、效果不可衡量等问题;③指导企业在重大营销节点(如春节、双11)开展高效传播;④提升品牌权威性、区域渗透力与危机应对能力; 阅读建议:建议结合自身企业所处阶段和发展目标,参考文中提供的“传声港服务组合”与“预算分配建议”进行策略匹配,同时重视AI工具在投放、监测与优化中的实际应用,定期复盘数据以实现持续迭代。
先展示下效果 https://pan.quark.cn/s/987bb7a43dd9 VeighNa - By Traders, For Traders, AI-Powered. Want to read this in english ? Go here VeighNa是一套基于Python的开源量化交易系统开发框架,在开源社区持续不断的贡献下一步步成长为多功能量化交易平台,自发布以来已经积累了众多来自金融机构或相关领域的用户,包括私募基金、证券公司、期货公司等。 在使用VeighNa进行二次开发(策略、模块等)的过程中有任何疑问,请查看VeighNa项目文档,如果无法解决请前往官方社区论坛的【提问求助】板块寻求帮助,也欢迎在【经验分享】板块分享你的使用心得! 想要获取更多关于VeighNa的资讯信息? 请扫描下方二维码添加小助手加入【VeighNa社区交流微信群】: AI-Powered VeighNa发布十周年之际正式推出4.0版本,重磅新增面向AI量化策略的vnpy.alpha模块,为专业量化交易员提供一站式多因子机器学习(ML)策略开发、投研和实盘交易解决方案: :bar_chart: dataset:因子特征工程 * 专为ML算法训练优化设计,支持高效批量特征计算与处理 * 内置丰富的因子特征表达式计算引擎,实现快速一键生成训练数据 * Alpha 158:源于微软Qlib项目的股票市场特征集合,涵盖K线形态、价格趋势、时序波动等多维度量化因子 :bulb: model:预测模型训练 * 提供标准化的ML模型开发模板,大幅简化模型构建与训练流程 * 统一API接口设计,支持无缝切换不同算法进行性能对比测试 * 集成多种主流机器学习算法: * Lass...
【顶级EI完整复现】【DRCC】考虑N-1准则的分布鲁棒机会约束低碳经济调度(Matlab代码实现)内容概要:本文介绍了名为《【顶级EI完整复现】【DRCC】考虑N-1准则的分布鲁棒机会约束低碳经济调度(Matlab代码实现)》的技术文档,重点围绕电力系统中低碳经济调度问题展开,结合分布鲁棒优化(Distributionally Robust Optimization, DRO)与机会约束规划(Chance-Constrained Programming, CCP),引入N-1安全准则以提升系统在元件故障情况下的可靠性。该方法在不确定性环境下(如风电出力波动)保障调度方案的可行性与经济性,同时降低碳排放。文档提供了完整的Matlab代码实现,便于科研人员复现实验结果,适用于高水平学术研究与工程应用验证。; 适合人群:具备电力系统优化、运筹学及不确定性建模背景的研究生、科研人员及电力行业工程师,熟悉Matlab编程与优化工具箱(如YALMIP、CPLEX/Gurobi)者更佳;适合从事智能电网、低碳调度、鲁棒优化方向的研究者; 使用场景及目标:①复现顶级EI期刊论文中的分布鲁棒机会约束模型;②研究N-1安全准则在低碳经济调度中的集成方法;③掌握分布鲁棒优化在电力系统不确定性处理中的建模技巧;④为微电网、综合能源系统等场景下的可靠、低碳调度提供算法支撑; 阅读建议:建议结合文档中提供的网盘资源(含YALMIP-develop等工具包)进行代码调试与实验验证,重点关注不确定性建模、机会约束转化、鲁棒优化求解流程,并可进一步扩展至多能源协同、需求响应等复杂场景。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值