Những năm gần đây, khái niệm web thời gian thực được nhắc tới rất nhiều. Nếu quan sát các ứng dụng mạng xã hội, bạn sẽ thấy các bài viết, thông báo, hay khi chat, bạn nhận được thông tin mới rất ảo diệu.

Để thực hiện công việc đó, chúng ta có thể sử dụng phương thức HTTP, thực hiện request để lấy data. Trong khi, real-time web, thì không hể ràng buộc việc đó, nó cho phép người dụng nhận thông tin mới từ máy chủ ngay khi nó xuất hiện - không cần thực hiện request nào.

Có rất nhiều công nghệ để thực hiện chức năng thời gian thực như vậy, nhưng WebSocket protocol nổi lên như một công nghệ nổi bật từ khi được phát triển từ năm 2009. Tuy nhiên, tới tận thời gian gần đây, việc thực thi WebSocket protocol trong Rails rất khó. Chúng ta cần sử dụng các thư viện thứ 3 như Faye hay sử dụng thư viện Javascript. Hãy cùng tìm hiểu về WebSocket và cách mà Rails 5 hỗ trợ real-time với Action Cable

 WebSockets là gì?

WebSockets là phương thức được xây dựng trên TCP. Chúng duy trì kết nối tới máy chủ, nhờ đó mà máy chủ có thể gửi các thông tin tới máy khách, kể cả trong khi không có yêu cầu từ máy khách.

Với sự hỗ trợ của Action Cable trong Rails 5, chúng ta có thể thực thi WebSockets theo chuẩn thiết kể của Rails. 

 

Giới thiệu về Action Cable

Trong docs, nó được giới thiệu là "full-stack offering": Nó cung cấp cả client-side JavaScript framework, và Ruby server-side framework. Vì nó gắn liền với Rails, nên chúng ta sẽ phải truy cập tới các models từ trong các WebSocket workers

Vậy làm sao mà Action Cable có thể tạo và duy trì kết nối WebSocket bên trong một ứng dụng Rails 5?

Action Cable có thể chạy như một server riêng rẽ, hoặc chúng ta có thể thiết lập để nó chạy trên bên trong server của ứng dụng Rails. 

Action Cable thông qua Rack socket để quản lý các kết nối tới server, đa luồng, tạo nên các kênh kết nối. Với mối kênh kết nối tới sub-URI của ứng dụng để truyền dữ liệu từ những vùng nhất định của dự án tới các vùng khác.

Action Cable cung cấp code để truyền dữ liệu của những nội dung nhất định ( tin nhắn, thông báo ... ) thông qua kênh truyền, tới subscriber ( vùng đang kết nối ). Subscriber được khởi tạo phía client với một đoạn code Javascript, sử dụng Jquery để thêm nội dung vào DOM.

Cuối cùng, Action Cable sử dụng Redis để lưu trữ các dữ liệu tạm thời, đồng bộ dự liệu giữa các phần của dự án.

Tiếp theo, chúng ta sẽ xây dựng một ứng dụng chat cơ bản trong Rails 5.

Xây dựng ứng dụng chat Real-Time sử dụng Action Cable

Ứng dụng cho phép người dùng có thể đăng kí, đăng nhập, tạo một chat room hoặc chọn một chat room đã có sẵn và bắt đầu gửi tin nhắn. Chúng ta sẽ sử dụng Action Cable để chắc chắn rằng: Bất kì người dùng nào trong chat room đều sẽ nhận được tin nhắn trong phòng chat bất cứ khi nào có tin nhắn mới mà không cần tải lại trang web.

rails new action-cable-example --database=postgresql
  1. Đầu tiên, các bạn cần cài đặt Ruby 2.3.0 và Rails 5.0
  2. Khởi tạo dự án 
  3.  Add gem redis và gem puma 
    gem 'redis', '~> 3.0'
    gem 'puma'
  4. Then bundle install

Domain Model

Chúng ta sẽ có users, chat roomsmessages. Chat room sẽ có topic và nhiều messages, message sẽ có content, và thuộc về user và một chatroom nào đó. User sẽ có username, và sẽ có nhiều messages

Chúng ta sẽ lược bớt quá trình tạo model, routes, controllers cơ bản trong tutorial này.

Đối với chatroom controller, chúng ta sẽ cần một action show

# app/controllers/chatrooms_controller.rb

class ChatroomsController < ApplicationController
  ...

  def show
    @chatroom = Chatroom.find_by(slug: params[:slug])
    @message = Message.new
  end
end

Chatroom view sẽ render partial messages

# app/views/chatrooms/show.html.erb

<div class="row col-md-8 col-md-offset-2">
  <h1><%= @chatroom.topic %></h1>

<div class="panel panel-default">
  <% if @chatroom.messages.any? %>
    <div class="panel-body" id="messages">
      <%= render partial: 'messages/message', collection: @chatroom.messages%>
    </div>
  <%else%>
    <div class="panel-body hidden" id="messages">
    </div>
  <%end%>
</div>

  <%= render partial: 'messages/message_form', locals: {message: @message, chatroom: @chatroom}%>
</div>

Chúng ta cần 1 form để tạo message mới

# app/views/messages/_message_form.html.erb

<%=form_for message, remote: true, authenticity_token: true do |f|%>
  <%= f.label :your_message%>:
  <%= f.text_area :content, class: "form-control", data: {textarea: "message"}%>

  <%= f.hidden_field :chatroom_id, value: chatroom.id %>
  <%= f.submit "send", class: "btn btn-primary", style: "display: none", data: {send: "message"}%>
<%end%>

Chúng ta sẽ cần thêm action để tạo message

class MessagesController < ApplicationController

  def create
    message = Message.new(message_params)
    message.user = current_user
    if message.save
      # do some stuff
    else 
      redirect_to chatrooms_path
    end
  end

  private

    def message_params
      params.require(:message).permit(:content, :chatroom_id)
    end
end

Tạo Action Cable

Khi khởi tạo một ứng dụng Rails 5 mới, chúng ta sẽ có:

├── app
    ├── channels
        ├── application_cable
        ├── channel.rb
        └── connection.rb

Module ApplicationCable có class Channel và class Connection đã được định nghĩa

Connection class sẽ dùng để xác thực các kết nối — ví dụ, for example, thiết lập một kênh kết nối tới inbox của người dung, sẽ yêu cầu xác thực người dùng. 

module ApplicationCable
  class Connection < ActionCable::Connection::Base
  end
end

The Channel class sẽ dùng để định nghĩa logic được chia sẻ giữa các kênh mà chúng ta định nghĩa.

# app/channels/channel.rb

module ApplicationCable
  class Channel < ActionCable::Channel::Base
  end
end

Thiết lập kết nối WebSocket

Step 1: Server-Side

Đầu tiên, chúng ta cần trỏ Action Cable server lên một sub-URI 

Trong routes.rb:

Rails.application.routes.draw do

  # Serve websocket cable requests in-process
  mount ActionCable.server => '/cable'

  resources :chatrooms, param: :slug
  resources :messages

  ...

end

Như vậy, Action Cable sẽ lắng nghe các yêu cầu WebSocket tại địa chỉ ws://localhost:3000/cable.

Tiếp theo, chúng ta cần tạo kết nối WebSocket từ client, gọi là consumer.

 ​Step 2: Thiết lập kết nối Socket : Client-Side

 app/assets/javascripts/channels we'll create a file: chatrooms.js. Here is where we will define the client-side instance of our WebSocket connection.

Định nghĩa một instance kết nối WebSocket client

// app/assets/javascripts/channels/chatrooms.js

//= require cable
//= require_self
//= require_tree .

this.App = {};

App.cable = ActionCable.createConsumer();  

Note: Hãy chắc chắn bạn có require thư mục channels trong asset pipeline:

// app/assets/javascripts/application.js

//= require_tree ./channels

Để chỉ định cho consumer sẽ kết nối tới đâu, chúng ta sẽ set socket URIs trong environment files, và truyền nó thông qua action_cable_meta_tag.

 

# config/development.rb
Rails.application.configure do 
  config.action_cable.url = "ws://localhost:3000/cable"
end 

 

# app/vippews/layouts/application.html.erb

<%= action_cable_meta_tag %>

Xây dựng kênh

Chúng ta đã sử dụng Action Cable để tạo thành công một connection, đón nhận bất kì yêu cầu WebSocket nào tại địa chỉ  ws://localhost:3000/cable. Nhưng như vậy chưa đủ để tạo chức năng gửi tin nhắn real-time. Chúng ta cần định nghĩa cụ thể kênh gửi nhận tin nhắn và hướng dẫn các thành phần khác của ứng dụng truyền - nhận dữ liệu từ kênh này.

Step 1: Định nghĩa Kênh truyền

Định nghĩa một kênh truyền với Action Cable rất dễ dàng. Chúng ta sẽ tạo ra một file app/channels/messages_channel

# app/channels/messages_channel.rb

class MessagesChannel < ApplicationCable::Channel  

end  

Kênh truyền tin tức này sẽ chỉ cần một function, cho một mục đích duy nhất, là #subscribed

# app/channels/messages_channel.rb
class MessagesChannel < ApplicationCable::Channel  
  def subscribed
    stream_from 'messages'
  end
end  

Step 2: Truyền thông tin tới kênh

Ngay khi có một tin nhắn được tạo, chúng ta cần truyền nó tới kênh customer. 

#  app/controllers/messages_controller.rb

class MessagesController < ApplicationController

  def create
    message = Message.new(message_params)
    message.user = current_user
    if message.save
      ActionCable.server.broadcast 'messages',
        message: message.content,
        user: message.user.username
      head :ok
    end
  end

  ...
end

Chúng ta truyền một số tham biến sau vào method #broadcast của Action Cable server:

  • 'messages', Tên của channel
  • Thông tin sẽ được truyền tới kênh dưới dạng JSON:
    • message, nội dung của tin nhắn vừa được tạo.
    • user, thiết lập giá trị bằng username của user vừa tạo.

Step 3: Áp dụng Redis cho Action Cable

Action Cable sử dụng Redis để gửi và nhận tin nhắn thông qua kênh đã tạo. Vì vậy, khi chúng ta 'nói' với Action Cable server #broadcast tới  'messages'channel, chúng ta thực ra đang "gửi những tin nhắn mới tới kênh 'messages' được duy trì bởi Redis."

Cùng lúc đó, #subscribed trong Messages Channel đang streaming các tin nhắn được gửi thông qua kênh 'messages' được duy trì bởi Redis.

Vậy, Redis đóng vai trò như một vùng lưu trữ dữ liệu và đảm bảo rằng các tin nhắn sẽ được bảo vệ và cập nhật qua 'vùng' của dự án

Action Cable sẽ tìm kiếm cấu hình Redis trong Rails.root.join('config/cable.yml')

production:
  adapter: redis
  url: redis://localhost:6379/1

development:
  adapter: async

test:
  adapter: async

Định nghĩa Subscriber của kênh

Trước đó, chúng ta đã tạo ra consumer

// app/assets/javascripts/channels/chatrooms.js

this.App = {};

App.cable = ActionCable.createConsumer();  

consumer là đầu phía client của kết nối WebSocket.

Bây giờ chúng ta sẽ tạo ra cơ chế đón thông tin cho consumer.

 

// app/assets/javascripts/channels/messages.js

App.messages = App.cable.subscriptions.create('MessagesChannel', {  
  received: function(data) {
    $("#messages").removeClass('hidden')
    return $('#messages').append(this.renderMessage(data));
  },

  renderMessage: function(data) {
    return "<p> <b>" + data.user + ": </b>" + data.message + "</p>";
  }
});

Sơ đồ hoạt động 

 

Bài dịch từ https://blog.heroku.com/real_time_rails_implementing_websockets_in_rails_5_with_action_cable