using websockets with play!

Post on 10-May-2015

6.660 Views

Category:

Technology

1 Downloads

Preview:

Click to see full reader

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!.

TRANSCRIPT

Building real-time web apps using WEBSOcKeTS WITH PLAY!

andrew@42go.com @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 */)

http://caniuse.com/websockets

http://caniuse.com/websockets

Find me: andrew@42go.com @connerdelights

Questions?

http://eng.42go.com/

top related