CSC/ECE 517 Spring 2015/ch1a 12 LS

From Expertiza_Wiki
Jump to navigation Jump to search

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.

Examples

Template Streaming

Some basic knowledge about how web pages are rendered can be found here and also here.

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>Template Streaming, George Ogata https://github.com/oggy/template_streaming</ref>:

Figure 1 progress without streaming

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:

Figure 2 progress with streaming


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>ActionController::Streaming 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>ActionController::Streaming 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 Aproach

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>Is it live? 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>Server-sent events 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.

Conclusion

The supporting for live streaming has deep impact on Rails. It not only makes Rails more competitive with Node.js, but also opens a gate for Rails to embrace lots of new areas and applications that it does not fits well before. The main advantage of Rails can be summarized as follows:

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>Why Rails 4 Live Streaming is a big deal, Hongli Lai 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>Installing puma on windows, Robert Kranz https://github.com/hicknhack-software/rails-disco/wiki/Installing-puma-on-windows</ref>:

gem install puma -- --with-opt-dir=c:\openssl


References

<references>