欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

play2.x笔记 官网内容翻译+截取 第二章

程序员文章站 2022-06-05 15:30:22
...

本文来自 fair-jm.iteye.com 转截请注明出处  

  

play的官方文档(for scala) 第二章的笔记 有兴趣的可以看下 有错误欢迎指出  

 

  1. Handling asynchronous results
  2. Streaming HTTP responses
  3. Comet sockets
  4. WebSockets

 

 

1、Handling asynchronous results

来自 <https://www.playframework.com/documentation/2.3.x/ScalaAsync>

 

Play自下而上都是异步的。Play以异步非阻塞的方式处理每个请求。

 

在controllers理应该避免阻塞。常见的阻塞操作包括JDBC调用流API HTTP请求和长时间的计算

 

当然也可以通过增加默认的线程数来允许controllers里的阻塞代码承受更多的并发访问遵循以下的推荐可以保持controllers的异步以此来保持更高的扩展性和高负载下的可用性

 

 

创建非阻塞的actions

Play的工作方式需要action越快越好(无阻塞)。具体的做法是通过返回一个future的结果(Future[Result])作为回应。

Play将会在承诺被取回(一个Future对应一个Promise承诺取回也就是Future得到结果)时服务结果。

 

如何创建Future[Result]

所有的Play异步API都会给你Future

play.api.libs.WS

play.api.libs.Akka

 

例子:

import play.api.libs.concurrent.Execution.Implicits.defaultContext

val futureInt:Future[Int]= scala.concurrent.Future{
  intensiveComputation()
}

注意上面用的线程池是默认的线程池也就是如果要处理很耗时的任务会阻塞线程那最好用其他的线程池而不是play的默认线程池

也可以使用Actor

 

返回Futures

import play.api.libs.concurrent.Execution.Implicits.defaultContext

def index =Action.async {
  val futureInt = scala.concurrent.Future{ intensiveComputation()}
  futureInt.map(i =>Ok("Got result: "+ i))
}

 

来自 <https://www.playframework.com/documentation/2.3.x/ScalaAsync>

 

 

Action默认就是异步的

Action.apply和Action.async的处理机制是一样的只有一种Action类型(异步),两者的差别只是返回值不同.

 

 

超时机制

超时机制的实现是相当简单的:

import play.api.libs.concurrent.Execution.Implicits.defaultContext
import scala.concurrent.duration._

def index =Action.async {
  val futureInt = scala.concurrent.Future{ intensiveComputation()}
  val timeoutFuture = play.api.libs.concurrent.Promise.timeout("Oops",1.second)
  Future.firstCompletedOf(Seq(futureInt, timeoutFuture)).map {
    case i:Int=>Ok("Got result: "+ i)
    case t:String=>InternalServerError(t)
  }
}

 

来自 <https://www.playframework.com/documentation/2.3.x/ScalaAsync>

 

 

可见scala本身的Future提供的功能还是很完善的基础设施完善所需的额外代码就少了  

 

------------------------------------------------------------------------------------------------------------------------------------

 

2 Streaming HTTP responses

 

HTTP 1.1以来,保持单个连接开放来处理多个HTTP请求和回应的话,服务器必须在回应中包含一个合适的Content-Length HTTP

 

def index =Action{
 
Ok("Hello World")
}

 

默认下,返回一个简单的结果是不会指定Content-Length的,当然简单的回应的发送信息是已知的,play也会计算这个长度并放在头中。(要注意基于文本的内容计算长度可不是看上去那么简单,因为他要根据特定的字符集来计算) 

 

上面的代码是下面的简化写法,实际用到了play.api.libs.iteratee.Enumerator:

 

def index =Action{
 
Result(
    header
=ResponseHeader(200),
    body
=Enumerator("Hello World")
 
)
}

这意味着要正确地计算Content-Length Play必须消费整个enumerator并且把内容载入内存中计算 

 

 

发送大数据量的内容

将小的数据量的内容全部读入当然没问题

那大数据量的怎么办呢?

 

一个错误的例子:

def index =Action{

val file =new java.io.File("/tmp/fileToServe.pdf")
 
val fileContent:Enumerator[Array[Byte]]=Enumerator.fromFile(file)   
   
 
Result(
    header
=ResponseHeader(200),
    body
= fileContent
 
)
}

为什么错? 因为没设置Content-Length 然后play就要把整个吃进去计算实在是浪费资源

 

正确的姿势:

def index =Action{

val file =new java.io.File("/tmp/fileToServe.pdf")
 
val fileContent:Enumerator[Array[Byte]]=Enumerator.fromFile(file)   
   
 
Result(
    header
=ResponseHeader(200,Map(CONTENT_LENGTH -> file.length.toString)),
    body
= fileContent
 
)
}

接着Play就会用惰加载的方式消费内容了

 

 

提供文件(Serving files)

play也提供了一些简便的方式来实现上面的需求:

def index =Action{
 
Ok.sendFile(new java.io.File("/tmp/fileToServe.pdf"))
}

回应中增加的内容:

 Content-Disposition: attachment;filename=fileToServe.pdf

(关于Content-Disposition:http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html)

 

提供自定义的文件名:

def index =Action{
 
Ok.sendFile(
    content
=new java.io.File("/tmp/fileToServe.pdf"),
    fileName
= _ =>"termsOfService.pdf"
 
)
}

 

浏览器直接显示(inline) 而不是下载:

def index =Action{
 
Ok.sendFile(
    content
=new java.io.File("/tmp/fileToServe.pdf"),
    inline
=true
 
)
}

 

分块响应(Chunked responses)

上面的例子是对于可以在传输文件之前得到文件长度的

那么如果对于动态计算的内容事先不能知道内容长度怎么处理呢?

 

要使用Chunked transfer encoding

Chunked transfer encodingHTTP 1.1的一种数据传输机制

web服务器提供以一系列的块的方式来提供文件

他使用Transfer-Encoding回应头而不是Content-Length

因为没有使用Content-Length 所以服务器不需要在传输前知道内容的长度可以用于传输动态生成的文件

 

数据的结束是通过最后传一个长度为0chunk结束的

 

好处: 可以处理活数据(数据可用就立马传输)

坏处: 浏览器就显示不了下载进度条了 

 

play中要实现:

def index =Action{

val data = getDataStream
 
val dataContent:Enumerator[Array[Byte]]=Enumerator.fromStream(data)
 
 
ChunkedResult(
    header
=ResponseHeader(200),
    chunks
= dataContent
 
)
}

和上面一样也有种简便的方式:

def index =Action{

val data = getDataStream
 
val dataContent:Enumerator[Array[Byte]]=Enumerator.fromStream(data)
 
 
Ok.chunked(dataContent)
}

当然里面的Enumerator是可以自己定义的:

def index =Action{
 
Ok.chunked(
   
Enumerator("kiki","foo","bar").andThen(Enumerator.eof)
 
)
}

 

HTTP的回应:

HTTP/1.1200 OK
Content-Type: text/plain; charset=utf-8
Transfer-Encoding: chunked

4
kiki
3
foo
3
bar
0

 

------------------------------------------------------------------------------------------------------------------------------------

 

3 Comet sockets

Comet socket仅仅是内容为text/html只包含<script>元素的chunked回应

我们可以向浏览器发送事件

 

def comet =Action{
 
val events =Enumerator(
   
"""<script>console.log('kiki')</script>""",
   
"""<script>console.log('foo')</script>""",
   
"""<script>console.log('bar')</script>"""
 
)
 
Ok.chunked(events).as(HTML)
}

 

也可以用play.api.libs.iteratee.Enumeratee

这个是一个适配器Enumerator[A]转换到Enumerator[B]

 

import play.twirl.api.Html

// Transform a String message into an Html script tag
val toCometMessage =Enumeratee.map[String]{ data =>
 
Html("""<script>console.log('"""+ data +"""')</script>""")
}

def comet =Action{
 
val events =Enumerator("kiki","foo","bar")
 
Ok.chunked(events &> toCometMessage)
}

 

使用 play.api.libs.Comet

上面的代码可以简单地写成:

def comet =Action{
 
val events =Enumerator("kiki","foo","bar")
 
Ok.chunked(events &>Comet(callback ="console.log"))
}

实际上这个帮助类干了更多的事比如为了浏览器的兼容性发送一个初始化的空白缓冲数据

支持StringJSON可以扩展类型类来支持更多的消息类型

 

 

永远的iframe技术(The forever iframe technique)

标准的输出Comet socket的技术是载入一个无限的chunked comet回应在iframe

并且调用父框架上的回调函数

 

def comet =Action{
 
val events =Enumerator("kiki","foo","bar")
 
Ok.chunked(events &>Comet(callback ="parent.cometMessage"))
}

 

html:

<scripttype="text/javascript">
 
var cometMessage =function(event){
    console
.log('Received event: '+ event)
 
}
</script>

<iframesrc="/comet"></iframe>

 

------------------------------------------------------------------------------------------------------------------------------------

 

 

4 websocket

允许浏览器进行全双工通信

使用webSocket可以复用存在的play使用的TCP端口

 

处理 WebSocket

webSocket是个完全不同的野兽不能用标准的Action来驾驭

play提供了两种方式来驾驭WebSocket

第一种是使用actors

第二种是使用iteratees

两种方式都可以通过WebSocket这个构造者获得

 

通过Actor处理WebSocket

机制比较简单 Play会给用来发送数据的akka.actor.ActorRef

我们使用这个来创建一个akka.actor.Props

 

import play.api.mvc._
import play.api.Play.current

def socket =WebSocket.acceptWithActor[String,String]{ request => out =>
 
MyWebSocketActor.props(out)
}

actor:

import akka.actor._

objectMyWebSocketActor{
 
def props(out:ActorRef)=Props(newMyWebSocketActor(out))
}

classMyWebSocketActor(out:ActorRef)extendsActor{
 
def receive ={
   
case msg:String=>
      out
!("I received your message: "+ msg)
 
}
}

所有从客户端接收的数据都会发送给上面这个actor

任何发送给out那个ActorRef的数据都会发送给客户端

 

WebSocket关闭时 Play会自动停止那个actor

想主动关闭一个websocket连接时也只需要关闭那个actor就行了

一颗毒药丸:

import akka.actor.PoisonPill

self !PoisonPill

(:这个是触发 ActorKilledException默认的策略是Stop来实现的)

 

 

拒绝操作也比较简单改变就是方法从acceptWithActor变为了tryAcceptWithActor

import scala.concurrent.Future
import play.api.mvc._
import play.api.Play.current

def socket =WebSocket.tryAcceptWithActor[String,String]{ request =>
 
Future.successful(request.session.get("user")match{
   
caseNone=>Left(Forbidden)
   
caseSome(_)=>Right(MyWebSocketActor.props)
 
})
}

 

上面的代码如果在session中没有user这个属性那么返回Forbidden否则建立连接

 

处理不同格式的消息

上面的例子全部都是基于String Play内置支持Array[Byte]JsValue

比如:

import play.api.mvc._
import play.api.libs.json._
import play.api.Play.current

def socket =WebSocket.acceptWithActor[JsValue,JsValue]{ request => out =>
 
MyWebSocketActor.props(out)
}

 

也可以自定义需要处理的格式

比如下面这个例子i我们接收JSON消息并且将其转化为InEvent 返回的消息转化为OutEvent

第一件事是完成 InEventOutEventJSON转换:(PS:能直接用format说明InEventOutEventcase class)

import play.api.libs.json._

implicitval inEventFormat =Json.format[InEvent]
implicitval outEventFormat =Json.format[OutEvent]

 

第二件事是创建FrameFormatter:

import play.api.mvc.WebSocket.FrameFormatter
implicitval inEventFrameFormatter =FrameFormatter.jsonFrame[InEvent]
implicitval outEventFrameFormatter =FrameFormatter.jsonFrame[OutEvent]

 

最后可以用于WebSocket:

import play.api.mvc._
import play.api.Play.current

def socket =WebSocket.acceptWithActor[InEvent,OutEvent]{ request => out =>
 
MyWebSocketActor.props(out)
}

 

使用Iteratees来处理WebSocket

Actors对于处理消息来说是个更好的抽象

Iteratee对于处理流来说是个更好的抽象

 

例子:

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)
}

这种方式最后返回的是那两个channel

inIteratee[A,Unit]  当接收到EOF时说明客户端那边关闭了socket

outEnumerator[B]  当发送EOF时说明服务端这边关闭了Socket

 

第二个例子是忽视输入直接输出后关闭:

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.broadcase可以用于广播:

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)