+1

Action Cable - Friend or Foe?

Mình có cơ hội được biết và làm việc với Action Cable từ phiên bản của Rails 5.0 beta 3. Trong những lần tìm kiếm tài liệu về Action Cable để giải quyết vấn đề mình gặp phải, mình đã gặp được bài viết khá hay về Action CableAction Cable - Friend or Foe? tạm dịch là: Action Cable - Bạn hay Thù?

Tiêu đề bài viết là thế nhưng nội dung bài viết nói nhiều hơn về HTTP Requests và lịch sử ra đời của WebSockets. Mình sẽ dịch lại bài viết theo hướng mình hiểu để chia sẻ với mọi người nhé 😄! Mình sẽ cố gắng dịch sát nghĩa nhất với bài viết của tác giả để tránh mất đi cái hay của nó và một số từ chuyên ngành mình sẽ giữ nguyên là tiếng Anh vì khi dịch sang tiếng Việt, mình không biết phải dùng từ như thế nào cho phù hợp với ý nghĩa mà nó đang có. Hy vọng vốn tiếng Anh của mình không đến nỗi quá tệ 😄!

Tại thời điểm mình dịch bài viết thì Rails 5 đã được release chính thức. Nhưng trong bài viết thì nó vẫn đang là beta. Vậy mình vẫn cứ dịch nguyên ngữ cảnh của thời điểm mà Rails 5 vẫn đang trong giai đoạn beta nhé.

Action Cable - Friend Or Foe?

Action Cable sẽ là một tính năng chính của Rails 5, và sẽ được phát hành vào khoảng mùa đông năm nay. Action Cable làm được gì cho các lập trình viên Rails? WebSockets có thực sự hữu ích như mọi người vẫn nói?

Bài viết này không phải là một tutorial hay một bài viết kiểu "làm việc với Action Cable như thế nào" mà nó là một dạng bài "tại sao". OK, giờ chúng ta sẽ đi xem vì sao chúng ta lại có bài viết này - WebSockets đang cố gắng giải quyết vấn đề gì? Chúng ta đã giải quyết vấn đề đó như thế nào trong quá khứ (chắc là thời điểm WebSockets chưa ra đời 😄).

Đừng bấm Refresh

Web được xây dựng dựa trên các HTTP request. Khi bạn request một trang (với method GET) và nhận lại một response (phản hồi) từ máy chủ với trang mà bạn đã yêu cầu. Chúng ta đã phát triển thêm một phương thức mở rộng hơn để tạo ra một dạng web stateless (phi trạng thái) dựa trên yêu cầu và chỉnh sửa các tài nguyên trên máy chủ đó là REST (REpresentational State Transfer) - một dạng mô hình server và client không lưu trạng thái của nhau. Mỗi request từ client lên server thì client luôn phải đóng gói đầy đủ các thông tin cần thiết để server có thể hiểu được.

Do HTTP request là một dạng giao thức phi trạng thái, nên với yêu cầu làm sao để biết ai là người đang request lên server mà không cần đọc nội dung của request? Thực sự thì chúng ta không có cách nào để biết rằng request này thuộc về phiên làm việc (session) nào. Và thường trong web programming thì chúng ta sẽ sử dụng một cookie chứa ID người dùng để phân biệt giữa các session khác nhau.

Khi web càng ngày càng phát triển hơn với video, audio, ... để thay thế các trang web chỉ chứa văn bản thuần thì chúng ta thực sự mong muốn có một giao thức có thể kết nối liên tục mà không bị gián đoạn giữa server và client. Chúng ta sẽ điểm qua một số yêu cầu mong muốn có một giao thức như thế này nhé:

  • Các client cần gửi thông tin đến server thật nhanh: Với những yêu cầu có lưu lượng trao đổi dữ liệu cao ví dụ như web games thì trong một giây, các client cần phải trao đổi rất nhiều thông tin với server để xử lý những yêu cầu và thao tác của người dùng (ví dụ đơn giản là một game bắn súng). Các request này đôi khi được gọi là các giao tiếp "full-duplex" (song song toàn phần) hay "bi-directional" (hai chiều).

  • "Live" data: Các trang web có các dạng dữ liệu (sống - live) động, cần được cập nhật tức thì lên trang ngay khi có sự thay đổi (ví dụ như bình luận một status của Facebook) mà không cần phải có thao tác refresh lại trang web từ người dùng. Đôi khi, nó được gọi là ứng dụng "realtime" (thời gian thực), và ý nghĩa của nó đôi khi không được chính xác. Vì "realtime" có nghĩa là liên tục, cập nhật theo từng giây. Nhưng trong thực tế, với ví dụ là phần bình luận status của Facebook mà mình đã đưa trước đó, thì nó sẽ không thay đổi theo từng giây. Nó thường khoảng vài giây cho đến phút hoặc hơn. Nhưng chúng ta vẫn có thể gọi nó là "live" cũng không sao cả 😄 (mình chả hiểu ý nghĩa của thanh niên này viết muốn truyền đạt cái gì mà phần sau đá phần trước - hoặc mình dịch không được chính xác (yaoming))!

  • Streaming: HTTP đã chứng tỏ rằng nó không phù hợp cho việc streaming data. Nhiều năm về trước, việc streaming một video cần phải có plugin của bên thứ ba (ví dụ như RealPlayer). Ngay cả bây giờ thì việc streaming một video cũng là một công việc phức tạp nếu không có WebSockets, và nó cũng gần như là không thể stream các dữ liệu dạng nhị phân cho JavaScript mà không có sự hỗ trợ của Flash hay các Java applet (eek - câu cảm thán này mình giữ nguyên vì mình thấy thanh niên này biểu cảm phết. Kêu éc phát một =)))!

Con đường tới WebSockets

Hey! Hey server! You got any new data? Server. SERVER

p=. _Hey! Hey server! You got any new data? Server. SERVER_

Trong nhiều năm qua, chúng ta đã phát triển rất nhiều các giải pháp khác nhau cho những vấn đề này. Một vài giải pháp đã không còn được sử dụng như Flash XMLSocket relays hay multipart/x-mixed-replace. Tuy nhiên, một vài kỹ thuật vẫn còn được sử dụng để giải quyết vấn đề này. Chúng ta cùng điểm xem nhé.

Polling

Polling giống với việc client sẽ liên tục gọi lên server trong một khoảng thời gian cố định (ví dụ 3s/lần) để xem có bất kỳ dữ liệu nào mới hay không. Chúng ta cùng quay lại với ví dụ về phần bình luận, chúng ta sẽ tạo ra một ứng dụng với kỹ thuật polling. Viết JavaScript gọi lên server mỗi 3s/lần để kiểm tra các bình luận mới nhất. Nếu có dữ liệu mới thì thực hiện việc cập nhật nội dung vào phần bình luận.

Ưu điểm của polling là sự chắc chắn (rock-solid - chả biết mình dịch đúng không nữa) và dễ dàng triển khai. Vì lý do đó mà nó vẫn đang được sử dụng khá nhiều. Nó cũng phù hợp với vấn đề tốc độ mạng và việc mất kết nối mạng cũng không vấn đề gì nếu bạn bị miss một vài poll. Bạn chỉ việc giữ kết nối (polling) cho đến khi nào nó (chắc là có mạng) làm việc trở lại. Ngoài ra, việc phi trạng thái của HTTP hay sự thay đổi liên tục của địa chỉ IP (ví dụ như việc điện thoại sử dụng dịch vụ chuyển vùng dữ liệu - data roaming) cũng không làm ảnh hưởng đến ứng dụng.

Tuy nhiên, bạn cần phải xác định đến việc mở rộng server của mình để phục vụ client. Vì mỗi client cứ 3s lại request lên server để kiểm tra xem có dữ liệu mới hay không. Có nhiều cách để giảm tải cho việc này, một trong số đó là HTTP caching.

Polling có thể chấp nhận được cho một ứng dụng được gọi là "live" (vì mọi người thường không quan tâm đến việc bị delay 3s cho ứng dụng chat hay comment), nhưng nó không phù hợp với yêu cầu trao đổi thông tin nhanh chóng (giống như game) hay streaming data.

Long-polling

Long-polling về mặt cơ bản thì nó khá giống với polling, nhưng không cần phải thiết lập thời gian cố định giữa các request (poll) lên server. Client sẽ request lên server để kiểm tra dữ liệu, nếu có giữ liệu mới, server sẽ trả lại cho client. Còn không, nó sẽ giữ cho kết nối đó luôn mở, tạo ra một kết nối liên tục, và khi nó nhận được giữ liệu mới, request đó sẽ được hoàn thành.

Điều này chính xác thì nó được thực hiện như thế nào? Đó là một số kỹ thuật phụ của long-polling mà bạn có thể đã được nghe trước đó, giống như BOSHComet. Nó đủ để nói rằng, kỹ thuật long-polling phức tạp hơn so với polling.

Long-polling rất tốt cho trường hợp dữ liệu không thay đổi thường xuyên. Quay lại với ví dụ ứng dụng comment, nếu 45s trước có một comment được thêm mới, thay vì 15 poll (vì polling dùng 3s/lần) cho một client thì server chỉ cần mở một kết nối liên tục cho client đó là đủ.

Tuy nhiên, nó sẽ không phù hợp với ứng dụng mà dữ liệu được thay đổi liên tục, ví dụ như thông tin của một sàn chứng khoán chẳng hạn. Giá của một cổ phiếu có thể thay đổi vào khoảng 1ms (hoặc nhanh hơn) trong ngày giao dịch. Điều đó có nghĩa rằng bất cứ lúc nào, client yêu cầu có dữ liệu mới thì server phải trả lời ngay lập tức cho client biết. Điều này sẽ vượt quá tầm kiểm soát một cách nhanh chóng, bởi vì ngay sau khi client nhận được một phản hồi từ server, nó sẽ tiếp tục tạo một kết nối khác để tiếp tục kiểm tra. Việc này có thể cho kết quả là sẽ có khoảng 5-10 request trong mỗi giây cho mỗi client. Bạn có thể hạn chế thời gian gửi request của client. Nhưng như vậy thì ứng dụng của bạn sẽ không phải Realtime nữa rồi 😄.

Server-sent Events (SSEs)

Server-sent Events cơ bản là kết nối một chiều từ server tới client. Client không thể sử dụng SSEs để gửi dữ liệu trở lại cho server. Server-sent Events được thêm vào API của trình duyệt từ năm 2006 và hiện tại, nó vẫn được hỗ trợ bởi hầu hết các trình duyệt (browser) hiện nay, ngoại trừ bạn Internet Explorer (facepalm).

Việc sử dụng Server-sent Events thực sự rất đơn giản từ phía client (JavaScript). Bạn khởi tạo một đối tượng EventSource, định nghĩa một onmessagecallback để thực hiện xử lý dữ liệu khi có dữ liệu mới từ server. Server-sent Events được thêm vào Rails từ phiên bản 4.0 với module ActionController::Live.

Việc phục vụ một client với SSEs yêu cầu một kết nối liên tục, điều này có nghĩa rằng: sử dụng Server-sent Events không làm việc tốt trong mọi thời điểm trên Heroku, vì họ sẽ ngắt mọi kết nối sau 30 giây. Unicorn cũng làm điều tương tự thế, và WEBrick cũng không làm việc hiểu quả ở mọi thời điểm. Lựa chọn còn lại là bạn sử dụng Passenger, Puma hoặc Thin, nhưng bạn lại không thể sử dụng nó trên Heroku (yaoming)! Ngoài ra, bạn cũng không thể sử dụng ứng dụng khi dùng trình duyệt IE.

Internet Explorer

How WebSockets work

WebSocket sẽ giải cứu thế giới? Cũng có thể lắm chứ 😄. Trước tiên, chúng ta sẽ đi tìm hiểu xem nó sẽ làm được những gì nhé (go)!

Kết nối liên tục, kết nối có trạng thái (stateful)

Không giống như HTTP, WebSocket là một kiểu kết nối có trạng thái (stateful). Điều đó có nghĩa là gì? Chúng ta thử so sánh ẩn dụ vui nhé - các HTTP request giống như một hộp thư. Tất cả các request sẽ đi tới cùng một điểm giống nhau, và bạn phải nhìn vào các request (ví dụ như địa chỉ người gửi) để biết rằng ai đã gửi cho bạn. Ngược lại, WebSocket giống việc dựng một đường ống (pipe line) giữa server và client. Thay vì mọi request đều đến từ một chỗ, thì nó sẽ được tách ra nhiều đường ống riêng biệt. Khi một request mới đi qua một đường ống, bạn sẽ biết là ai đang gửi nó mà không cần phải nhìn vào nội dung của request.

No data frames

Tổng quan, các thông điệp (message) đều chứa datametadata (siêu dữ liệu). data là những gì chúng ta muốn trao đổi. Còn metadata là dữ liệu để mô tả cho data. Bạn có thể nói rằng, một giao thức truyền thông sẽ hiệu quả hơn nếu nó đòi hỏi metadata ít hơn so với giao thức khác.

HTTP cần khá nhiều metadata để có thể làm việc. Trong HTTP, metadata được chứa trong HTTP headers. Bạn có thể xem một headers của một HTTP response của Rails server:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Vary: Accept-Encoding
X-Runtime: 0.121484
X-Powered-By: Phusion Passenger 5.0.14
X-Xss-Protection: 1; mode=block
Set-Cookie: _session_id=f9087b681653d9daf948137f7ece14bf; path=/; secure; HttpOnly
Server: nginx/1.8.0 + Phusion Passenger 5.0.14
Via: 1.1 vegur
Cache-Control: max-age=0, private, must-revalidate
Date: Wed, 23 Sep 2015 19:43:03 GMT
X-Request-Id: effc7fe2-0ab8-4462-8b64-cb055f5d1b13
Strict-Transport-Security: max-age=31536000
Content-Length: 39095
Connection: close
X-Content-Type-Options: nosniff
Etag: W/"469b11fcecff716247571b85ff1fc7ae"
Status: 200 OK
X-Frame-Options: SAMEORIGIN

Yikes (mình lại giữ nguyên biểu cảm này của tác giả =))), header này chứa ~670 byte không bao gồm những thông tin cookie mà chúng ta đã gửi lên (nó chiếm khoảng ~2,000 byte). Bạn có thể thấy việc không hiệu quả nếu như dữ liệu của chúng ta nhỏ hoặc chúng ta cần thực hiện nhiều request.

WebSockets giải quyết được điều đó. Để mở một kết nối WebSocket, client tạo một HTTP request thông thường kèm theo một header đặc biệt là upgrade lên server. Server sẽ tạo ra một HTTP response trả lời với nội dung kiểu "OK, tôi đã hiểu, tôi sẽ mở một kết nối WebSockets cho bạn. Hãy giữ nó". Sau đó, client sẽ mở một đường ống để giữ kết nối đó.

Một khi kết nối WebSockets được mở, dữ liệu được gửi thông qua ống dẫn đó mà hầu như không cần đến metadata. Hoặc nếu có thì thường ít hơn là 6 byte (ngon)!

Vậy tất cả những điều này có ý nghĩa gì với chúng ta? Không phải là toàn bộ. Chúng ta thử làm một bài toán đơn giản nhé. Khi sử dụng WebSockets, bạn sẽ loại bỏ được khoảng 2KB dữ liệu cho mỗi message. Và điều đó sẽ giúp bạn tiết kiệm được hàng petabytes băng thông (việc tiết kiệm này cũng sẽ khác nhau tùy vào tính chất của ứng dụng mà bạn triển khai).

Liên lạc hai chiều

Bạn đã nghe rất nhiều người nói rằng WebSockets là "full-duplex". Vậy nó có nghĩa là gì? Rõ ràng, full-duplex nó phải tốt hơn half-duplex chứ. Vì full-duplex nó gấp đôi half-duplex cơ mà =)).

Tất cả những gì nói về full-duplex đều có nghĩa rằng nó là một giao tiếp đồng thời. Với HTTP, client thường phải hoàn thành request trước khi server có thể trả lời request đó. Không giống như WebSockets - client và server có thể gửi message thông qua ống dẫn bất cứ lúc nào.

Caniuseit? (Can I Use It)

Những trình duyệt nào bạn có thể sử dụng được WebSockets? Khá nhiều. Đây là một lợi thế mà WebSockets vượt qua SSEs - một đối thủ cạnh tranh của nó. Bạn có thể xem thêm thông tin các browser và phiên bản nào của browser hỗ trợ WebSockets tại đây!

Action Cable

Action Cable được giới thiệu tại RailsConf 2015! David đã nói về Basecamp (một ứng dụng chat) đã sử dụng polling 3s/lần request trong hơn 10 năm qua. Nhưng sau đó, anh ấy đã nói:

Nếu bạn có thể thực hiện WebSockets thậm chí còn ít hơn so với làm việc với polling. Vậy tại sao bạn lại không làm điều đó?

Nếu WebSockets dễ như polling, thì tất cả chúng ta sẽ muốn sử dụng nó. Cập nhật liên tục sẽ tốt hơn là mỗi 3s/lần cập nhật. Nếu chúng ta có thể cập nhật liên tục mà không phải trả thêm bất kỳ chi phí nào (như ví dụ về băng thông), thì chúng ta nên làm điều đó.

Vậy, thước đo tiêu chuẩn của chúng ta là gì? Action Cable dễ (hoặc dễ hơn) sử dụng so với polling? Chúng ta cùng đi xem qua Action Cable trong Rails nhé.

API Overview

Action Cable cung cấp cho chúng ta những điều sau:

  • "Cable" hay "Connection" là một kết nối WebSocket từ client tới server. Action Cable sẽ giả định rằng bạn chỉ có một kết nối WebSocket và bạn sẽ gửi tất cả dữ liệu từ các ứng dụng cùng nhau.
  • "Channels" - là thành phần con của "Cable". Một "Cable" sẽ có nhiều "Channels"
  • "Broadcaster" - Action Cable là một máy chủ riêng biệt. Về cơ bản, server Action Cable sử dụng chức năng pubsub của Redis để theo dõi những gì mà nó đã phát đi như đã phát cho những ống nào và cho những ai.

Rails cung cấp cho chúng ta một class để sử dụng Action Cable, đó là ActionCable::Channel::Base. Nó cũng giống như ActiveRecord model hay ActionController. Chúng ta cùng xem một ví dụ đầy đủ của một ứng dụng đơn giản sử dụng Action Cable nhé:

  # app/channels/application_cable/connection.rb
  module ApplicationCable
    class Connection < Action Cable::Connection::Base
      # uniquely identify this connection
      identified_by :current_user

      # called when the client first connects
      def connect
        self.current_user = find_verified_user
      end

      protected
        def find_verified_user
          # session isn't accessible here
          if current_user = User.find(cookies.signed[:user_id])
            current_user
          else
            # writes a log and raises an exception
            reject_unauthorized_connection
          end
        end
    end
  end

  class WebNotificationsChannel < ApplicationCable::Channel
    def subscribed
      # called every time a
      # client-side subscription is initiated
      stream_from "web_notifications_#{current_user.id}"
    end

    def like(data)
      comment = Comment.find(data['comment_id')
      comment.like(by: current_user)
      comment.save
    end
  end

  # CLIENT-SIDE code
  # Somewhere else in your app
  Action Cable.server.broadcast \
    "web_notifications_1", { title: 'New things!', body: 'All shit fit for print' }

  # Client-side coffescript which assumes you've already requested the right to send web notifications
  @App = {}
  App.cable = Cable.createConsumer "ws://cable.example.com"
  App.cable.subscriptions.create "WebNotificationsChannel",
    received: (data) ->
      # Called every time we receive data
      new Notification data['title'], body: data['body']
    connected: ->
      # Called every time we connect
    like: (data) ->
      @perform 'like', data

Có một vài điều cần chú ý:

  • Tên channel WebNotificationsChannel là tùy chọn, dựa vào tên của class.
  • Chúng ta có thể gọi các public method của Channel từ phía code client side. Như đoạn @perform 'like', data thì có nghĩa rằng chúng ta đang gọi method like của Channel với tham số truyền vào là data (là dữ liệu mà bạn muốn server xử lý).
  • stream_from sẽ thiết lập một kết nối giữa client và tên của queue trong Redis pubsub.
  • Action Cable.server.broadcast sẽ thực hiện việc thêm các message vào Redis pubsub queue.

Hiệu suất và khả năng mở rộng ứng dụng

Trọng tâm chính của bài viết tập trung vào hiệu suất và tốc độ của ứng dụng Ruby. Nói cách khác, WebSockets sẽ cho chúng ta nhiều cách để mở rộng ứng dụng hoặc hiệu suất tốt hơn so với polling. Nó trực quan hơn polling nếu bạn lấy ví dụ như một site lớn là Facebook. Facebook không thể sử dụng polling là 3s/lần tạo request.

Nhưng việc chuyển giao từ polling sang WebSockets cũng là một sự thách thức và đánh đổi lớn. Bạn đang bán đi một lượng HTTP request cho lượng lớn các kết nối liên tục.

Persistent connections

Để đánh giá hiệu năng, chúng ta sẽ sử dụng thor, một công cụ đánh giá WebSockets. Chúng ta sẽ mở 1500 kết nối tới Action Cable chạy trên server Puma (mặc định, Puma sẽ sử dụng 16 luồng) với sự thay đổi số lượng kết nối liên tục:

|_. Simultaneous WebSocket connections |_. Mean connection time |
| 3 | 17ms |
| 30 | 196ms |
| 300 | 1638ms |

Như bạn thấy, Action Cable sẽ chậm dần với số lượng kết nối đồng thời tăng dần. Chúng ta thử cho phép Puma chạy trong chế độ nhóm. Với 4 worker, kết quả có chút cải thiện:

|_. Simultaneous WebSocket connections |_. Mean connection time |
| 3 | 9ms |
| 30 | 89ms |
| 300 | 855ms |

Điều thú vị là những con số này có chút cải thiện hơn so với ứng dụng Node.JS (source code). Dưới đây là kết quả của ứng dụng dùng Node.JS

|_. Simultaneous WebSocket connections |_. Mean connection time |
| 3 | 5ms |
| 30 | 65ms |
| 300 | 3600ms |

Nhìn vào các con số. Chúng ta có thể nghi ngờ rằng ứng dụng sẽ bị lỗi trong môi trường mà số lượng gửi/nhận dữ liệu lớn. Nhưng đối với ứng dụng thông thường thì có lẽ nó cũng khá là ổn.

Chúng ta thực sự cần gì?

Tổng quan, các developer muốn sử dụng WebSockets, nhưng những ứng dụng của họ muốn gì? Đôi khi, các cuộc tranh cãi xung quanh WebSockets giống như việc đặt cái xe trước con ngựa (bạn có thể Google Images với từ khóa "cart before the horse" để xem thêm về ý nghĩa nhé (yaoming)) - theo đuổi mục tiêu lớn nhất, công nghệ thú vị nhất khi mà polling đã đủ tốt?

Nếu bạn có thể làm việc với WebSockets đơn giản hơn polling. Vậy tại sao bạn lại không muốn sử dụng WebSockets?

Action Cable dễ sử dụng hơn polling? Không ai chắc chắn. Đó là một câu hỏi có tính chủ quan. Và câu trả lời thì có lẽ bạn sẽ tự trả lời cho chính mình.

Quay trở lại với ba yêu cần cần phải sử dụng một giao thức kết nối liên tục giữa client và server và xem cách mà Action Cable sẽ thực hiện nó nhé:

  • Các client cần gửi thông tin đến server thật nhanh: Action Cable có vẻ thích hợp cho tất cả các trường hợp cần sử dụng. Mặc dù không ai chắc chắn rằng có bao nhiêu ứng dụng web game được viết bằng ngôn ngữ Rails cả. Nhưng việc xử lý nhiều yêu cầu liên tục trong một giây giữa client và server thì hoàn toàn có thể thích hợp với Action Cable.
  • "Live" data: Với ví dụ về bình luận thì Action Cable dư sức thực hiện được.
  • Streaming: Thành thật mà nói, chắc không ai sử dụng Ruby server để streaming data tới client (có thể tác giả sai (yaoming))!

HTTP đi kèm với rất nhiều điều như: caching, routing (điều hướng, định tuyến), multiplexing (ghép), gzipping, ... khi chúng ta bỏ HTTP. Bạn có đủ sức triển khai lại tất cả những thứ đã được liệt kê với Action Cable, nhưng tại sao?

Lời kết

Đến đây là bài dịch của mình đã kết thúc. Hy vọng mọi người có thể hiểu được phần nào những gì mà bài viết gốc muốn truyền tải là (dù mình đã lược bớt một số phần mà mình cho là không cần thiết lắm) muốn chúng ta hiểu vì sao WebSockets được ra đời. Nó ra đời để giải quyết những vấn đề gì. Và những vấn đề nó giải quyết được thì đã được giải quyết như thế nào trong quá khứ (trước khi nó được ra đời) 😄!

Hẹn gặp lại mọi người trong các bài viết tiếp theo (và chắc chắn mình sẽ không dịch bài nữa đâu, sợ rồi (yaoming))!

Bài viết gốc: Action Cable - Friend or Foe?!


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí