Recent years have seen the rise of "the real-time web." Web apps we use every day rely on real-time features—things like new posts magically appearing at the top of your feeds or real time collaboration tools like Google Docs without the user having to refresh the page.*
- Polling.
Polling or Ajax Polling is the process in which the client periodically requests data from the server.
- Long Polling.
In Long Polling the client makes a request to the server to load a resource, If it has the necessary data then the server responds with the data otherwise the request is held on to until the necessary resource is available. Once the client gets the resource it restarts the process with the request.
- Server-sent events(SSE).
SSE is a standard where the client requests server for a resource, this request is held on to and subsequent iterations with this client will happen over the created connection via events.
- Web sockets.
Are protocol buit over the TCP/IP protocol. This allows the connection between the client and the server to be "bi-directional" (full-duplex).
- Commet.
Is an umbrella term encompassing multiple techniques for achieving client-server interaction.
HTML operates using a request-response protocol. That is, a request is made to the server and the server responds with either the necessary data or an error.
Think of this as talking on a walkie-talkie only one of the participants get to talk at a time. And usually we need some means of communicating that one of the parties is "done", ("over").
Not only is this old fashioned but leads to a very bad user experience (UX).
Real-time web enables both (or all) of the participants to interact with one another just as they would in real life. Think of a Skype call. Where all the users can talk at once no restrictions !!
Action Cable implements WebSockets. And as the docs quotes
It's a full-stack offering that provides both a client-side JavaScript framework and a server-side Ruby framework. You have access to your full domain model written with Active Record or your ORM of choice.
Before actually implementing the code. Let us understand how Action Cable implements WebSockets (Opening a connection and maintaining it).
Action Cable can be configured to run as a stand-alone server or as a process in the main Application. (We will be using the second approach as it will be a lot easier to implement).
Action Cable utilises the Pub/Sub approach to communicate data between the server and client(s).
-
To show case the power of web-sockets and Action Cable we will be building a collaborative board. Where all connected users can well draw. (I feel showing another chat application would not make sense). When done it will look like shown below.
-
ruby -v => 2.3.0
rails -v => 5.0.2
With the above two dependencies installed. We are now ready to generate the application.
-
rails new action-cable-demo
-
cd action-cable-demo && rails g controller boards index
This will create a controller called
BoardsController
with one actionindex
and generate the necessary views. -
We will be using HTML's <canvas> as the base. Which will enable us to draw stuff on. Open up
app/views/boards/index.html
and replace the content with
<div id='main-content'>
<canvas id='board' width='400' height='400' style="position:absolute;top:1%;left:10%;border:2px solid;"></canvas>
<div style="position:absolute;top:12%;left:43%;">Choose Color</div>
<div style="position:absolute;top:15%;left:45%;width:10px;height:10px;background:green;" id="green"></div>
<div style="position:absolute;top:15%;left:46%;width:10px;height:10px;background:blue;" id="blue"></div>
<div style="position:absolute;top:15%;left:47%;width:10px;height:10px;background:red;" id="red"></div>
<div style="position:absolute;top:17%;left:45%;width:10px;height:10px;background:yellow;"id="yellow"></div>
<div style="position:absolute;top:17%;left:46%;width:10px;height:10px;background:orange;"id="orange"></div>
<div style="position:absolute;top:17%;left:47%;width:10px;height:10px;background:black;" id="black"></div>
<div style="position:absolute;top:20%;left:43%;">Eraser</div>
<div style="position:absolute;top:24%;left:45%;width:15px;height:15px;background:white;border:2px solid;" id="white"></div>
<img id="canvasimg" style="position:absolute;top:10%;left:52%;" style="display:none;">
</div>
<input type="button" value="clear" id="clr" size="23" style="position:absolute;top:65%;left:10%;">
-
Start up the webserver from the root of your project folder with
rails s
and visitlocalhost:3000
in your borwser and you should see a board with some colours. -
open
app/assets/javascripts/boards.coffee
and paste in this code.$(document).ready -> canvas = undefined window.ctx = undefined w = undefined h = undefined flag = false prevX = 0 currX = 0 prevY = 0 currY = 0 dot_flag = false x = 'black' #default color y = 3 #Size of the line. $("#green").on 'click', -> color(this) $("#blue").on 'click', -> color(this) $("#red").on 'click', -> color(this) $("#yellow").on 'click', -> color(this) $("#orange").on 'click', -> color(this) $("#black").on 'click', -> color(this) $("#white").on 'click', -> color(this) $("#clr").on 'click', -> erase() init = -> canvas = $("#board")[0] window.ctx = canvas.getContext('2d') w = canvas.width h = canvas.height canvas.addEventListener 'mousemove', ((e) -> findxy 'move', e return ), false canvas.addEventListener 'mousedown', ((e) -> findxy 'down', e return ), false canvas.addEventListener 'mouseup', ((e) -> findxy 'up', e return ), false canvas.addEventListener 'mouseout', ((e) -> findxy 'out', e return ), false return # Set the sellected color color = (obj) -> x = obj.id if x == 'white' y = 14 else y = 3 draw = -> ctx.beginPath(); ctx.moveTo(prevX, prevY); ctx.lineTo(currX, currY); ctx.strokeStyle = x; ctx.lineWidth = y; ctx.stroke(); ctx.closePath(); erase = -> m = confirm("Want to clear") if m ctx.clearRect(0, 0, w, h) $("#canvasimg")[0].style.display = "none" findxy = (res, e) -> if res == 'down' prevX = currX prevY = currY currX = e.clientX - (canvas.offsetLeft) currY = e.clientY - (canvas.offsetTop) flag = true dot_flag = true if dot_flag window.ctx.beginPath() window.ctx.fillStyle = x window.ctx.fillRect currX, currY, 2, 2 window.ctx.closePath() dot_flag = false if res == 'up' or res == 'out' flag = false if res == 'move' if flag prevX = currX prevY = currY currX = e.clientX - (canvas.offsetLeft) currY = e.clientY - (canvas.offsetTop) draw() #Initialize the board init()
With this if we refresh the page we should be able to draw on the canvas.
-
With this all subscribers get to see what we are drawing and collobrate with us.
Open
config/routes.rb
and addmount ActionCable.server => '/cable'
Open
app/views/layouts/application.html.erb
and add (to the head section)<%= action_cable_meta_tag %>
-
rails gives us a generator to generate channels just like with
controllers
,models
andmigrations
rails g channel board draw clear
[Think of this as generating a controller with actions]Running this will generate two files; a javascript file located in
app/assets/javascripts/channels/board.coffee
and a ruby file located inapp/channels/board_channel.rb
.Let's analyze the ruby file first:
class BoardChannel < ApplicationCable::Channel def subscribed # stream_from "some_channel" end def unsubscribed # Any cleanup needed when channel is unsubscribed end def draw end def clear end end
The
subscribed
method is a default that’s called when a client connects to the channel, and it’s usually used tosubscribe
the client to listen to changes. Thedraw
andclear
actions are custom actions that we created when we ran the generator. It will be used to receive data from its client-side representation.App.board = App.cable.subscriptions.create "BoardChannel", connected: -> # Called when the subscription is ready for use on the server disconnected: -> # Called when the subscription has been terminated by the server received: (data) -> # Called when there's incoming data on the websocket for this channel draw: -> @perform 'draw' clear: -> @perform 'clear'
The first line of the above code
App.board = App.cable.subscriptions.create "BoardChannel"
is where the magic happens. This initializes the client and subscribes to the server via a connection called "board_channel" -
With the framework setup let us now change the draw and clear method such that it actually transmits the events over the socket and updates all subscribers.
Modify
app/assets/javascripts/channels/board.coffee
's receive, draw and clear methods to:
App.board = App.cable.subscriptions.create "BoardChannel",
connected: ->
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
data = data.data
if data.message == "true"
m = confirm('Are you sure you want to clear ?')
if m
window.ctx.clearRect 0, 0, data.w, data.h
$("#canvasimg")[0].style.display = 'none'
else
window.ctx.beginPath()
window.ctx.moveTo data.prevX, data.prevY
window.ctx.lineTo data.currX, data.currY
window.ctx.strokeStyle = data.strokeStyle
window.ctx.lineWidth = data.lineWidth
window.ctx.stroke()
window.ctx.closePath()
draw: (data) ->
@perform 'draw', data: data
clear: (data) ->
@perform 'clear', data: data
And the erase
and draw
functions in our app/javascripts/boards.coffee
to be
draw = ->
App.board.draw({
currX: currX,
currY: currY,
prevX: prevX,
prevY: prevY,
strokeStyle: x,
lineWidth: y,
})
erase = ->
App.board.clear({message: "true", w: w, h: h})
This makes the publising part of our pub/sub complete. Let us handel the data that we are getting and boardcast it to all subscribers.
Edit app/channels/board_channel.rb
to receive the data sent from the server and publish it to all Subscribes.
class BoardChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def draw(data)
ActionCable.server.broadcast "room_channel", data: data['data']
end
def clear(data)
ActionCable.server.broadcast "room_channel", data: data['data']
end
end
NOTE The use of
ActionCable.server.broadcast
in the channels ! This was done to show that the boardcast method is accessible across the rails application. So if need be we could be broadcasting events from say a long running background job, to notify that it is complete.
-
Open up a new window and navigate to
http://localhost:3000
Drawing content on one should reflect on the other window as well. -
Deploying the application to Heroku.
- Login to Heroku.
- Create a new application
- add heroku origin.
- Replace
sqlite3
inGemfile
topg
. - update
config/database.yml
to includepg
connection parameters. - Since we are not using redis to handle our socket connections we have to make changes to
config/cable.yml
and changeadapter: async
That about sums up the basics of Real-Time Web and how rails and action cable makes it trivial to create real-time applications. Hope you have enjoyed this tutorial, you can visit a working version of this application on Heroku