我从Play用户那里收到有关在Play Framework中实现调用/响应WebSocket的问题。 这不是经常出现的事情,因为这意味着使用WebSocket基本上可以完成AJAX为您所做的工作,那么,这又意味着什么呢? 但是,我想到了一些用例:
- 您必须仅在服务器端对流进行一些转换。 例如,转换可能需要大量的数据库工作,或者对于移动客户端而言在计算上过于昂贵,或者您可能想使用服务器专用的密钥来加密流。
- 您已经在使用WebSockets处理来自服务器的事件流,并且对调用的响应只是该流中的更多事件,因此您希望共享
这些事件的传输机制相同。 - 您的应用程序特别健谈,并且您不希望每个调用/响应上都有HTTP协议的开销。
可能会有更多用例– WebSockets是一项相当新的技术,作为一个行业,我们还没有真正确定最佳用例是什么。
一个简单的回声实现
Play WebSocket是通过提供一个消费客户端消息的迭代器和为客户端生成消息的枚举器来实现的。 如果我们只是想回应客户发送给我们的每条消息,那么我们将要返回一个iteratee,其输入成为我们返回的枚举数的输出。 Play并没有开箱即用的功能,但是我们可能会在以后的版本中添加一些开箱即用的功能。 现在,我打算写一个方法调用的joined
,它返回一个加入iteratee /枚举器对:
/**
* Create a joined iteratee enumerator pair.
*
* When the enumerator is applied to an iteratee, the iteratee subsequently consumes whatever the iteratee in the pair
* is applied to. Consequently the enumerator is "one shot", applying it to subsequent iteratees will throw an
* exception.
*/
def joined[A]: (Iteratee[A, Unit], Enumerator[A]) = {
val promisedIteratee = Promise[Iteratee[A, Unit]]()
val enumerator = new Enumerator[A] {
def apply[B](i: Iteratee[A, B]) = {
val doneIteratee = Promise[Iteratee[A, B]]()
// Equivalent to map, but allows us to handle failures
def wrap(delegate: Iteratee[A, B]): Iteratee[A, B] = new Iteratee[A, B] {
def fold[C](folder: (Step[A, B]) => Future[C]) = {
val toReturn = delegate.fold {
case done @ Step.Done(a, in) => {
doneIteratee.success(done.it)
folder(done)
}
case Step.Cont(k) => {
folder(Step.Cont(k.andThen(wrap)))
}
case err => folder(err)
}
toReturn.onFailure {
case e => doneIteratee.failure(e)
}
toReturn
}
}
if (promisedIteratee.trySuccess(wrap(i).map(_ => ()))) {
doneIteratee.future
} else {
throw new IllegalStateException("Joined enumerator may only be applied once")
}
}
}
(Iteratee.flatten(promisedIteratee.future), enumerator)
}
如果您不了解迭代器,则此代码可能会有些吓人,但是正如我所说,将来我们可能会将其添加到Play本身。 本博客文章中的其余代码将很简单。
现在我们已经加入了iteratee / enumerator,让我们实现一个echo WebSocket。 在本文的其余部分中,我们将假定所有WebSocket都在发送/接收JSON消息。
def echo = WebSocket.using[JsValue] { req =>
joined[JsValue]
}
现在,我们有了一个回显调用/响应WebSocket。 但这不是很有用,我们想对传入消息做些事情,并产生新的传出消息作为响应。
处理消息
因此,既然我们已经通过连接的iteratee /枚举器表达了呼叫/响应,那么如何将呼叫消息转换为不同的响应消息呢? 答案是枚举。 枚举可用于转换迭代和枚举。 我们同时返回一个枚举数和一个迭代数,那么我们要转换哪一个呢? 答案是没关系,我将使用它来转换iteratee。 我们将使用的枚举是地图枚举:
def process = WebSocket.using[JsValue] { req =>
val (iter, enum) = joined[JsValue]
(Enumeratee.map[JsValue] { json =>
Json.obj(
"status" -> "received",
"msg" -> json
)
} &> iter, enum)
}
枚举是最终用户迭代的最强大功能之一。 您可以在此处使用任何枚举,但让我们看一下其他常见用例的一些示例。
如果我们不想对每条消息都返回答复怎么办? 有很多方法可以做到这一点,但是最简单的方法是使用collect
枚举,它具有部分功能:
def process = WebSocket.using[JsValue] { req =>
val (iter, enum) = joined[JsValue]
(Enumeratee.collect[JsValue] {
case json if (json \ "foo").asOpt[JsValue].isDefined =>
Json.obj(
"status" -> "received",
"msg" -> json
)
} &> iter, enum)
}
也许我们想为单个输入产生许多响应。 在这种情况下,可以使用mapConcat
枚举,我们的map函数将返回一系列JsValue
消息以返回:
def process = WebSocket.using[JsValue] { req =>
val (iter, enum) = joined[JsValue]
(Enumeratee.mapConcat[JsValue] { json =>
Seq(
Json.obj(
"status" -> "received",
"msg" -> json
),
Json.obj("foo" -> "bar")
)
} &> iter, enum)
}
如果我们要执行一些阻止操作怎么办? 在Play 2.2中,只需提供适合阻止对您决定使用的任何枚举的调用的执行上下文即可完成此操作,但是Play 2.1尚不支持此操作,因此我们必须自己将回调分配给另一个执行上下文。 可以使用mapM
枚举来完成:
val ec: ExecutionContext = ...
def process = WebSocket.using[JsValue] { req =>
val (iter, enum) = joined[JsValue]
(Enumeratee.mapM[JsValue] { json =>
Future {
// Some expensive computation, eg a database call, that returns JsValue
}(ec)
} &> iter, enum)
}
从外部枚举器推送
您可能希望将呼叫/响应消息与其他自发地将消息推送到客户端的枚举器(例如,所有客户端的广播枚举器)的消息结合在一起。 这可以通过将加入的枚举数与外部枚举数交织来完成:
val globalEvents: Enumerator[JsValue] = ...
def process = WebSocket.using[JsValue] { req =>
val (iter, enum) = joined[JsValue]
(Enumeratee.map[JsValue] { json =>
...
} &> iter, Enumerator.interleave(enum, globalEvents))
}
结论
您的应用程序可能需要以呼叫响应方式使用WebSockets。 如果是这样,在Play中使用枚举将传入的消息流映射到传出的消息是最自然和惯用的方式。 它允许您立即调用Play提供的大量可组合枚举,并使您的代码简单易行。
翻译自: https://www.javacodegeeks.com/2013/06/call-response-websockets-in-play-framework.html