ReactiveMongo与Akka,Scala和WebSockets

我一直在为我的一个项目寻找一个简单的websocket服务器,以使用反应性mongo测试某些东西。 但是,环顾四周时,如果没有完整的框架,我将找不到真正的简单基本实现。 最后,我偶然发现了Typesage激活程序项目之一: http ://typesafe.com/activator/template/akka-spray-websocket。 即使名称暗示需要使用spray,它实际上还是从此处使用websocket的东西: https : //github.com/TooTallNate/Java-WebSocket ,它提供了一个非常简单的基本websocket实现。

因此,在本文中,我将向您展示如何与Akka和ReactiveMongo一起设置非常简单的websocket服务器(不需要其他框架)。 以下屏幕截图显示了我们的目标:

ReactiveMongo与Akka,Scala和WebSockets_第1张图片
在此屏幕快照中,您可以看到一个与我们的服务器通信的简单websocket客户端。 我们的服务器具有以下功能:

  1. 客户端发送的任何内容都会回显。
  2. 任何添加到mongoDB中特定(上限)集合的输入都会自动推向所有侦听器。

您可以剪切并粘贴本文中的所有代码,但仅从git中获取代码可能会更容易。 您可以在github上找到它: https : //github.com/josdirksen/smartjava/tree/master/ws-akka

入门

我们需要做的第一件事是设置工作区,因此让我们先看一下sbt配置:

organization  := "org.smartjava"
 
version       := "0.1"
 
scalaVersion  := "2.11.2"
 
scalacOptions := Seq("-unchecked", "-deprecation", "-encoding", "utf8")
 
libraryDependencies ++= {
  val akkaV = "2.3.6"
  Seq(
    "com.typesafe.akka"   %%  "akka-actor"    % akkaV,
    "org.java-websocket" % "Java-WebSocket" % "1.3.1-SNAPSHOT",
    "org.reactivemongo" %% "reactivemongo" % "0.10.5.0.akka23"
  )
}
 
resolvers ++= Seq("Code Envy" at "http://codenvycorp.com/repository/"
  ,"Typesafe" at "http://repo.typesafe.com/typesafe/releases/")

这里没什么特别的,我们只需要指定我们的依赖项并添加一些解析器,以便sbt知道从何处检索依赖项。 在看代码之前,让我们先看一下项目的目录结构和文件:

├── build.sbt
└── src
    └── main
        ├── resources
        │   ├── application.conf
        │   └── log4j2.xml
        └── scala
            ├── Boot.scala
            ├── DB.scala
            ├── WSActor.scala
            └── WSServer.scala

在src / main / resources目录中,我们存储配置文件,在src / main / scala中,我们存储所有scala文件。 让我们从查看配置文件开始。 对于此项目,我们使用两个:

Application.conf文件包含我们项目的配置,如下所示:

akka {
  loglevel = "DEBUG"
}
 
mongo {
  db = "scala"
  collection = "rmongo"
  location = "localhost"
}
 
ws-server {
  port = 9999
}

如您所见,我们仅定义日志级别,如何使用mongo以及我们希望Websocket服务器在哪个端口上监听。 而且我们还需要一个log4j2.xml文件,因为reactmongo库使用该文件进行日志记录:



    
        
            
        
    
    
        
            
        
    

因此,带着无聊的东西让我们来看一下scala文件。

启动websocket服务器并注册路径

Boot.scala文件如下所示:

package org.smartjava
 
import akka.actor.{Props, ActorSystem}
 
/**
 * This class launches the system.
 */
object Boot extends App {
  // create the actor system
  implicit lazy val system = ActorSystem("ws-system")
  // setup the mongoreactive connection
  implicit lazy val db = new DB(Configuration.location, Configuration.dbname);
 
  // we'll use a simple actor which echo's everything it finds back to the client.
  val echo = system.actorOf(EchoActor.props(db, Configuration.collection), "echo")
 
  // define the websocket routing and start a websocket listener
  private val wsServer = new WSServer(Configuration.port)
  wsServer.forResource("/echo", Some(echo))
  wsServer.start
 
  // make sure the actor system and the websocket server are shutdown when the client is
  // shutdown
  sys.addShutdownHook({system.shutdown;wsServer.stop})
}
 
// load configuration from external file
object Configuration {
  import com.typesafe.config.ConfigFactory
 
  private val config = ConfigFactory.load
  config.checkValid(ConfigFactory.defaultReference)
 
  val port = config.getInt("ws-server.port")
  val dbname = config.getString("mongo.db")
  val collection = config.getString("mongo.collection")
  val location = config.getString("mongo.location")
}

在此源文件中,我们看到两个对象。 通过Configuration对象,我们可以轻松地从application.conf文件访问配置元素,而Boot对象将启动我们的服务器。 代码中的注释几乎可以解释发生了什么,但让我指出主要内容:

  1. 我们创建一个Akka actor系统,并连接到我们的mongoDB实例。
  2. 我们定义一个参与者,我们可以将其注册到特定的websocket路径。
  3. 然后,我们创建并启动websocketserver并注册指向我们刚刚创建的actor的路径。
  4. 最后,我们注册一个关闭钩子,以清理所有内容。

就是这样。 现在,让我们看一下代码中有趣的部分。 接下来是WSServer.scala文件。

设置WebSocket服务器

在WSServer.scala文件中,我们定义了websocket服务器。

package org.smartjava
 
import akka.actor.{ActorSystem, ActorRef}
import java.net.InetSocketAddress
import org.java_websocket.WebSocket
import org.java_websocket.framing.CloseFrame
import org.java_websocket.handshake.ClientHandshake
import org.java_websocket.server.WebSocketServer
import scala.collection.mutable.Map
import akka.event.Logging
 
/**
 * The WSserver companion objects defines a number of distinct messages sendable by this component
 */
object WSServer {
  sealed trait WSMessage
  case class Message(ws : WebSocket, msg : String) extends WSMessage
  case class Open(ws : WebSocket, hs : ClientHandshake) extends WSMessage
  case class Close(ws : WebSocket, code : Int, reason : String, external : Boolean) 
                                                                                                         extends WSMessage
  case class Error(ws : WebSocket, ex : Exception) extends WSMessage
}
 
/**
 * Create a websocket server that listens on a specific address.
 *
 * @param port
 */
class WSServer(val port : Int)(implicit system : ActorSystem, db: DB ) 
                             extends WebSocketServer(new InetSocketAddress(port)) {
 
  // maps the path to a specific actor.
  private val reactors = Map[String, ActorRef]()
  // setup some logging based on the implicit passed in actorsystem
  private val log = Logging.getLogger(system, this);
 
  // Call this function to bind an actor to a specific path. All incoming
  // connections to a specific path will be routed to that specific actor.
  final def forResource(descriptor : String, reactor : Option[ActorRef]) {
    log.debug("Registring actor:" + reactor + " to " + descriptor);
    reactor match {
      case Some(actor) => reactors += ((descriptor, actor))
      case None => reactors -= descriptor
    }
  }
 
  // onMessage is called when a websocket message is recieved.
  // in this method we check whether we can find a listening
  // actor and forward the call to that.
  final override def onMessage(ws : WebSocket, msg : String) {
 
    if (null != ws) {
      reactors.get(ws.getResourceDescriptor) match {
        case Some(actor) => actor ! WSServer.Message(ws, msg)
        case None => ws.close(CloseFrame.REFUSE)
      }
    }
  }
 
  final override def onOpen(ws : WebSocket, hs : ClientHandshake) {
    log.debug("OnOpen called {} :: {}", ws, hs);
    if (null != ws) {
      reactors.get(ws.getResourceDescriptor) match {
        case Some(actor) => actor ! WSServer.Open(ws, hs)
        case None => ws.close(CloseFrame.REFUSE)
      }
    }
  }
 
  final override def onClose(ws : WebSocket, code : Int, reason : String, external : Boolean) {
    log.debug("Close called {} :: {} :: {} :: {}", ws, code, reason, external);
    if (null != ws) {
      reactors.get(ws.getResourceDescriptor) match {
        case Some(actor) => actor ! WSServer.Close(ws, code, reason, external)
        case None => ws.close(CloseFrame.REFUSE)
      }
    }
  }
  final override def onError(ws : WebSocket, ex : Exception) {
    log.debug("onError called {} :: {}", ws, ex);
    if (null != ws) {
      reactors.get(ws.getResourceDescriptor) match {
        case Some(actor) => actor ! WSServer.Error(ws, ex)
        case None => ws.close(CloseFrame.REFUSE)
      }
    }
  }
}

源文件很大,但不难理解。 让我解释一下核心概念:

  1. 我们首先将许多消息定义为案例类。 这些是我们发送给演员的消息。 它们反映了我们的websocket服务器可以从客户端接收的消息。
  2. WSServer本身是从org.java_websocket库提供的WebSocketServer扩展的。
  3. WSServer定义了一个称为forResource的附加功能。 使用此功能,我们可以定义在websocket服务器上收到消息时要调用的参与者。
  4. 最后,我们重写了当websocket服务器上发生特定事件时调用的不同on *方法。

现在让我们看一下回声功能

Akka回声演员

在这种情况下,回声参与者有两个角色。 首先,它提供了通过响应同一条消息来响应传入消息的功能。 除此之外,它还会创建一个子actor(名为ListenActor)来处理从mongoDB收到的文档。

object EchoActor {
 
  // Messages send specifically by this actor to another instance of this actor.
  sealed trait EchoMessage
 
  case class Unregister(ws : WebSocket) extends EchoMessage
  case class Listen() extends EchoMessage;
  case class StopListening() extends EchoMessage
 
  def props(db: DB): Props = Props(new EchoActor(db))
}
 
/**
 * Actor that handles the websocket request
 */
class EchoActor(db: DB) extends Actor with ActorLogging {
  import EchoActor._
 
  val clients = mutable.ListBuffer[WebSocket]()
  val socketActorMapping = mutable.Map[WebSocket, ActorRef]()
 
  override def receive = {
 
    // receive the open request
    case Open(ws, hs) => {
      log.debug("Received open request. Start listening for ", ws)
      clients += ws
 
      // create the child actor that handles the db listening
      val targetActor = context.actorOf(ListenActor.props(ws, db));
 
      socketActorMapping(ws) = targetActor;
      targetActor ! Listen
    }
 
    // recieve the close request
    case Close(ws, code, reason, ext) => {
      log.debug("Received close request. Unregisting actor for url {}", ws.getResourceDescriptor)
 
      // send a message to self to unregister
      self ! Unregister(ws)
      socketActorMapping(ws) ! StopListening
      socketActorMapping remove ws;
    }
 
    // recieves an error message
    case Error(ws, ex) => self ! Unregister(ws)
 
    // receives a text message
    case Message(ws, msg) => {
      log.debug("url {} received msg '{}'", ws.getResourceDescriptor, msg)
      ws.send("You send:" + msg);
    }
 
    // unregister the websocket listener
    case Unregister(ws) => {
      if (null != ws) {
        log.debug("unregister monitor")
        clients -= ws
      }
    }
  }
}

这个演员的代码几乎应该自我解释。 到目前为止,有了这个actor和代码,我们已经有了一个简单的websocket服务器,该服务器使用actor来处理消息。 在我们从EchoHandler收到的“ Open”消息开始查看ListenActor之前,让我们快速看一下如何从数据库对象连接到mongoDB:

package org.smartjava;
 
import play.api.libs.iteratee.{Concurrent, Enumeratee, Iteratee}
import reactivemongo.api.collections.default.BSONCollection
import reactivemongo.api._
import reactivemongo.bson.BSONDocument
import scala.concurrent.ExecutionContext.Implicits.global
 
/**
 * Contains DB related functions.
 */
class DB(location:String, dbname:String)  {
 
  // get connection to the database
  val db: DefaultDB = createConnection(location, dbname)
  // create a enumerator that we use to broadcast received documents
  val (bcEnumerator, channel) = Concurrent.broadcast[BSONDocument]
  // assign the channel to the mongodb cursor enumerator
  val iteratee = createCursor(getCollection(Configuration.collection))
                    .enumerate()
                    .apply(Iteratee
                        .foreach({doc: BSONDocument => channel.push(doc)}));
 
  /**
   * Return a simple collection
   */
  private def getCollection(collection: String): BSONCollection = {
    db(collection)
  }
 
  /**
   * Create the connection
   */
  private def createConnection(location: String, dbname: String)  : DefaultDB = {
    // needed to connect to mongoDB.
    import scala.concurrent.ExecutionContext
 
    // gets an instance of the driver
    // (creates an actor system)
    val driver = new MongoDriver
    val connection = driver.connection(List(location))
 
    // Gets a reference to the database
    connection(dbname)
  }
 
  /**
   * Create the cursor
   */
  private def createCursor(collection: BSONCollection): Cursor[BSONDocument] = {
    import reactivemongo.api._
    import reactivemongo.bson._
    import scala.concurrent.Future
 
    import scala.concurrent.ExecutionContext.Implicits.global
 
    val query = BSONDocument(
      "currentDate" -> BSONDocument(
        "$gte" -> BSONDateTime(System.currentTimeMillis())
      ));
 
    // we enumerate over a capped collection
    val cursor  = collection.find(query)
      .options(QueryOpts().tailable.awaitData)
      .cursor[BSONDocument]
 
    return cursor
  }
 
  /**
   * Simple function that registers a callback and a predicate on the
   * broadcasting enumerator
   */
  def listenToCollection(f: BSONDocument => Unit,
                         p: BSONDocument => Boolean ) = {
 
    val it = Iteratee.foreach(f)
    val itTransformed = Enumeratee.takeWhile[BSONDocument](p).transform(it);
    bcEnumerator.apply(itTransformed);
  }
}

大部分代码是相当标准的,但我想指出几点。 在本课开始时,我们设置了一个迭代器,如下所示:

val db: DefaultDB = createConnection(location, dbname)
  val (bcEnumerator, channel) = Concurrent.broadcast[BSONDocument]
  val iteratee = createCursor(getCollection(Configuration.collection))
                    .enumerate()
                    .apply(Iteratee
                        .foreach({doc: BSONDocument => channel.push(doc)}));

我们要做的是首先使用Concurrent.broadcast函数创建一个广播枚举器。 该枚举器可以将通道提供的元素推送给多个使用者(迭代器)。 接下来,我们在ReactiveMongo游标提供的枚举数上创建一个iteratee,在这里我们使用刚创建的通道将文档传递到连接到bcEnumerator的任何iteratee。
我们在listenToCollection函数中将迭代对象连接到bcEnumerator:

def listenToCollection(f: BSONDocument => Unit,
                         p: BSONDocument => Boolean ) = {
 
    val it = Iteratee.foreach(f)
    val itTransformed = Enumeratee.takeWhile[BSONDocument](p).transform(it);
    bcEnumerator.apply(itTransformed);
  }

在此函数中,我们传入一个函数和一个谓词。 每当将文档添加到mongo且谓词用于确定何时停止向iteratee发送消息时,都会执行该函数。

唯一缺少的部分是ListenActor

ListenActor响应来自Mongo的消息

以下代码显示负责响应mongoDB消息的参与者。 收到监听消息后,它将使用listenToCollection函数进行注册。 每当从mongo传递消息时,它都会向自身发送一条消息,以进一步将其传播到websocket。

object ListenActor {
  case class ReceiveUpdate(msg: String);
  def props(ws: WebSocket, db: DB): Props = Props(new ListenActor(ws, db))
}
class ListenActor(ws: WebSocket, db: DB) extends Actor with ActorLogging {
 
  var predicateResult = true;
 
  override def receive = {
    case Listen => {
 
      log.info("{} , {} , {}", ws, db)
 
      // function to call when we receive a message from the reactive mongo
      // we pass this to the DB cursor
      val func = ( doc: BSONDocument) => {
        self ! ReceiveUpdate(BSONDocument.pretty(doc));
      }
 
      // the predicate that determines how long we want to retrieve stuff
      // we do this while the predicateResult is true.
      val predicate = (d: BSONDocument) => {predicateResult} :Boolean
      Some(db.listenToCollection(func, predicate))
    }
 
    // when we recieve an update we just send it over the websocket
    case ReceiveUpdate(msg) => {
      ws.send(msg);
    }
 
    case StopListening => {
      predicateResult = false;
 
      // and kill ourselves
      self ! PoisonPill
    }
  }
}

现在,我们已经完成了所有这些工作,我们可以运行此示例。 在启动时,您会看到以下内容:

[DEBUG] [11/22/2014 15:14:33.856] [main] [EventStream(akka://ws-system)] logger log1-Logging$DefaultLogger started
[DEBUG] [11/22/2014 15:14:33.857] [main] [EventStream(akka://ws-system)] Default Loggers started
[DEBUG] [11/22/2014 15:14:35.104] [main] [WSServer(akka://ws-system)] Registring actor:Some(Actor[akka://ws-system/user/echo#1509664759]) to /echo
15:14:35.211 [reactivemongo-akka.actor.default-dispatcher-5] INFO  reactivemongo.core.actors.MongoDBSystem - The node set is now available
15:14:35.214 [reactivemongo-akka.actor.default-dispatcher-5] INFO  reactivemongo.core.actors.MongoDBSystem - The primary is now available

接下来,当我们连接一个网络套接字时,我们将看到以下内容:

ReactiveMongo与Akka,Scala和WebSockets_第2张图片

[DEBUG] [11/22/2014 15:15:18.957] [WebSocketWorker-32] [WSServer(akka://ws-system)] OnOpen called org.java_websocket.WebSocketImpl@3161f479 :: org.java_websocket.handshake.HandshakeImpl1Client@6d9a6e19
[DEBUG] [11/22/2014 15:15:18.965] [ws-system-akka.actor.default-dispatcher-2] [akka://ws-system/user/echo] Received open request. Start listening for  WARNING arguments left: 1
[INFO] [11/22/2014 15:15:18.973] [ws-system-akka.actor.default-dispatcher-5] [akka://ws-system/user/echo/$a] org.java_websocket.WebSocketImpl@3161f479 , org.smartjava.DB@73fd64

现在,将消息插入到我们使用以下命令创建的mongo集合中:

db.createCollection( "rmongo", { capped: true, size: 100000 } )

然后插入一条消息:

> db.rmongo.insert({"test": 1234567, "currentDate": new Date()})
WriteResult({ "nInserted" : 1 })

在我们的websocket客户端中导致以下结果:

ReactiveMongo与Akka,Scala和WebSockets_第3张图片

  • 如果您对源文件感兴趣,请查看GitHub中的以下目录: https : //github.com/josdirksen/smartjava/tree/master/ws-akka

翻译自: https://www.javacodegeeks.com/2014/12/reactivemongo-with-akka-scala-and-websockets.html

你可能感兴趣的:(ReactiveMongo与Akka,Scala和WebSockets)