Upload
andrew-conner
View
6.659
Download
1
Tags:
Embed Size (px)
DESCRIPTION
We use websockets for our clients because we care deeply about a fast, responsive user experience. At the Play! Framework meetup based near us in Mountain View, CA (http://www.meetup.com/PlayFramework/), we presented an introduction to using Websockets with Play!. We cover some relevant background into alternatives, benchmarks, and how Websockets work within Play!.
Citation preview
Building real-time web apps using WEBSOcKeTS WITH PLAY!
[email protected] @connerdelights
How we used to do it
request
How we used to do it
requestresponse
How we used to do it
requestresponse
new message!
How we used to do it
requestresponse
new message!
The web has changed (quite a bit)
The dynamic web needs real-time communication
The dynamic web needs real-time communication
short polling
short polling
short polling
short polling
Resource intensive, slow, limited to one response
Chunked Responses
Chunked Responses
Hacky,no error handling, half-duplex
Long Polling
Long Polling
Long Polling
Long Polling
well supported, still half-duplex,single req/resp
These do not handle high bursts of messages
These do not handle high bursts of messages
Stuck with the single request → response
model
GET / HTTP/1.1Host: www.google.comConnection: keep-aliveCache-Control: max-age=0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.71 Safari/537.36DNT: 1Accept-Encoding: gzip,deflate,sdchAccept-Language: en-US,en;q=0.8Cookie: PREF=ID=e248d326d84eb3dc:FF=0:TM=1372622071:LM=1274622070:S=2bERIaHgSKRjWeC8; NID=47=CHAZZVkq40TcovIu-FuXlU0pF2UPfqqSEhNqx8hqUKnZ7-s4uxGjtBEFK7kRtTSVEu4fzJ00vhB4OrLRxw8JfV5EuiKczEC2_EHkBqr1kNwn_NdZ73XRl2umFybXYoiVD_
HTTP/1.1 200 OKDate: Tue, 23 Jul 2013 23:28:02 GMTExpires: -1Cache-Control: private, max-age=0Content-Type: text/html; charset=UTF-8Set-Cookie: PREF=ID=e248d326d84eb3dc:FF=1997f24999d1d9ef:FF=0:TM=1334622374:LM=1374622082:S=ynynJppwL64C6VMRU; expires=Thu, 23-Jul-2015 23:28:02 GMT; path=/; domain=.google.comContent-Encoding: gzipServer: gwsX-XSS-Protection: 1; mode=blockX-Frame-Options: SAMEORIGINTransfer-Encoding: chunked
Inefficient
Over 1kb in headers
Can Play! do these?
Can Play! do these?
Yes!
Can Play! do these?
In fact, if you’re supporting older browsers, consider them!
Yes!
Websockets
Websockets
Full duplex,efficient, fast,only newer* browsers
How fast are websockets?
our office
EC2US West
How fast are websockets?
our office
EC2US West
Average 10ms ping
~13ms mean improvement
Full benchmarks:http://eng.42go.com/
GET / HTTP/1.1Host: www.google.comConnection: keep-aliveCache-Control: max-age=0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.71 Safari/537.36DNT: 1Accept-Encoding: gzip,deflate,sdchAccept-Language: en-US,en;q=0.8Cookie: PREF=ID=e248d326d84eb3dc:FF=0:TM=1372622071:LM=1274622070:S=2bERIaHgSKRjWeC8; NID=47=CHAZZVkq40TcovIu-FuXlU0pF2UPfqqSEhNqx8hqUKnZ7-s4uxGjtBEFK7kRtTSVEu4fzJ00vhB4OrLRxw8JfV5EuiKczEC2_EHkBqr1kNwn_NdZ73XRl2umFybXYoiVD_
HTTP/1.1 200 OKDate: Tue, 23 Jul 2013 23:28:02 GMTExpires: -1Cache-Control: private, max-age=0Content-Type: text/html; charset=UTF-8Set-Cookie: PREF=ID=e248d326d84eb3dc:FF=1997f24999d1d9ef:FF=0:TM=1334622374:LM=1374622082:S=ynynJppwL64C6VMRU; expires=Thu, 23-Jul-2015 23:28:02 GMT; path=/; domain=.google.comContent-Encoding: gzipServer: gwsX-XSS-Protection: 1; mode=blockX-Frame-Options: SAMEORIGINTransfer-Encoding: chunked Over 1kb in headers
How fast are websockets?
Initial Websockettransmission
WebsocketClient
HTTPClient
How fast are websockets?
EC2US West
TCP Websocket
0.002ms 0.02ms
Websockets arenot magical :)
Full benchmarks:http://eng.42go.com/
How fast are websockets?
EC2US West
TCP Websocket
0.002ms 0.02ms
Still quite fast
Full benchmarks:http://eng.42go.com/
Websockets deal with streams of data
from the client
to the client
What are my messages?Here’s Your messages!You have a new notification
from the client
to the client
What are my messages?Here’s Your messages!You have a new notificationI visited my user profile pageI visited the home page
from the client
to the client
What are my messages?Here’s Your messages!You have a new notificationI visited my user profile pageI visited the home pageHere’s a Message for FrankGot your message
from the client
to the client
What are my messages?Here’s Your messages!You have a new notificationI visited my user profile pageI visited the home pageHere’s a Message for FrankGot your messageCode push, reconnect please!
from the client
to the client
How does Play! handle websockets?
How does Play! handle websockets?
let’s step back a bit...
val it = Seq(1,2,3,4,5).toIterator while(it.hasNext) { println(it.next())}
Iterators
val it = Seq(1,2,3,4,5).toIterator while(it.hasNext) { println(it.next())}
val list = Seq(1,2,3,4,5)
list.foreach(println)
Iterators
val list = Seq(1,2,3,4,5)
list.foreach(println)
Instead of imperatively traversing containers, apply a function to elements in a container.
Iterators
val list = Seq(1,2,3,4,5)
list.foreach(println)
Container Function
Iterators
Iteratorsval list = Seq(1,2,3,4,5)
list.foreach(println)
Container FunctionProducer Consumer
Iteratee(ie, the consumer)
Immutably consumes chunks from a Producer. Think: Iterates over a stream*.
Iteratee(ie, the consumer)
Immutably consumes chunks from a Producer. Think: Iterates over a stream*.
Iteratees can actually do a bit more,
but we’ll save that for another day.
Enumerator(ie, the PRODUCER)
Produces typed chunks.Only produces when there is a CONSUMER.
Play! needs a producer and consumer for a Websocket stream
val in = Iteratee.foreach[JsArray](println)
val in = Iteratee.foreach[JsArray](println)
ConsumerThis iteratee will consume from an Enumerator (the client), printing the input to the console
val in = Iteratee.foreach[JsArray](println) val out = Enumerator("You connected!").andThen(Enumerator.eof)
ProducerThis Enumerator will be consumed by an iteratee
(The client), sending a string then EOF
def index = WebSocket.using[JsArray] { request => val in = Iteratee.foreach[JsArray](println) val out = Enumerator("You connected!").andThen(Enumerator.eof) (in, out) }
def index = WebSocket.using[JsArray] { request => val in = Iteratee.foreach[JsArray](println) val out = Enumerator("You connected!").andThen(Enumerator.eof) (in, out) }
It works!
> var createSocket = function() { var s = new WebSocket("ws://localhost:9000"); s.onopen = function() { s.send("['hey!']"); } s.onclose = function(e) { console.log("closed!",e); } s.onmessage = function(msg) { console.log("got: ", msg.data); } return s; }undefined> var socket = createSocket()undefinedgot: You connected!closed! CloseEvent {reason: "", code: 1005, wasClean: true, clipboardData:
def echo = WebSocket.using[JsArray] { request =>
val (out, channel) = Concurrent.broadcast[JsArray] val in = Iteratee.foreach[JsArray](channel.push) (in, out) }
def echo = WebSocket.using[JsArray] { request =>
val (out, channel) = Concurrent.broadcast[JsArray] val in = Iteratee.foreach[JsArray](channel.push) (in, out) }
feeds into
Channels let us imperatively push data
into an enumerator
Channels
Channels...
how about a chat room?super simple^
Simplified version of the Play! Chat room websocket sample
https://github.com/playframework/playframework/blob/master/samples/scala/websocket-chat
case class Join(username: String)case class Quit(username: String)case class Talk(username: String, text: String)
class ChatRoom extends Actor { var members = Set.empty[String] val (chatEnumerator, chatChannel) = Concurrent.broadcast[JsValue] val chatBot = "Marvin" def receive = ??? }
case class Join(username: String)case class Quit(username: String)case class Talk(username: String, text: String)
class ChatRoom extends Actor { var members = Set.empty[String] val (chatEnumerator, chatChannel) = Concurrent.broadcast[JsValue] val chatBot = "Marvin" def receive = ??? } channel
outputlets us push data
into the enumerator
case class Join(username: String)case class Quit(username: String)case class Talk(username: String, text: String)
class ChatRoom extends Actor { var members = Set.empty[String] val (chatEnumerator, chatChannel) = Concurrent.broadcast[JsValue] val chatBot = "Marvin" def receive = { case Join(username) => { members = members + username broadcastMessage(chatBot, s"$username has joined") sender ! chatEnumerator } case Quit(username) => case Talk(username, text) => } def broadcastMessage(user: String, text: String): Unit = ???}
case class Join(username: String)case class Quit(username: String)case class Talk(username: String, text: String)
class ChatRoom extends Actor { var members = Set.empty[String] val (chatEnumerator, chatChannel) = Concurrent.broadcast[JsValue] val chatBot = "Marvin" def receive = { case Join(username) => { members = members + username broadcastMessage(chatBot, s"$username has joined") sender ! chatEnumerator } case Quit(username) => case Talk(username, text) => } def broadcastMessage(user: String, text: String): Unit = ???}
send back a reference to the chatroom’s enumerator
case class Join(username: String)case class Quit(username: String)case class Talk(username: String, text: String)
class ChatRoom extends Actor { var members = Set.empty[String] val (chatEnumerator, chatChannel) = Concurrent.broadcast[JsValue] val chatBot = "Marvin" def receive = { case Join(username) => { members = members + username broadcastMessage(chatBot, s"$username has joined") sender ! chatEnumerator } case Quit(username) => { broadcastMessage(chatBot, s"$username has left") members = members - username } case Talk(username, text) => broadcastMessage(username, text) } def broadcastMessage(user: String, text: String): Unit = ???}
case class Join(username: String)case class Quit(username: String)case class Talk(username: String, text: String)
class ChatRoom extends Actor {
... def broadcastMessage(user: String, text: String): Unit = { val msg = Json.obj("user" -> JsString(user), "message" -> JsString(text), "members" -> JsArray(members.toList.map(JsString))) chatChannel.push(msg) }}
object Application extends Controller { lazy val chatroomActor = Akka.system.actorOf(Props[ChatRoom]) def chat(username: String) = WebSocket.async[JsValue] { request => (chatroomActor ? Join(username)) map { // grab the Enumerator from ChatRoom: case out: Enumerator[JsValue] => val in = Iteratee.foreach[JsValue] { event => chatroomActor ! Talk(username, (event \ "text").as[String]) }.mapDone { _ => chatroomActor ! Quit(username) } (in, out) } }}
This can be modified to be a lightweight pub-sub system by
creating many channels
Play!
Lessons learneduse debugging tools!
Lessons learneduse debugging tools!
Lessons learnedweird things can happen when you’re
depending on a long-lived socket
Lessons learnedweird things can happen when you’re
depending on a long-lived socket
clients lie voodoo when packet loss is high
disconnects happen
Lessons learned
babysit connections in your application
ping / pong
Lessons learned
your clients need to auto-reconnect(we’ll be open sourcing our
reconnecting WS library soon!)
Lessons learned
be careful about thread safety
val in = Iteratee.foreach[JsArray](/* handler */)
Lessons learned
make sure this guy is fast
val in = Iteratee.foreach[JsArray](/* handler */)