KEMBAR78
Using Websockets with Play! | PDF
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
request
response
How we
used to
do it
request
response
new message!
How we
used to
do it
request
response
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 toone 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.1
Host: www.google.com
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-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.36
DNT: 1
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Cookie: PREF=ID=e248d326d84eb3dc:FF=0:TM=1372622071:LM=1274622070:S=2bERIaHgSKRjWeC8; NID=47=CHAZZVkq40TcovIu-
FuXlU0pF2UPfqqSEhNqx8hqUKnZ7-s4uxGjtBEFK7kRtTSVEu4fzJ00vhB4OrLRxw8JfV5EuiKczEC2_EHkBqr1kNwn_NdZ73XRl2umFybXYoiVD_
HTTP/1.1 200 OK
Date: Tue, 23 Jul 2013 23:28:02 GMT
Expires: -1
Cache-Control: private, max-age=0
Content-Type: text/html; charset=UTF-8
Set-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.com
Content-Encoding: gzip
Server: gws
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Transfer-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
EC2
US West
How fast are
websockets?
our office
EC2
US West
Average 10ms ping
~13ms mean improvement
Full benchmarks:
http://eng.42go.com/
GET / HTTP/1.1
Host: www.google.com
Connection: keep-alive
Cache-Control: max-age=0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
User-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.36
DNT: 1
Accept-Encoding: gzip,deflate,sdch
Accept-Language: en-US,en;q=0.8
Cookie: PREF=ID=e248d326d84eb3dc:FF=0:TM=1372622071:LM=1274622070:S=2bERIaHgSKRjWeC8; NID=47=CHAZZVkq40TcovIu-
FuXlU0pF2UPfqqSEhNqx8hqUKnZ7-s4uxGjtBEFK7kRtTSVEu4fzJ00vhB4OrLRxw8JfV5EuiKczEC2_EHkBqr1kNwn_NdZ73XRl2umFybXYoiVD_
HTTP/1.1 200 OK
Date: Tue, 23 Jul 2013 23:28:02 GMT
Expires: -1
Cache-Control: private, max-age=0
Content-Type: text/html; charset=UTF-8
Set-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.com
Content-Encoding: gzip
Server: gws
X-XSS-Protection: 1; mode=block
X-Frame-Options: SAMEORIGIN
Transfer-Encoding: chunked
Over 1kb in headers
How fast are
websockets?
Initial Websocket
transmission
Websocket
Client
HTTP
Client
How fast are
websockets?
EC2
US West
TCP Websocket
0.002ms 0.02ms
Websockets are
not magical :)
Full benchmarks:
http://eng.42go.com/
How fast are
websockets?
EC2
US 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 notification
I visited my user profile page
I visited the home page
from the client
to the client
What are my messages?
Here’s Your messages!
You have a new notification
I visited my user profile page
I visited the home page
Here’s a Message for Frank
Got your message
from the client
to the client
What are my messages?
Here’s Your messages!
You have a new notification
I visited my user profile page
I visited the home page
Here’s a Message for Frank
Got your message
Code 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
Iterators
val list = Seq(1,2,3,4,5)
list.foreach(println)
Container Function
Producer 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)
Consumer
This 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)
Producer
This 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()
undefined
got: 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
output
lets 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 learned
use debugging tools!
Lessons learned
use debugging tools!
Lessons learned
weird things can happen when you’re
depending on a long-lived socket
Lessons learned
weird 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/

Using Websockets with Play!

  • 1.
    Building real-time web appsusing WEBSOcKeTS WITH PLAY! andrew@42go.com @connerdelights
  • 2.
  • 3.
    How we used to doit request response
  • 4.
    How we used to doit request response new message!
  • 5.
    How we used to doit request response new message!
  • 6.
  • 7.
    The dynamic web needsreal-time communication
  • 8.
    The dynamic web needsreal-time communication
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
    Long Polling well supported,stillhalf-duplex,single req/resp
  • 19.
    These do nothandle high bursts of messages
  • 20.
    These do nothandle high bursts of messages Stuck with the single request → response model
  • 21.
    GET / HTTP/1.1 Host:www.google.com Connection: keep-alive Cache-Control: max-age=0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 User-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.36 DNT: 1 Accept-Encoding: gzip,deflate,sdch Accept-Language: en-US,en;q=0.8 Cookie: PREF=ID=e248d326d84eb3dc:FF=0:TM=1372622071:LM=1274622070:S=2bERIaHgSKRjWeC8; NID=47=CHAZZVkq40TcovIu- FuXlU0pF2UPfqqSEhNqx8hqUKnZ7-s4uxGjtBEFK7kRtTSVEu4fzJ00vhB4OrLRxw8JfV5EuiKczEC2_EHkBqr1kNwn_NdZ73XRl2umFybXYoiVD_ HTTP/1.1 200 OK Date: Tue, 23 Jul 2013 23:28:02 GMT Expires: -1 Cache-Control: private, max-age=0 Content-Type: text/html; charset=UTF-8 Set-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.com Content-Encoding: gzip Server: gws X-XSS-Protection: 1; mode=block X-Frame-Options: SAMEORIGIN Transfer-Encoding: chunked Inefficient Over 1kb in headers
  • 22.
  • 23.
  • 24.
    Can Play! do these? In fact,if you’re supporting older browsers, consider them! Yes!
  • 25.
  • 26.
  • 27.
    How fast are websockets? ouroffice EC2 US West
  • 28.
    How fast are websockets? ouroffice EC2 US West Average 10ms ping ~13ms mean improvement Full benchmarks: http://eng.42go.com/
  • 29.
    GET / HTTP/1.1 Host:www.google.com Connection: keep-alive Cache-Control: max-age=0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 User-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.36 DNT: 1 Accept-Encoding: gzip,deflate,sdch Accept-Language: en-US,en;q=0.8 Cookie: PREF=ID=e248d326d84eb3dc:FF=0:TM=1372622071:LM=1274622070:S=2bERIaHgSKRjWeC8; NID=47=CHAZZVkq40TcovIu- FuXlU0pF2UPfqqSEhNqx8hqUKnZ7-s4uxGjtBEFK7kRtTSVEu4fzJ00vhB4OrLRxw8JfV5EuiKczEC2_EHkBqr1kNwn_NdZ73XRl2umFybXYoiVD_ HTTP/1.1 200 OK Date: Tue, 23 Jul 2013 23:28:02 GMT Expires: -1 Cache-Control: private, max-age=0 Content-Type: text/html; charset=UTF-8 Set-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.com Content-Encoding: gzip Server: gws X-XSS-Protection: 1; mode=block X-Frame-Options: SAMEORIGIN Transfer-Encoding: chunked Over 1kb in headers
  • 30.
    How fast are websockets? InitialWebsocket transmission Websocket Client HTTP Client
  • 31.
    How fast are websockets? EC2 USWest TCP Websocket 0.002ms 0.02ms Websockets are not magical :) Full benchmarks: http://eng.42go.com/
  • 32.
    How fast are websockets? EC2 USWest TCP Websocket 0.002ms 0.02ms Still quite fast Full benchmarks: http://eng.42go.com/
  • 33.
    Websockets deal with streams ofdata from the client to the client
  • 34.
    What are mymessages? Here’s Your messages! You have a new notification from the client to the client
  • 35.
    What are mymessages? Here’s Your messages! You have a new notification I visited my user profile page I visited the home page from the client to the client
  • 36.
    What are mymessages? Here’s Your messages! You have a new notification I visited my user profile page I visited the home page Here’s a Message for Frank Got your message from the client to the client
  • 37.
    What are mymessages? Here’s Your messages! You have a new notification I visited my user profile page I visited the home page Here’s a Message for Frank Got your message Code push, reconnect please! from the client to the client
  • 38.
  • 39.
  • 40.
    val it =Seq(1,2,3,4,5).toIterator while(it.hasNext) { println(it.next()) } Iterators
  • 41.
    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
  • 42.
    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
  • 43.
    val list =Seq(1,2,3,4,5) list.foreach(println) Container Function Iterators
  • 44.
    Iterators val list =Seq(1,2,3,4,5) list.foreach(println) Container Function Producer Consumer
  • 45.
    Iteratee (ie, the consumer) Immutablyconsumes chunks from a Producer. Think: Iterates over a stream*.
  • 46.
    Iteratee (ie, the consumer) Immutablyconsumes chunks from a Producer. Think: Iterates over a stream*. Iteratees can actually do a bit more, but we’ll save that for another day.
  • 47.
    Enumerator (ie, the PRODUCER) Producestyped chunks. Only produces when there is a CONSUMER.
  • 48.
    Play! needs aproducer and consumer for a Websocket stream
  • 49.
    val in =Iteratee.foreach[JsArray](println)
  • 50.
    val in =Iteratee.foreach[JsArray](println) Consumer This iteratee will consume from an Enumerator (the client), printing the input to the console
  • 51.
    val in =Iteratee.foreach[JsArray](println) val out = Enumerator("You connected!").andThen(Enumerator.eof) Producer This Enumerator will be consumed by an iteratee (The client), sending a string then EOF
  • 52.
    def index =WebSocket.using[JsArray] { request => val in = Iteratee.foreach[JsArray](println) val out = Enumerator("You connected!").andThen(Enumerator.eof) (in, out) }
  • 53.
    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() undefined got: You connected! closed! CloseEvent {reason: "", code: 1005, wasClean: true, clipboardData:
  • 54.
    def echo =WebSocket.using[JsArray] { request => val (out, channel) = Concurrent.broadcast[JsArray] val in = Iteratee.foreach[JsArray](channel.push) (in, out) }
  • 55.
    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
  • 56.
  • 57.
    Channels... how about achat room? super simple ^
  • 58.
    Simplified version ofthe Play! Chat room websocket sample https://github.com/playframework/playframework/blob/master/samples/scala/websocket-chat
  • 59.
    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 = ???   }
  • 60.
    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 output lets us push data into the enumerator
  • 61.
    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 = ??? }
  • 62.
    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
  • 63.
    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 = ??? }
  • 64.
    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) } }
  • 65.
    object Application extendsController { 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) } } }
  • 66.
    This can bemodified to be a lightweight pub-sub system by creating many channels Play!
  • 67.
  • 68.
  • 69.
    Lessons learned weird thingscan happen when you’re depending on a long-lived socket
  • 70.
    Lessons learned weird thingscan happen when you’re depending on a long-lived socket clients lie voodoo when packet loss is high disconnects happen
  • 71.
    Lessons learned babysit connectionsin your application ping / pong
  • 72.
    Lessons learned your clientsneed to auto-reconnect (we’ll be open sourcing our reconnecting WS library soon!)
  • 73.
    Lessons learned be carefulabout thread safety val in = Iteratee.foreach[JsArray](/* handler */)
  • 74.
    Lessons learned make surethis guy is fast val in = Iteratee.foreach[JsArray](/* handler */)
  • 75.
  • 76.
  • 77.