Action Cable in Rails
Hôm nay chúng ta sẽ build 1 app chat sử dụng ActionCable websocket, rails và postgreql
Http và Websockets
Với Http việc kết nối giữa client server có vòng đời ngắn. Client request đến server, kết nối được hình thành và dữ liệu được máy chủ trả về cho client gọi là response. Sau đó kết nối được đóng lại. Nhưng làm thế nào để client biết server có thay đổi. Thông thường, http sử dụng long pulling. Client sẽ hỏi server xem có gì thay đổi không trong khoảng thời gian nhất định
Không giống như http, websocket là phương thức cho phép client và server giữ kết nối, client và server có thể trao đổi qua lại. Client subscribes đến channel trên server và khi có thay đổi, server sẽ phát tín hiệu và client nhận nó.
ActionCable hoạt động như thế nào?
Trong Rails, controller được xây dựng với mục đích xử lí các http request. Để xử lí các kết nối websocket, rails đã tạo ra thư mục mới gọi là channels. Channels hoạt động giống controller để xử lí các Websocket request. Các channel có thể được client subscribe để truyền dữ liệu qua lại.
Cài đặt
ActionCable là tín năng mới được tích hợp trên rails 5.
Databse: PostgreSQL
1 2 | rails _5.0.0.beta3_ new chatapp --database=postgresql |
Sau đó cấu hình ở trong file database.yml xong thì chạy
1 2 3 | cd chatapp rake db:create |
Các bước cần làm:
- Tạo channel websocket phía server gọi là room_channel, nó sẽ có các các method để xử lí việc subscribe, unsubscribe và gửi data đến client
- Sử dụng javascript ở client đế gọi subscribe, unsubcribe, xử lí việc gửi mà nhận dữ liệu.
1 2 | rails g controller rooms show |
1 2 3 4 5 | <span class="token comment">#config/routes.rb</span> <span class="token constant">Rails</span><span class="token punctuation">.</span>application<span class="token punctuation">.</span>routes<span class="token punctuation">.</span>draw <span class="token keyword">do</span> root to<span class="token punctuation">:</span> <span class="token string">'rooms#show'</span> |
Tiếp theo chúng ta sẽ tạo model mesage
1 2 | rails g model message content:text |
rồi tạo bảng message trong db
1 2 | rails db:migrate |
List tất cả các message
1 2 3 4 5 6 7 8 | <span class="token comment">#app/controllers/rooms_controller.rb</span> <span class="token keyword">class</span> <span class="token class-name">RoomsController</span> <span class="token operator"><</span> <span class="token constant">ApplicationController</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">show</span></span> <span class="token variable">@messages</span> <span class="token operator">=</span> <span class="token constant">Message</span><span class="token punctuation">.</span>all <span class="token keyword">end</span> <span class="token keyword">end</span> |
view rooms#show
1 2 3 4 5 6 7 8 9 10 11 | <span class="token operator"><</span>h1<span class="token operator">></span><span class="token constant">Chat</span> room<span class="token operator"><</span><span class="token operator">/</span>h1<span class="token operator">></span> <span class="token operator"><</span>div id<span class="token operator">=</span><span class="token string">"messages"</span><span class="token operator">></span> <span class="token operator"><</span><span class="token string">%= render @messages %> </div> <form> <label>Say something:</label><br> <input type=</span><span class="token string">"text"</span> data<span class="token operator">-</span>behavior<span class="token operator">=</span><span class="token string">"room_speaker"</span><span class="token operator">></span> <span class="token operator"><</span><span class="token operator">/</span>form<span class="token operator">></span> |
1 2 3 4 5 6 | <span class="token comment"># app/view/messages/_message.html.erb</span> <span class="token operator"><</span>div <span class="token keyword">class</span><span class="token operator">=</span>“message”<span class="token operator">></span> <span class="token operator"><</span>p<span class="token operator">></span><span class="token operator"><</span><span class="token operator">%</span><span class="token operator">=</span> message<span class="token punctuation">.</span>content <span class="token string">%></p></span> <span class="token operator"><</span><span class="token operator">/</span>div<span class="token operator">></span> |
1 2 3 4 5 | <span class="token comment">// app/assets/javascripts/application.js</span> <span class="token comment">//= require jquery</span> <span class="token comment">//= require jquery_ujs</span> |
Tạo channel
routes.
1 2 3 4 | <span class="token comment">#config/routes.rb</span> <span class="token comment"># Serve websocket cable requests in-process</span> mount <span class="token constant">ActionCable</span><span class="token punctuation">.</span>server <span class="token operator">=</span><span class="token operator">></span> <span class="token string">'/cable'</span> |
cable.coffee
1 2 3 4 5 6 7 | #= require action_cable #= require_self #= require_tree ./channels # @App ||= {} App.cable = ActionCable.createConsumer() |
Đặt <%= action_cable_meta_tag %> trong head của app/views/layouts/application.html.erb
Tạo channel
1 2 | rails g channel room speak |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <span class="token comment">#app/channels/room_channel.rb</span> <span class="token keyword">class</span> <span class="token class-name">RoomChannel</span> <span class="token operator"><</span> <span class="token constant">ApplicationCable</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Channel</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">subscribed</span></span> <span class="token comment"># stream_from "some_channel"</span> <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">unsubscribed</span></span> <span class="token comment"># Any cleanup needed when channel is unsubscribed</span> <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">speak</span></span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
subscribed method sẽ được gọi khi client kết nối đến channel, và nó thường được sử dụng để bật lắng nghe những thay đổi cho client. speak method nhận data từ client.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #app<span class="token operator">/</span>assets<span class="token operator">/</span>javascripts<span class="token operator">/</span>channels<span class="token operator">/</span>room<span class="token punctuation">.</span>coffee App<span class="token punctuation">.</span>room <span class="token operator">=</span> App<span class="token punctuation">.</span>cable<span class="token punctuation">.</span>subscriptions<span class="token punctuation">.</span>create <span class="token string">"RoomChannel"</span><span class="token punctuation">,</span> connected<span class="token operator">:</span> <span class="token operator">-</span><span class="token operator">></span> # Called when the subscription is ready <span class="token keyword">for</span> use on the server disconnected<span class="token operator">:</span> <span class="token operator">-</span><span class="token operator">></span> # Called when the subscription has been terminated by the server received<span class="token operator">:</span> <span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> # Called when there's incoming data on the websocket <span class="token keyword">for</span> <span class="token keyword">this</span> channel speak<span class="token operator">:</span> <span class="token operator">-</span><span class="token operator">></span> @perform <span class="token string">'speak'</span> |
Ở đây, client subscribes đến server thông qua App.room = App.cable.subscriptions.create "RoomChannel"
. connected và disconnected dùng để xử lí trạng thái kết nối, received xử lí data nhận được từ server. speak method có nhiệm vụ gửi data lên server.
Truyền dữ liệu
Thêm tham số vào speak method
1 2 3 4 5 6 7 | #app<span class="token operator">/</span>assets<span class="token operator">/</span>javascripts<span class="token operator">/</span>channels<span class="token operator">/</span>room<span class="token punctuation">.</span>coffee App<span class="token punctuation">.</span>room <span class="token operator">=</span> App<span class="token punctuation">.</span>cable<span class="token punctuation">.</span>subscriptions<span class="token punctuation">.</span>create <span class="token string">"RoomChannel"</span><span class="token punctuation">,</span> #rest <span class="token keyword">of</span> the code speak<span class="token operator">:</span> <span class="token punctuation">(</span>message<span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> @perform <span class="token string">'speak'</span><span class="token punctuation">,</span> message<span class="token operator">:</span> message |
speak sẽ gửi 1 message Json object đến speak method trong class RoomChannel.
1 2 3 4 5 6 | <span class="token comment">#app/channels/room_channel.rb</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">speak</span></span><span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token constant">ActionCable</span><span class="token punctuation">.</span>server<span class="token punctuation">.</span>broadcast <span class="token string">"room_channel"</span><span class="token punctuation">,</span> message<span class="token punctuation">:</span> data<span class="token punctuation">[</span><span class="token string">'message'</span><span class="token punctuation">]</span> <span class="token keyword">end</span> |
Bây giờ, speak method sẽ phát tin nhắn lên room_channel. Nhưng làm thế nào để chúng ta nhận được?
Để làm điều này ta chỉ định tất cả các subscriber nhận nó sử dụng stream_from trong subscribed method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <span class="token comment">#app/channels/room_channel.rb</span> <span class="token keyword">class</span> <span class="token class-name">RoomChannel</span> <span class="token operator"><</span> <span class="token constant">ApplicationCable</span><span class="token punctuation">:</span><span class="token punctuation">:</span><span class="token constant">Channel</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">subscribed</span></span> stream_from <span class="token string">"room_channel"</span> <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">unsubscribed</span></span> <span class="token comment"># Any cleanup needed when channel is unsubscribed</span> <span class="token keyword">end</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">speak</span></span><span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token constant">ActionCable</span><span class="token punctuation">.</span>server<span class="token punctuation">.</span>broadcast <span class="token string">"room_channel"</span><span class="token punctuation">,</span> message<span class="token punctuation">:</span> data<span class="token punctuation">[</span><span class="token string">'message'</span><span class="token punctuation">]</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
Về bản chất, room_channel là môi trường trong actioncable server, nơi mà dữ liệu đến và đổ về những client nhất định. Chúng ra có thể nhận dữ liệu từ subscribed method sử dụng received method trong room.coffee
1 2 3 4 5 6 7 8 9 10 11 12 13 | #app<span class="token operator">/</span>assets<span class="token operator">/</span>javascripts<span class="token operator">/</span>channels<span class="token operator">/</span>room<span class="token punctuation">.</span>coffee received<span class="token operator">:</span> <span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token function">alert</span><span class="token punctuation">(</span>data<span class="token punctuation">[</span><span class="token string">'message'</span><span class="token punctuation">]</span><span class="token punctuation">)</span> #speak <span class="token keyword">function</span> <span class="token function">$</span><span class="token punctuation">(</span><span class="token parameter">document</span><span class="token punctuation">)</span><span class="token punctuation">.</span>on <span class="token string">'keypress'</span><span class="token punctuation">,</span> <span class="token string">'[data-behavior~=room_speaker]'</span><span class="token punctuation">,</span> <span class="token punctuation">(</span>event<span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token keyword">if</span> event<span class="token punctuation">.</span>keyCode is <span class="token number">13</span> # <span class="token keyword">return</span><span class="token operator">/</span>enter <span class="token operator">=</span> send App<span class="token punctuation">.</span>room<span class="token punctuation">.</span>speak event<span class="token punctuation">.</span>target<span class="token punctuation">.</span>value event<span class="token punctuation">.</span>target<span class="token punctuation">.</span>value <span class="token operator">=</span> <span class="token string">''</span> event<span class="token punctuation">.</span><span class="token function">preventDefault</span><span class="token punctuation">(</span><span class="token punctuation">)</span> |
Một event listener được thêm phía dưới file cho textbox ở trong template. Khi chúng ta viết gì đó và nhấn enter, nó sẽ gọi speak method trong room.coffee và gửi text đã nhập lên server.
Xử lí database
Khi nhận data từ client gửi lên, thay vì phát nó lên channel thì bây giờ lưu vào database.
1 2 3 4 5 6 7 | <span class="token comment">#app/channels/room_channel.rb</span> <span class="token comment">#the rest of the methods</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">speak</span></span><span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token constant">Message</span><span class="token punctuation">.</span>create<span class="token operator">!</span> content<span class="token punctuation">:</span> data<span class="token punctuation">[</span><span class="token string">'message'</span><span class="token punctuation">]</span> <span class="token keyword">end</span> |
Để giảm thời gian chờ server xử lí request, ta sử dụng background job để phát sóng message lên channel
1 2 3 4 5 6 | <span class="token comment">#app/models/message.rb</span> <span class="token keyword">class</span> <span class="token class-name">Message</span> <span class="token operator"><</span> <span class="token constant">ApplicationRecord</span> after_create_commit <span class="token punctuation">{</span> <span class="token constant">MessageBroadcastJob</span><span class="token punctuation">.</span>perform_later <span class="token keyword">self</span> <span class="token punctuation">}</span> <span class="token keyword">end</span> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <span class="token comment">#app/jobs/message_broadcast_job.rb</span> <span class="token keyword">class</span> <span class="token class-name">MessageBroadcastJob</span> <span class="token operator"><</span> <span class="token constant">ApplicationJob</span> queue_as <span class="token symbol">:default</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">perform</span></span><span class="token punctuation">(</span>message<span class="token punctuation">)</span> <span class="token constant">ActionCable</span><span class="token punctuation">.</span>server<span class="token punctuation">.</span>broadcast <span class="token string">'room_channel'</span><span class="token punctuation">,</span> message<span class="token punctuation">:</span> render_message<span class="token punctuation">(</span>message<span class="token punctuation">)</span> <span class="token keyword">end</span> <span class="token keyword">private</span> <span class="token keyword">def</span> <span class="token method-definition"><span class="token function">render_message</span></span><span class="token punctuation">(</span>message<span class="token punctuation">)</span> <span class="token constant">ApplicationController</span><span class="token punctuation">.</span>renderer<span class="token punctuation">.</span>render<span class="token punctuation">(</span>partial<span class="token punctuation">:</span> <span class="token string">'messages/message'</span><span class="token punctuation">,</span> locals<span class="token punctuation">:</span> <span class="token punctuation">{</span> message<span class="token punctuation">:</span> message <span class="token punctuation">}</span><span class="token punctuation">)</span> <span class="token keyword">end</span> <span class="token keyword">end</span> |
1 2 3 4 5 | <span class="token comment">#app/assets/javascripts/channels/room.coffee</span> received<span class="token punctuation">:</span> <span class="token punctuation">(</span>data<span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> $<span class="token punctuation">(</span><span class="token string">'#messages'</span><span class="token punctuation">)</span><span class="token punctuation">.</span>append data<span class="token punctuation">[</span><span class="token string">'message'</span><span class="token punctuation">]</span> |
Hoàn thành
Bây giờ, bạn có thể start server và nhập vào textbox và tin nhắn của mình xuất hiện trong cuộc trò chuyện.