我一直在为我的一个项目寻找一个简单的websocket服务器,以使用反应性mongo测试某些东西。 但是,环顾四周时,如果没有完整的框架,我将找不到真正的简单基本实现。 最后,我偶然发现了Typesage激活程序项目之一: http ://typesafe.com/activator/template/akka-spray-websocket。 即使名称暗示需要使用spray,它实际上还是从此处使用websocket的东西: https : //github.com/TooTallNate/Java-WebSocket ,它提供了一个非常简单的基本websocket实现。
因此,在本文中,我将向您展示如何与Akka和ReactiveMongo一起设置非常简单的websocket服务器(不需要其他框架)。 以下屏幕截图显示了我们的目标:
在此屏幕快照中,您可以看到一个与我们的服务器通信的简单websocket客户端。 我们的服务器具有以下功能:
- 客户端发送的任何内容都会回显。
- 任何添加到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对象将启动我们的服务器。 代码中的注释几乎可以解释发生了什么,但让我指出主要内容:
- 我们创建一个Akka actor系统,并连接到我们的mongoDB实例。
- 我们定义一个参与者,我们可以将其注册到特定的websocket路径。
- 然后,我们创建并启动websocketserver并注册指向我们刚刚创建的actor的路径。
- 最后,我们注册一个关闭钩子,以清理所有内容。
就是这样。 现在,让我们看一下代码中有趣的部分。 接下来是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)
}
}
}
}
源文件很大,但不难理解。 让我解释一下核心概念:
- 我们首先将许多消息定义为案例类。 这些是我们发送给演员的消息。 它们反映了我们的websocket服务器可以从客户端接收的消息。
- WSServer本身是从org.java_websocket库提供的WebSocketServer扩展的。
- WSServer定义了一个称为forResource的附加功能。 使用此功能,我们可以定义在websocket服务器上收到消息时要调用的参与者。
- 最后,我们重写了当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
接下来,当我们连接一个网络套接字时,我们将看到以下内容:
[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客户端中导致以下结果:
- 如果您对源文件感兴趣,请查看GitHub中的以下目录: https : //github.com/josdirksen/smartjava/tree/master/ws-akka
翻译自: https://www.javacodegeeks.com/2014/12/reactivemongo-with-akka-scala-and-websockets.html