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)
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容