原文:WebSockets
WebSockets
WebSockets是一个可以被基于允许双向全双工通信协议的Web浏览器使用的套接字。只要在服务端和客户端之间有一个活跃的WebSocket 连接, 客户端可以发送信息,服务端可以在任何时候收到信息。
新的兼容HTML5的浏览器本身通过JavaScript WebSocket API 支持WebSockets 。然而WebSockets 不仅仅局限于被WebBrowsers使用,有很多的WebSocket客户端库可以用,例如允许服务端彼此通信,也允许本地的移动应用可以使用WebSockets。在这些环境中使用WebSockets 有可以复用Play服务端已使用的TCP端口的好处。
提示:查看caniuse.com了解更多关于浏览器支持WebSockets的已知问题和更多信息。
处理WebSockets
到目前为止,我们使用Action实例处理标准的HTTP 请求,并发送回标准的HTTP应答。而WebSockets 是完全不同的东西,并不能通过标准的Action处理。
Play提供了两个不同的内置机制来处理WebSockets。第一个是使用Akka Streams(通常用Actor),第二种是使用Iteratees。这两种机制都可以使用构建器提供的WebSocket访问。
使用Akka Streams 和 Actors处理 WebSockets
为了使用Actor处理WebSocket,我们需要给Play一个携带Actor信息的akka.actor.Props 对象,当Play接受到WebSocket 连接时就可以创建Actor。Play将会给我们一个 akka.actor.ActorRef来发送上行信息,如此我们可以使用它来帮助创建Props 对象:
import play.api.mvc._
import play.api.libs.streams._
class Controller1 @Inject() (implicit system: ActorSystem, materializer: Materializer) {
def socket = WebSocket.accept[String, String] { request =>
ActorFlow.actorRef(out => MyWebSocketActor.props(out))
}
}
注意ActorFlow.actorRef(...) 可以使用任何的Akka Streams Flow[In, Out, _]替换,但是一般情况下,Actor是最直接的方式。
在这种情况下,我们是这样发送的Actor的:
import akka.actor._
object MyWebSocketActor {
def props(out: ActorRef) = Props(new MyWebSocketActor(out))
}
class MyWebSocketActor(out: ActorRef) extends Actor {
def receive = {
case msg: String =>
out ! ("I received your message: " + msg)
}
}
从客户端收到的任何信息都会被发送给Actor,并且由Play提供的发送给Actor的任何信息都会被发送给客户端。上面简单的Actor发送从客户端收回的每一个信息都会附加一个I received your message:
当检测到一个WebSocket已经关闭
当WebSocket 已经关闭时,Play会自动的停止Actor。那么你可以通过使用Actor的postStop 方法来处理这个状况,清理WebSocket消耗的任何资源,例如:
override def postStop() = {
someResource.close()
}
关闭WebSocket
当处理的WebSocket结束时,Play将自动的关闭WebSocket。 所以,Play发送PoisonPill给你自己的Actor ,来关闭WebSocket:
import akka.actor.PoisonPill
self ! PoisonPill
拒绝WebSocket
有时你希望拒绝WebSocket 请求,例如,如果用户必须被验证了才能连接WebSocket,或者如果WebSocket与资源关联,它的ID通过路径传递,但是没有这个ID相关的资源。Play提供了acceptOrResult 来解决这个问题,允许你返回一个结果(如禁止,或没找到),或者处理WebSocket 的Actor:
import scala.concurrent.Future
import play.api.mvc._
import play.api.libs.streams._
class Controller3 @Inject() (implicit system: ActorSystem, materializer: Materializer) extends play.api.mvc.Controller {
def socket = WebSocket.acceptOrResult[String, String] { request =>
Future.successful(request.session.get("user") match {
case None => Left(Forbidden)
case Some(_) => Right(ActorFlow.actorRef(MyWebSocketActor.props))
})
}
}
注意:WebSocket 协议没有实现同原协议,所以没有防备跨站点的WebSocket劫持。为了保护WebSocket不被劫持,在请求中的Origin头必须被检查以防服务端的源,并且应该实现手动的验证(包括CSRF令牌),然后acceptOrResult通过返回Forbidden 结果拒绝请求。
处理不同类型的信息
到目前为止,我们只看到处理String 结构的信息。Play也已经内置了 Array[Byte] 结构的处理,和从String结构的信息解析的JsValue 信息。你可以把这些做为类型参数传递给WebSocket 的创建方法,例如:
import play.api.libs.json.JsValue
import play.api.mvc._
import play.api.libs.streams._
class Controller4 @Inject() (implicit system: ActorSystem, materializer: Materializer) {
import akka.actor._
class MyWebSocketActor(out: ActorRef) extends Actor {
import play.api.libs.json.JsValue
def receive = {
case msg: JsValue =>
out ! msg
}
}
object MyWebSocketActor {
def props(out: ActorRef) = Props(new MyWebSocketActor(out))
}
def socket = WebSocket.accept[JsValue, JsValue] { request =>
ActorFlow.actorRef(out => MyWebSocketActor.props(out))
}
}
你也许已经注意到了,有两个类型参数,这让我们可以处理传入不同类型的信息给输出的信息。这通常对低级的结构类型无用,但是如果你把信息解析成高级类型就可以使用了。
例如,假如我们想接收JSON信息,我们想把传入的信息解析为InEvent 并格式化输出的信息为OutEvent。我们想做的第一件事情是为所有的InEvent 和 OutEvent 类型创建JSON格式:
import play.api.libs.json._
implicit val inEventFormat = Json.format[InEvent]
implicit val outEventFormat = Json.format[OutEvent]
现在我们可以为这些类型创建一个MessageFlowTransformer :
import play.api.mvc.WebSocket.FrameFormatter
implicit val messageFlowTransformer = MessageFlowTransformer.jsonMessageFlowTransformer[InEvent, OutEvent]
最终,我在我们的WebSocket中使用这些:
import play.api.libs.json._
import play.api.mvc._
import play.api.libs.streams._
// Note: requires implicit ActorSystem and Materializer (inject into your controller)
def socket = WebSocket.accept[InEvent, OutEvent] { request =>
ActorFlow.actorRef(out => MyWebSocketActor.props(out))
}
现在在我们的Actor中,我们将接收到InEvent类型的信息,并且我们可以发送OutEvent类型的信息。
使用Iteratees处理 WebSockets
为了处理WebSocket 请求,我们用WebSocket 替代了Action:
import play.api.mvc._
import play.api.libs.iteratee._
import play.api.libs.concurrent.Execution.Implicits.defaultContext
def socket = WebSocket.using[String] { request =>
// Log events to the console
val in = Iteratee.foreach[String](println).map { _ =>
println("Disconnected")
}
// Send a single 'Hello!' message
val out = Enumerator("Hello!")
(in, out)
}
WebSocket能够访问请求的头(从启动WebSocket 连接的HTTP 请求),允许你可以取到标准的头和Session数据,然而,它不能访问请求Body和HTTP应答。
当构建一个这样的WebSocket ,我必须返回in 和out 通道。
in通道是一个Iteratee[A,Unit](A是信息的类型——这里我们用String),它将通知每一个信息,并且当套接字在客户端关闭时会EOF。
out通道是一个Enumerator[A],它将会生成发送到Web客户端的信息,它可以通过发送EOF关闭连接服务端连接。
在这个例子中我们创建了一个简单的Iteratee, 它在控制台上打印了每一个信息。为了发送信息,我们创建了一个简单的虚拟
Enumerator,它将会发送一个Hello!信息。
提示:只要设置location 为ws://localhost:9000, 你就可以在https://www.websocket.org/echo.html,测试WebSockets 。
让我们再写一个丢弃输入数据并发送Hello!信息后关闭套接字的例子:
import play.api.mvc._
import play.api.libs.iteratee._
def socket = WebSocket.using[String] { request =>
// Just ignore the input
val in = Iteratee.ignore[String]
// Send a single 'Hello!' message and close
val out = Enumerator("Hello!").andThen(Enumerator.eof)
(in, out)
}
这是另一个例子,在这个例子中输入数据被记录到标准输出,并广播到使用Concurrent.broadcast的客户端:
import play.api.mvc._
import play.api.libs.iteratee._
import play.api.libs.concurrent.Execution.Implicits.defaultContext
def socket = WebSocket.using[String] { request =>
// Concurrent.broadcast returns (Enumerator, Concurrent.Channel)
val (out, channel) = Concurrent.broadcast[String]
// log the message to stdout and send response back to client
val in = Iteratee.foreach[String] {
msg => println(msg)
// the Enumerator returned by Concurrent.broadcast subscribes to the channel and will
// receive the pushed messages
channel push("I received your message: " + msg)
}
(in,out)
}