Using Websockets with Play!

9,089 views
9,122 views

Published on

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

Published in: Technology
0 Comments
12 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
9,089
On SlideShare
0
From Embeds
0
Number of Embeds
5,547
Actions
Shares
0
Downloads
61
Comments
0
Likes
12
Embeds 0
No embeds

No notes for slide

Using Websockets with Play!

  1. 1. Building real-time web apps using WEBSOcKeTS WITH PLAY! andrew@42go.com @connerdelights
  2. 2. How we used to do it request
  3. 3. How we used to do it request response
  4. 4. How we used to do it request response new message!
  5. 5. How we used to do it request response new message!
  6. 6. The web has changed (quite a bit)
  7. 7. The dynamic web needs real-time communication
  8. 8. The dynamic web needs real-time communication
  9. 9. short polling
  10. 10. short polling
  11. 11. short polling
  12. 12. short polling Resource intensive,slow, limited toone response
  13. 13. Chunked Responses
  14. 14. Chunked Responses Hacky, no error handling,half-duplex
  15. 15. Long Polling
  16. 16. Long Polling
  17. 17. Long Polling
  18. 18. Long Polling well supported,still half-duplex,single req/resp
  19. 19. These do not handle high bursts of messages
  20. 20. These do not handle high bursts of messages Stuck with the single request → response model
  21. 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. 22. Can Play! do these?
  23. 23. Can Play! do these? Yes!
  24. 24. Can Play! do these? In fact, if you’re supporting older browsers, consider them! Yes!
  25. 25. Websockets
  26. 26. Websockets Full duplex,efficient, fast,only newer* browsers
  27. 27. How fast are websockets? our office EC2 US West
  28. 28. How fast are websockets? our office EC2 US West Average 10ms ping ~13ms mean improvement Full benchmarks: http://eng.42go.com/
  29. 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. 30. How fast are websockets? Initial Websocket transmission Websocket Client HTTP Client
  31. 31. How fast are websockets? EC2 US West TCP Websocket 0.002ms 0.02ms Websockets are not magical :) Full benchmarks: http://eng.42go.com/
  32. 32. How fast are websockets? EC2 US West TCP Websocket 0.002ms 0.02ms Still quite fast Full benchmarks: http://eng.42go.com/
  33. 33. Websockets deal with streams of data from the client to the client
  34. 34. What are my messages? Here’s Your messages! You have a new notification from the client to the client
  35. 35. 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
  36. 36. 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
  37. 37. 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
  38. 38. How does Play! handle websockets?
  39. 39. How does Play! handle websockets? let’s step back a bit...
  40. 40. val it = Seq(1,2,3,4,5).toIterator while(it.hasNext) { println(it.next()) } Iterators
  41. 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. 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. 43. val list = Seq(1,2,3,4,5) list.foreach(println) Container Function Iterators
  44. 44. Iterators val list = Seq(1,2,3,4,5) list.foreach(println) Container Function Producer Consumer
  45. 45. Iteratee (ie, the consumer) Immutably consumes chunks from a Producer. Think: Iterates over a stream*.
  46. 46. 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.
  47. 47. Enumerator (ie, the PRODUCER) Produces typed chunks. Only produces when there is a CONSUMER.
  48. 48. Play! needs a producer and consumer for a Websocket stream
  49. 49. val in = Iteratee.foreach[JsArray](println)
  50. 50. val in = Iteratee.foreach[JsArray](println) Consumer This iteratee will consume from an Enumerator (the client), printing the input to the console
  51. 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. 52. def index = WebSocket.using[JsArray] { request => val in = Iteratee.foreach[JsArray](println) val out = Enumerator("You connected!").andThen(Enumerator.eof) (in, out) }
  53. 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. 54. def echo = WebSocket.using[JsArray] { request => val (out, channel) = Concurrent.broadcast[JsArray] val in = Iteratee.foreach[JsArray](channel.push) (in, out) }
  55. 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. 56. Channels
  57. 57. Channels... how about a chat room? super simple ^
  58. 58. Simplified version of the Play! Chat room websocket sample https://github.com/playframework/playframework/blob/master/samples/scala/websocket-chat
  59. 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. 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. 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. 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. 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. 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. 65. 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) } } }
  66. 66. This can be modified to be a lightweight pub-sub system by creating many channels Play!
  67. 67. Lessons learned use debugging tools!
  68. 68. Lessons learned use debugging tools!
  69. 69. Lessons learned weird things can happen when you’re depending on a long-lived socket
  70. 70. 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
  71. 71. Lessons learned babysit connections in your application ping / pong
  72. 72. Lessons learned your clients need to auto-reconnect (we’ll be open sourcing our reconnecting WS library soon!)
  73. 73. Lessons learned be careful about thread safety val in = Iteratee.foreach[JsArray](/* handler */)
  74. 74. Lessons learned make sure this guy is fast val in = Iteratee.foreach[JsArray](/* handler */)
  75. 75. http://caniuse.com/websockets
  76. 76. http://caniuse.com/websockets
  77. 77. Find me: andrew@42go.com @connerdelights Questions? http://eng.42go.com/

×