CSC/ECE 517 Spring 2015/ch1a 12 LS
Live Streaming
Introduction
The purpose of live streaming is to send data over the Internet in real-time. This requires a data source, a data encoder, a media publisher and a proper network.
In Rails, the concept of streaming first appeared in version 3.1, and is further optimized in Rails 4. Now Rails 4 supports live streaming, which uses handling I/O object and can send data from server to client in real-time.
Details
Template streaming
Since Rail 3.1, ::Streaming module has been added to ActionController. For example, the loading progress in a traditional Rails client-side may look like this<ref> https://github.com/oggy/template_streaming</ref>:
This is because that layouts are rendered before the content, thus causing control flow to be altered to render the page in the order that the client needs (layout first).But for some resources which are static, if they could be loaded while waiting for the server response, a lot of time can be saved.
The module ::Streaming inverts the regular Rails process of rendering the layout and template. Using ::Streaming, Rails will render template first and then layout. It will first run method yield and load up the template, and then renders the assets and layouts. In this way, the progress with streaming looks like this:
When to use streaming
For actions like new or edit, it may not be that necessary to use streaming. But for expensive actions like database query and generation of large amount of data, streaming can be extremely important and effective. Take the following action as an example:
def storyboard @stories = Story.all @developers = Page.all @comments = Comment.all end
Since most of the queries here are executing in the controller, it might take a while before them to finish, thus causing the front end (user) to wait for long. We can solve this problem and enhance user experience, the above code can be rewrite as follows,
def storyboard # Allow lazy execution of the queries @stories = Story.all @developers = Page.all @comments = Comment.all render :stream => true end
(Note: :Streaming only works with templates. It doesn’t work with :json or :xml)<ref>http://apidock.com/rails/ActionController/Streaming</ref>
Tweaking views
When using streaming, instance variables that are set in the template and are used in the layout won’t need some tweak to work well. This is done by using content_for, provide and yield. Here’s a simple example where the layout expects the template to tell which title to use<ref>http://apidock.com/rails/ActionController/Streaming</ref>:
<html> <head><title><%= yield :title %></title></head> <body><%= yield %></body> </html>
Instead of yield, we can also use content_for to specify the title:
<%= content_for :title, "Main" %> Hello
And the final result would be:
<html> <head><title>Main</title></head> <body>Hello</body> </html>
However, if content_for is called several times, the final result would have all calls concatenated.
For instance, if we have the following template:
<%= content_for :title, "Main" %> Hello <%= content_for :title, " page" %>
The final result would be:
<html> <head><title>Main page</title></head> <body>Hello</body> </html>
This means that, if you have yield :title in your layout and you want to use streaming, you would have to render the whole template (and eventually trigger all queries) before streaming the title and all assets, which kills the purpose of streaming. For this reason Rails 3.1 introduces a new helper called provide that does the same as content_for but tells the layout to stop searching for other entries and continue rendering.
For instance, the template above using provide would be:
<%= provide :title, "Main" %> Hello <%= content_for :title, " page" %>
Giving:
<html> <head><title>Main</title></head> <body>Hello</body> </html>
That said, when streaming, you need to properly check your templates and choose when to use provide and content_for.
Live Streaming
The module ::Live was then added to ActionController at Rail 4. The difference between Streaming and Live is that Streaming is implicitly included whenever you create a new controller class, while ::Live requires explicit include.
Traditional Response
In this type of communication between server and client, data will be buffered until all render works are finished, and everything will be sent to the client at the same time. And the stream object behaves just like an IO object. Here’s the sample code:
class MessageController < ApplicationController def message response.headers["Content-Type"] = "text/event-stream" 8.times { response.stream.write "This is a response message!\n" sleep 1 } ensure response.stream.close end end
When we run the code above, the web page will keep loading and remain blank for 8 seconds, and shows all 8 lines of “This is a response message!” altogether, as with the following image shows:
Live Streaming
In order to enable live streaming, we need to include a module named ActionController::Live. When this module is mixed into the controller, all actions within that controller will be able to stream data to the client in real time, like the following code:
class MessageController < ApplicationController #here we enabled live streaming include ActionController::Live def message response.headers["Content-Type"] = "text/event-stream" 8.times { response.stream.write "This is a live streaming message!\n" sleep 1 } ensure response.stream.close end end
The above code looks exactly the same as the traditional response, except the newly included module ActionController::Live. Now data can be streamed to the client immediately whenever write method is called. When we run the code above, the web page will print one line “This is a response message!” for every seconds and repeats 8 times.<ref>http://tenderlovemaking.com/2012/07/30/is-it-live.html</ref> Like the image shows below:
Server-Sent Event
Server-sent event (SSE) is a technology where browser can automatically receive update data from a server via HTTP connection. Whenever server sends data, the SSE method in the browser will trigger an event.The SSE EventSource API is standardized as part of HTML5 by the W3C. It can be used together with the Live API to achieve full-duplex communication.<ref>https://en.wikipedia.org/wiki/Server-sent_events</ref>
A simple SSE looks like the following :
require 'json' module ServerSide class SSE def initialize io @io = io end def write object, options = {} options.each do |k,v| @io.write "#{k}: #{v}n" end @io.write "data: #{object}nn" end def close @io.close end end end
This module assigns the I/O stream object to a hash and converts it into a key-value pair so that it is easy to read, store, and send it back in JSON format.
Now we can wrap stream object inside the SSE class. By doing this, we need to include SSE module inside the controller, so that the opening and closing of connections will be managed by SSE module. And we also add the ensure clause to make sure that the connection will always be closed after execution.
require 'sse' class MessagingController < ApplicationController include ActionController::Live def stream response.headers['Content-Type'] = 'text/event-stream' sse = SSE.new(response.stream) begin 5.times { sse.write(“Streaming using SSE!”) sleep 1 } ensure sse.close end end end
The code above will print the sentence “Streaming using SSE!” once per second, until the loop ends.
Here’s another sample showing how to use SSE to stream data<ref>https://github.com/rails/rails/blob/6061c540ac7880233a6e32de85cec72c20ed8778/actionpack/lib/action_controller/metal/live.rb</ref>:
# This class provides the ability to write an SSE (Server Sent Event) # to an IO stream. The class is initialized with a stream and can be used # to either write a JSON string or an object which can be converted to JSON. # # Writing an object will convert it into standard SSE format with whatever # options you have configured. You may choose to set the following options: # # 1) Event. If specified, an event with this name will be dispatched on # the browser. # 2) Retry. The reconnection time in milliseconds used when attempting # to send the event. # 3) Id. If the connection dies while sending an SSE to the browser, then # the server will receive a +Last-Event-ID+ header with value equal to +id+. # # After setting an option in the constructor of the SSE object, all future # SSEs sent across the stream will use those options unless overridden. # # Example Usage: # # class MyController < ActionController::Base # include ActionController::Live # # def index # response.headers['Content-Type'] = 'text/event-stream' # sse = SSE.new(response.stream, retry: 300, event: "event-name") # sse.write({ name: 'John'}) # sse.write({ name: 'John'}, id: 10) # sse.write({ name: 'John'}, id: 10, event: "other-event") # sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500) # ensure # sse.close # end # end # # Note: SSEs are not currently supported by IE. However, they are supported # by Chrome, Firefox, Opera, and Safari.
Note: By default Rails provides a one-way communication process by writing the stream to the client when data is available. However, if we can add SSEs, we can enable events and responses, thus making it two-way.
Benefit of live streaming
1.Send partial responses to the client immediately.
2.Using Server-sent events, the ability to create chat clients, push notifications, and real-time feeds are available within Rails itself
3.Continuously inform the user about the progress.
See example below<ref>http://blog.phusion.nl/2012/08/03/why-rails-4-live-streaming-is-a-big-deal/</ref>:
def big_work work = WorkModel.new while !work.done? work.do_some_calculations response.stream.write "Progress: #{work.progress}%\n" end response.stream.close end
Troubleshooting
When running streaming programs, we need to install PUMA to substitute the original WEBrick server, because puma can handle concurrency better. In order to do this, we need to add the following line to Gemfile:
gem 'puma'
After installed puma, if we start rails server, the console will print the following output:
"C:\Program Files (x86)\JetBrains\RubyMine 7.0.2\bin\runnerw.exe" C:\RailsInstaller\Ruby2.0.0\bin\ruby.exe -e $stdout.sync=true;$stderr.sync=true;load($0=ARGV.shift) D:/Cloud/Coding/RubymineProjects/LiveStreamingExample/bin/rails server -b 127.0.0.1 -p 3000 -e development => Booting Puma => Rails 4.1.8 application starting in development on http://127.0.0.1:3000 => Run `rails server -h` for more startup options => Ctrl-C to shutdown server Puma 2.11.0 starting... * Min threads: 0, max threads: 16 * Environment: development * Listening on tcp://127.0.0.1:3000
This means that PUMA is set as our default server and successfully booted.
We encountered failure on Windows when trying to install PUMA, the console printed the following error:
Building native extensions. This could take a while... ERROR: Error installing rails: ERROR: Failed to build gem native extension. /Users/WillieTran/.rvm/rubies/ruby-2.0.0-p247/bin/ruby extconf.rb *** extconf.rb failed *** Could not create Makefile due to some reason, probably lack of necessary libraries and/or headers. Check the mkmf.log file for more details. You may need configuration options.
To solve this problem, we need to install openssl for windows. And then run the following command<ref>https://github.com/hicknhack-software/rails-disco/wiki/Installing-puma-on-windows</ref>:
gem install puma -- --with-opt-dir=c:\openssl
References
<references>