WebSockets[翻译]


原文: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)
}

祝青

继续阅读此作者的更多文章