WebSockets 是浏览器上的全双工通信协议。在WebSockets通道存在期间,客户端和服务器之间可以自由通信。
现代 HTML5 兼容的浏览器可以通过 JavaScript API 原生地支持WebSockets。除了浏览器之外,还有许多WebSockets客户端库可用于服务器之间、原生的移动APP通信等场景。在这些环境使用WebSockets的好处是可以重用Play服务器现有的TCP端口。
提示:到这里查看支持WebSockets的浏览器相关问题。
处理WebSockets
到目前为止,我们都是用 Action 来处理标准 HTTP 请求并返回标准 HTTP 响应。但是标准的 Action 并不能处理 WebSockets 这种完全不同的请求。
Play 的 WebSockets 功能建立在Akka stream的基础上,将收到的 WebSockets 消息变成流,然后从流中产生响应并发送到客户端。
从概念上来说,一个 “流” 指收到消息、处理消息、最后产生消息这样一种消息转换。这里的输入和输出可以完全解耦开来。Akka提供了 Flow.fromSinkAndSource 构造函数来处理这种场景,事实上处理WebSockets时,输入和输出并不直接相互连接。
Play在 WebSocket 类中提供了构造WebSockets的工厂方法。
使用 Akka Streams 及 actors
为了使用 actor 来处理WebSockets,我们使用Play提供的ActorFlow工具来将ActorRef转换为流。当Play接收到一个WebSockets连接时,会创建一个actor,它接受一个 ActorRef => akka.actor.Props 函数为参数并返回一个socket:
import play.api.mvc._
import play.api.libs.streams.ActorFlow
import javax.inject.Inject
import akka.actor.ActorSystem
import akka.stream.Materializer
class Application @Inject()(cc:ControllerComponents)(implicit system: ActorSystem, mat: Materializer) extends AbstractController(cc) {
def socket = WebSocket.accept[String, String] { request =>
ActorFlow.actorRef { out =>
MyWebSocketActor.props(out)
}
}
}
注意ActorFlow.actorRef(...) 可以用 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
在actor停止时,Play也将自动关闭其处理的WebSocket。因此要手动关闭WebSocket,可以主动向actor发送PoisonPill:
impoort akka.actor.PoisonPill
self ! PoisonPill
拒绝WebSocket
某些时候我们需要拒绝一个WebSocket请求,如:连接前需要先对用户鉴权,或者请求了不存在的资源。Play提供了 acceptOrResult方法来应对这种情况,你可以直接返回一个Result(如 FORBIDDEN、NOT FOUND 等),也可以返回一个处理WebSocket的actor:
import play.api.mvc._
import play.api.libs.streams.ActorFlow
import javax.inject.Inject
import akka.actor.ActorSystem
import akka.stream.Materializer
class Application @Inject()(cc: ControllerComponents)(implicit system: ActorSystem, mat: Materializer) extends AbstractController(cc) {
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(out)
})
})
}
}
注意:WebSocket协议并未实现同源策略,因此无法防御跨站点WebSocket劫持。要保护websocket不被劫持,需要根据server的origin来检测request的Origin头,然后手动来进行鉴权(包括CSRF token)。如果一个WebSocket没有通过安全性检查,可以直接用acceptOrResult方法返回FORBIDDEN。
处理不同类型的消息
现在我们只处理了String类型的数据。其实Play也内置了 Array[Byte] 的handler,而且可以从String类型的数据帧中解析出JsValue。数据类型可以在WebSocket的创建方法中以类型参数形式来定义:
import play.api.libs.json._
import play.api.mvc._
import play.api.libs.streams.ActorFlow
import javax.inject.Inject
import akka.actor.ActorSystem
import akka.stream.Materializer
class Application @Inject()(cc:ControllerComponents)
(implicit system: ActorSystem, mat: Materializer)
extends AbstractController(cc) {
def socket = WebSocket.accept[JsValue, JsValue] { request =>
ActorFlow.actorRef { out =>
MyWebSocketActor.props(out)
}
}
}
你可能注意到了上边的两个JsValue类型,它允许我们处理不同类型的输入及输出。在高层级的数据帧类型上尤其有用。
举个栗子,比如我们希望收到JSON数据类型,并将输入的消息转为InEvent对象,然后将输出消息格式化为OutEvent对象。首先需要创建JSON来格式化我们的InEvent及OutEvent:
import play.api.libs.json._
implicit val inEventFormat = Json.format[InEvent]
implicit val outEventFormat = Json.format[OutEvent]
然后可以为这些类型来创建WebSocket MessageFlowTransformer:
import play.api.mvc.WebSocket.MessageFlowTransformer
implicit val messageFlowTransformer = MessageFlowTransformer.jsonMessageFlowTransformer[InEvent, OutEvent]
最后在WebSocket中使用它们:
import play.api.mvc._
import play.api.libs.streams.ActorFlow
import javax.inject.Inject
import akka.actor.ActorSystem
import akka.stream.Materializer
class Application @Inject()(cc:ControllerComponents)
(implicit system: ActorSystem, mat: Materializer)
extends AbstractController(cc) {
def socket = WebSocket.accept[InEvent, OutEvent] { request =>
ActorFlow.actorRef { out =>
MyWebSocketActor.props(out)
}
}
}
现在我们的actor可以直接受到InEvent类型的消息,然后直接发送 OutEvent。
使用Akka streams直接处理WebSockets
Actors抽象并不是总是适合你的场景,特别是如果WebSockets本身表现得更像流的时候。
import play.api.mvc._
import akka.stream.scaladsl._
def socket = WebSocket.accept[String, String] { request =>
// Log events to the console
val in = Sink.foreach[String](println)
// Send a single 'Hello!' message and then leave the socket open
val out = Source.single("Hello!").concat(Source.maybe)
Flow.fromSinkAndSource(in, out)
}
一个WebSocket可以访问初始化WebSocket连接的原始HTTP头,允许你检索标准头以及session数据。但是它不能访问请求体及HTTP响应。
在这个例子中,我们创建了一个简单的 sink 来打印消息到控制台。并创建了一个简单的 source 来发送简单的 “Hello!”。我们还需要维持一个永远不会发送任何内容的 source,否则我们的单个source将终止流,从而终止掉连接。
提示:你可以在 https://www.websocket.org/echo.html 上测试WebSockets。只需要将 location 设置为: ws://localhsot:9000。
下面是一个丢弃输入数据,并简单返回 “Hello!”的例子:
import play.api.mvc._
import akka.stream.scaladsl._
def socket = WebSocket.accept[String, String] { request =>
// Just ignore the input
val in = Sink.ignore
// Send a single 'Hello!' message and close
val out = Source.single("Hello!")
Flow.fromSinkAndSource(in, out)
}
下面是另一个例子,将输入简单记录到标准输出,然后使用发送回client:
import play.api.mvc._
import akka.stream.scaladsl._
def socket = WebSocket.accept[String, String] { request =>
// log the message to stdout and send response back to client
Flow[String].map { msg =>
println(msg)
"I received your message: " + msg
}
}
设置WebSocket帧长度
你可以使用play.server.websocket.frame.maxLength或者设置 --Dwebsocket.frame.maxLength系统变量来设置WebSocket数据帧的长度。举例如下:
sbt -Dwebsocket.frame.maxLength=64k run
你可以根据项目需要自由的调整适合的帧长度。同事使用较长的数据帧也可以减少DOS攻击。
来源:oschina
链接:https://my.oschina.net/u/140355/blog/2396335