12/08/2018, 14:25

Streaming trong RAILS 4

Streaming là gì? Streaming đã được sử dụng trong Rails từ phiên bản 3.2 tuy nhiên nó bị giới hạn chỉ sử dụng streaming template. Rail 4 mạnh mẽ hơn và khả năng streaming thời gian thực. Có nghĩa là Rail từ giờ có khả năng xử lý các đối tượng I/O nguyên bản cùng khả năng gửi dữ liệu tới client ...

Streaming là gì?

Streaming đã được sử dụng trong Rails từ phiên bản 3.2 tuy nhiên nó bị giới hạn chỉ sử dụng streaming template. Rail 4 mạnh mẽ hơn và khả năng streaming thời gian thực. Có nghĩa là Rail từ giờ có khả năng xử lý các đối tượng I/O nguyên bản cùng khả năng gửi dữ liệu tới client theo thời gian thực.

Streaming và Live là 2 modun riêng biệt được viết trong ActionController. Streaming là mặc định trong khi Live cần được định nghĩa trong controller.

Streaming API sử dụng class Fiber của Ruby (có từ 1.9.2). Fiber cung cấp các khối thread được xây dựng sẵn trong Ruby. Fiber gọi các thread và điều khiển chúng dừng hoặc tiếp tục.

Template Streaming

Streaming sẽ đảo ngược quá trình render layout và template của Rails thông thường. Mặc định, Rails sẽ render template đầu tiên và sau đó mới đến layout. yeild sẽ được chạy đầu tiên, sau đó load lên template, sau đó assets và layout được render.

Hãy xem xét ví dụ sau, hiển thị timeline của nhiều class:

class TimelineController
  def index
    @users = User.all
    @tickets = Ticket.all
    @attachments = Attachment.all
  end
end

Ở đây, sử dụng streaming khả phù hợp vì nếu tải bình thường, trang này sẽ mất rất nhiều thời gian để lấy tất cả dữ liệu trước.

Cách áp dụng streaming:

class TimelineController
  def index
    @users = User.all
    @tickets = Ticket.all
    @attachments = Attachment.all

    render stream: true
  end
end

Khi sử dụng render stream: true, Rails sẽ load query từ từ và cho phép chúng chạy sau khi assets và layout được render xong. Nhưng streaming chỉ làm việc với template và không sử dụng được với các dạng khác như json hoặc xml.

Passing the Stuff in Between

Streaming thay đổi cách thức render template và layout, điều này gây ra một issue. Việc gọi database sẽ không được thực hiện khi template được render xong, các logic và view sử dụng biến instance sẽ bị lỗi.

Vì thế, để tải các thuộc tính title hoặc meta, chúng ta cần sử dụng content_for thay cho yeild. Tất nhiên với body thì ta vẫn có thể sử dụng yeild.

Going Live với Live API

Live là một modun đặc biệt trong ActionController. Nó cho phép Rails mở và đóng stream. Hãy cùng tạo một ứng dụng đơn giản sử dụng Live.

Bình thường trên local, Rails sẽ sử dụng WEBrick nhưng WEBrick sẽ không làm việc với live streaming vì thế chúng ta sẽ sử dụng Puma.

Gemfile
gem "puma"
:~/testapp$ bundle install

Puma tích hợp vào Rails vì thế ta có thể sử dụng rails s để khởi động server, nó có cùng port với WEBrick

:~/testapp$ rails s
=> Booting Puma
=> Rails 4.0.0 application starting in development on http://0.0.0.0:3000
=> Run `rails server -h` for more startup options
=> Ctrl-C to shutdown server
Puma 2.3.0 starting...
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://0.0.0.0:3000

Giờ ta sẽ tạo controller để send messages

:~/testapp$ rails g controller messaging

class MessagingController < ApplicationController
  include ActionController::Live

  def send_message
    response.headers['Content-Type'] = 'text/event-stream'
    10.times {
      response.stream.write "This is a test Messagen"
      sleep 1
    }
    response.stream.close
  end
end

#routes.rb
get 'messaging' => 'messaging#send_message'

Ta sẽ access vào bằng curl như sau:

~/testapp$ curl -i http://localhost:3000/messaging
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-UA-Compatible: chrome=1
Content-Type: text/event-stream
Cache-Control: no-cache
Set-Cookie: request_method=GET; path=/
X-Request-Id: 68c6b7c7-4f5f-46cc-9923-95778033eee7
X-Runtime: 0.846080
Transfer-Encoding: chunked

This is a test message
This is a test message
This is a test message
This is a test message

Khi chúng ta gọi send_method, Puma khởi tạo 1 thread mới và xử lý data streaming cho 1 cliend trong thread này. Mặc định Puma cho phép 16 thread đồng thời, tức là 16 clients đồng thời (config này có thể thay đổi).

Thay đổi 1 chút, chúng ta sẽ build 1 form và send data ra view:

def index
end

def send_message
  response.headers['Content-Type'] = 'text/event-stream'
  10.times {
    response.stream.write "#{params[:message]}n"
    sleep 1
  }
  response.stream.close
end

#views/messages/index.html.erb
<%= form_tag messaging_path, :method => 'get' do %>
  <%= text_field_tag :message, params[:message] %>
  <%= submit_tag "Post Message" %>
<% end %>

#routes
root 'messaging#index'
get  'messaging' => 'messaging#send_message', :as => 'messaging'

Ngay khi bạn nhập message và ấn Post Message, browser sẽ nhận response stream như một text file để download chứa message được ghi 10 lần.

Ở đây, tất nhiên stream không biết nơi gửi data hoặc format là gì, do đó nó viết vào text file trên server.

Bạn có thể kiểm tra bằng cách gửi param:

:~/testapp$ curl -i http://localhost:3000/messaging?message="awesome"

HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-UA-Compatible: chrome=1
Content-Type: text/event-stream
Cache-Control: no-cache
Set-Cookie: request_method=GET; path=/
X-Request-Id: 382bbf75-7d32-47c4-a767-576ec59cc364
X-Runtime: 0.055470
Transfer-Encoding: chunked

awesome
awesome

Server Side Events (SSEs)

HTML5 có một method gọi là Server Side Events (SSEs). Nó là một method có sẵn trên browver, sẽ xác nhận và thực hiện event bất cứ khi nào server gửi data.

Bạn có thể sử dụng SSE kết hợp với LIVE API để thực hiện giao tiếp full-duplex.

Mặc định, Rails cung cấp một tiến trình giao tiếp một chiều bằng cách viết các stream tới client khi data sẵn sàng. Tuy nhiên, nếu thêm SSEs, chúng ta có thể kích hoạt các event và response hoạt động 2 chiều.

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

Modun này gán object stream I/O thành một hash và chuyển nó thành cặp key-value vì thế sẽ rất dễ dàng để đọc, lưu và gửi nó thành định dạng JSON.

require 'server_side/sse'

class MessagingController < ApplicationController
  include ActionController::Live

  def index
  end

  def stream
    response.headers['Content-Type'] = 'text/event-stream'
    sse = ServerSide::SSE.new(response.stream)
    begin
      loop do
        sse.write({ :message => "#{params[:message]}" })
        sleep 1
      end
    rescue IOError
    ensure
      sse.close
    end
  end
end

Chúng ta có response như sau:

:~/testapp$ curl -i http://localhost:3000/messaging?message="awesome"
HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-UA-Compatible: chrome=1
Content-Type: text/event-stream
Cache-Control: no-cache
Set-Cookie: request_method=GET; path=/
X-Request-Id: b922a2eb-9358-429b-b1bb-015421ab8526
X-Runtime: 0.067414
Transfer-Encoding: chunked

data: {:message=>"awesome"}

data: {:message=>"awesome"}

Những điểm cần lưu ý

  • Tất cả stream phải được closed nếu không nó sẽ mở mãi mãi.
  • Bạn phải chắc chắn code của mình là threadsage, như controller luôn phải tạo ra một thread mới khi method được gọi.
  • Sau commit đầu tiên của response, header không thể được ghi lại với write hoặc close

Tham khảo

https://www.sitepoint.com/streaming-with-rails-4/

0