Introduction
Welcome!
In today's times, we may listen to the Play framework, created by Guillaume Bort, that follows the model–view–controller architectural pattern. If you have not listened to it, don't worry, but I am sure that You are thinking about why I am talking about the Play framework. So, the main reason for talking about the Play framework is that the Play framework is written in Scala, and we will cover Asynchronous HTTP programming in Scala.
Now, coming to the topic of Asynchronous HTTP programming.
Asynchronous HTTP programming
Asynchronous HTTP programming is a big topic so let's discuss this in the following parts.
- Asynchronous Results
- Streaming HTTP Responses
- Comet
- WebSockets
Asynchronous Results
Let's see how we handle asynchronous results.
Make Controllers Asynchronous.
Play Framework is asynchronous Internally from the bottom up and handles every request asynchronously.
Also, the default configuration is tuned for asynchronous controllers, i.e., having the controller code wait for an operation or JDBC calls, streaming API, HTTP requests, and long computations are blocking operations.
We can also increase the number of threads in the default execution context to allow more concurrent requests to be processed by blocking controllers.
Creating Non-Blocking Actions
Because of the Play, the action code must be as fast as possible, i.e., non-blocking.
So the question is, what should we return as a result if we are not yet able to generate it?
The response is a future result!
A Future[Result] will be recovered with a value of type Result. Therefore, we can quickly generate the result without blocking by giving a Future[Result] instead of a typical Result.
While waiting for the response, the web client will be blocked, but nothing on the server will be blocked.
Also, the server resources can be used to serve other clients.
If you call out to a blocking API such as JDBC, you will have to run your ExecutionStage with a different executor to move it off Play's rendering thread pool. You can also create a subclass of Play.api.libs.concurrent.CustomExecutionContext regarding the custom dispatcher.
import play.api.libs.concurrent.CustomExecutionContext
trait MyExecutionContext extends ExecutionContext
class MyExecutionContextImpl @Inject() (system: ActorSystem)
extends CustomExecutionContext(system, "my.executor")
with MyExecutionContext
class CodingNinjas @Inject() (myExecutionContext: MyExecutionContext, val controllerComponents: ControllerComponents)
extends BaseController {
def index = Action.async {
Future {
Ok("result of blocking call")
}(myExecutionContext)
}
}
How to Create a Future[Result]
Let's discuss how we create a Future[result].
You need another future first to create a Future[Result], which means that the Future will give us the actual value that you need to compute the result:
val futurePIValue: Future[Double] = computePIAsynchronously()
val futureResult: Future[Result] = futurePIValue.map { pi =>
Ok("PI value computed: " + pi)
}
All Play's asynchronous API calls give you a Future. You can call the external web service to schedule asynchronous tasks using the Play.api.libs.WS API or to communicate with actors using play.api.libs.Akka.
Returning futures
We can use the action.apply the builder method to build actions to send an asynchronous result. Also, we need to use the action.async builder method:
def index = Action.async{
val futureInt = scala.concurrent.Future { intensiveComputation() }
futureInt.map(i => Ok("Got result: " + i))
}
Actions are Asynchronous by default.
By default, Play actions are asynchronous. You can check in the code below that the { Ok(...) } part of the code is not the method body of the controller. An anonymous function is being passed to the Action object's apply method, which creates an object of type Action. The unknown function you wrote will be called internally, and its result will be enclosed in the Future.
def echo = Action { request =>
Ok("Got request [" + request + "]")
}
Handling Time-outs
To avoid the web browser block, you can use Play.api.libs.concurrent.Futures to wrap a Future in the non-blocking timeout.
import scala.concurrent.duration._
import play.api.libs.concurrent.Futures._
def index = Action.async {
intensiveComputation()
.withTimeout(1.seconds)
.map { i =>
Ok("Got result: " + i)
}
.recover {
case e: scala.concurrent.TimeoutException =>
InternalServerError("timeout")
}
}
Let's discuss the Streaming HTTP Responses in Asynchronous HTTP programming.
Streaming HTTP Responses
Standard Responses and Content-Length Header
For keeping a single connection open to serve several HTTP requests and responses in HTTP1.1, the server must send the significant Content-Length HTTP header along with the response.
We are not specifying a Content-Length header by default when you send back a simple result, such as:
def index = Action {
Ok("Hello World")
}
Sending Large Amounts of Data
What about large data sets if it's not a problem to load the whole content into memory? Let's say to return a large file to the web client.
Firstly, let's see how to create a Source[ByteString, _] for the file content:
val file = new java.io.File("/tmp/fileToServe.pdf")
val path: java.nio.file.Path = file.toPath
val source: Source[ByteString, _] = FileIO.fromPath(path)
Now it looks simple right? Let's use this streamed HttpEntity to specify the response body:
def streamed = Action{
val file = new java.io.File("/tmp/fileToServe.pdf")
val path: java.nio.file.Path = file.toPath
val source: Source[ByteString, _] = FileIO.fromPath(path)
Result(header = ResponseHeader(200, Map.empty),body = HttpEntity.Streamed(source, None, Some("application/pdf")))
}
Now, we have a problem here as we don't specify the Content-Length in a streamed entity, Play will have to compute it, and the only way to do this is to consume the entire source content and load it into memory, then compute the response size.
Serving files
Of course, Play provides easy-to-use helpers for the common task of serving a local file:
def file = Action {
Ok.sendFile(new java.io.File("/tmp/fileToServe.pdf"))
}
The above helper will also compute the Content-Type header from the file name and add the Content-Disposition header. To show this file inline, add the header Content-Disposition: inline; filename=fileToServe.pdf to the HTTP response.
Chunked Responses
We can compute the content length before streaming it; therefore, it works well with streaming file content. But what about the dynamically computed content when no content size is available?
The benefit is that we can serve the data live, which means that we send chunks of data as soon as they are available, and the drawback is that it cannot display a proper download progress bar since the web browser doesn't know the content size.
Now, let's see the uses of Comet with String flow, JSON flow, and iframe in the Asynchronous HTTP programming.
Comet
Using Chunked Responses with Comet
The common use of chunked responses is to create a Comet socket.
Since A Comet socket is a chunked text/html response that contains only <script> elements, and For each chunk, we can write a <script> tag containing JavaScript that is executed by the web browser immediately. Also, we can send events to live to the web browser from the server this way, and for each message, firstly, we have to wrap it into a <script> tag that calls a JavaScript callback function and then write it to the chunked response.
Comet Imports
To use the Comet helper, we have to import the classes, which are the following-
import akka.stream.Materializer
import akka.stream.scaladsl.Source
import play.api.http.ContentTypes
import play.api.libs.Comet
import play.api.libs.json._
import play.api.mvc._
Using Comet with String Flow
For pushing a string of messages through a Flow, do the following:
def cometString = Action {
implicit val m = materializer
def stringSource: Source[String, _] = Source(List("kiki", "foo", "bar"))
Ok.chunked(stringSource.via(Comet.string("parent.cometMessage"))).as(ContentTypes.HTML)
}
Using Comet with JSON Flow
Fro pushing JSON messages through a Flow, do the following:
def cometJson = Action {
implicit val m = materializer
def jsonSource: Source[JsValue, _] = Source(List(JsString("jsonString")))
Ok.chunked(jsonSource.via(Comet.json("parent.cometMessage"))).as(ContentTypes.HTML)
}
Using Comet with iframe
The comet helper should be used with a forever-iframe technique, with an HTML page like:
<script type="text/javascript">
var cometMessage = function(event) {
console.log('Received event: ' + event)
}
</script>
<iframe src="/comet"></iframe>
You have to add the following config to your application.conf and ensure that you have route mappings set up to see the above Comet in action.
play.filters.headers {
frameOptions = "SAMEORIGIN"
contentSecurityPolicy = "connect-src 'self'"
}
Debugging Comet
Using the log() operation to show any errors in mapping data through the stream is the easiest or most straightforward way to debug a Comet stream that is not working.
At last, we will discuss the WebSockets in Asynchronous HTTP programming.
WebSockets
WebSockets are the type of sockets that are used from a web browser based on a protocol that allows two-way full duplex communication. Since the client can send messages, the server can receive messages as long as there is an active WebSocket connection between them.
The Modern HTML5 compliant web browsers support WebSockets via a JavaScript WebSocket API. However, WebSockets are not limited to just being used by WebBrowsers. Since there are many WebSocket client libraries available that allow servers to talk to each other and native mobile apps to use WebSockets. Also, using the WebSockets in these contexts has the advantage of reusing the existing TCP port used by the Play server.
Handling WebSockets
Until now, we have been using Action instances to handle standard HTTP requests and send back normal HTTP responses. WebSockets are a different beast and cannot be addressed via standard Action.
A WebSocket is modeled as a Flow, incoming WebSocket messages are fed into the Flow, and at last, the messages produced by the Flow are sent out to the client.
A flow is viewed as something that receives messages, does some processing, and then produces the processed messages. The input of the Flow may be disconnected entirely from the output of the Flow. Akka streams provide a constructor, Flow.fromSinkAndSource, precisely for this purpose, and often when handling WebSockets, the input and work will not be connected at all.
Also, The Play provides some factory methods for constructing WebSockets.
Handling WebSockets with Akka Streams and actors
To convert an ActorRef to a flow, we can use a Play utility, ActorFlow, to handle a WebSocket with an actor. This utility takes a function as an input that converts the ActorRef to send messages to an object akka.actor.props.
import play.api.mvc._
import play.api.libs.streams.ActorFlow
import javax.inject.Inject
import akka.actor.ActorSystem
import akka.stream.Materializer
class CodingNinjas @Inject() (cc: ControllerComponents)(implicit system: ActorSystem, mat: Materializer)
extends AbstractController(cc) {
def socket = WebSocket.accept[String, String] { request =>
ActorFlow.actorRef { out =>
MyWebSocketActor.props(out)
}
}
}
Handling WebSockets with Akka streams directly
If the WebSocket behaves more like a stream, then Actors are not the right abstraction to handle the WebSockets.
import play.api.mvc._
import akka.stream.scaladsl._
def socket = WebSocket.accept[String, String] { request =>
val in = Sink.foreach[String](println)
val out = Source.single("Hello!").concat(Source.maybe)
Flow.fromSinkAndSource(in, out)
}
If you want to retrieve standard headers and session data, then WebSocket allows you to access the request headers. However, it doesn't access the request body or the HTTP response.
In the above example, we have created a sink that prints each message to the console. We make a simple source to send messages for sending a single Hello! Message. Also, we need to concatenate a source that will never send anything; otherwise, the single source will terminate the Flow and the connection.
Accessing a WebSocket
You need to add a route to send data or access a WebSocketfor your WebSocket in your routes file.
Configuring WebSocket Frame Length
Now, coming to this topic, You can configure the maximum length for WebSocket data frames using Play.server.websocket.frame.maxLength or by passing -Dwebsocket.frame.maxLength system property when you are running your application.
Let's take an example.
sbt -Dwebsocket.frame.maxLength=64k run
The above configuration gives you more control of WebSocket frame length. Also, it can be adjusted to your application requirements which may reduce denial of service attacks using extended data frames.
Let's Check out some FAQs related to Asynchronous HTTP programming.