Action Cable 即时通讯
WebSocket 简介
由于 HTTP 的限制,早期浏览器要作即时通讯,有各种奇技淫巧 Polling, Comet, Long Polling 等等,直到浏览器支援的标准出现 WebSocket 才开始进入正轨。
WebSocket 通讯协定可以让浏览器和服务器,进行持续性的双向连线沟通。目前支援程度还不错 Can I use?
- 一些故事 Realtime Web Apps 2015
- websocketd Quick Demo
(图示来源 https://blog.heroku.com/real_time_rails_implementing_websockets_in_rails_5_with_action_cable)
但由于需要与 Server 端进行持续性的连线,对于原本采用 Process-Based 设计的 Rails 比较不适合,因此会将这部分的元件拆出去,解法有:
- 将即时通讯的部分拆出去成为独立的 Ruby 元件,独立于 Rails 处理一般 HTTP requests 流量的 Process 之外,详见下一节。
- 整套改成用其他程式语言,例如 Node.js 有
- http://sailsjs.org/ (Rails-like MVC, integrated with socket.io)
- https://www.meteor.com/ (Full-stack, including websocket)
另一个与 WebSocket 类似功能的网络标准是 SSE,但是功能比较少只能单向,而且跨浏览器支援度又不好,虽然 Rails 有内建(因为对 server 比较好实作)但很少人使用。
认识 Pub/Sub 订阅模型
Publish–subscribe pattern 发布/订阅设计模式是一种在即时通讯上很常用的架构,可以将通讯拆成发布方和订阅方,发布方异步地将讯息传送给不定数量的订阅方。Pub/Sub 部分可以从你的主应用 Process 外,独立出来成为一个单独的运作元件。
Pub/Sub middleware
附带有 Pub/Sub 功能的数据库:
- Redis
- PostgreSQL
或是专门的 Message Queue 软件中的一个功能:
- RabbitMQ
- 第三方服务 AWS SQS
- 第三方服务 Google Pub/Sub
Pub/Sub 模型 + WebSocket 的 Ruby 框架
- https://faye.jcoglan.com/
- Rails 5 内建的 ActionCable
第三方服务 Pub/Sub + WebSocket
Rails 5: ActionCable 简介和安装
因为越来越多的即时通讯需求,Rails 在 5.0 之后新增的 ActionCable 元件来支援 WebSocket,整合了后端 Rails 和前端 JavaScript 呼叫接口,可以很方便的开发即时通讯的应用。
(图示来源 https://blog.heroku.com/real_time_rails_implementing_websockets_in_rails_5_with_action_cable)
安装方式
- 升级 Rails5 并使用
gem "puma"
,不能用 webrick 了 - 安装 Redis
- brew install redis
- brew services start redis
- 修改 config/cable.yml 把 async 都改成用 redis
- 修改 config/application.rb 加上
config.action_cable.mount_path = '/cable'
- 修改 app/assets/javascripts/application.js 要加载 cable.js
ActionCable 应用
即时聊天室是最常见的应用,让我们用这个例子来举例。
单一公开聊天室
WebSocket 虽然支援双向通讯,但是在实作上最重要需要即时的部分,其实只有接收资料。送资料给 Server 其实不一定需要走 WebSocket 连线,也可以用 Ajax。以下两篇教学文章,刚好分别用了不同的方式来实作:
方法一:送出的部分沿用 ajax request,如同 Real-Time Rails: Implementing WebSockets in Rails 5 with Action Cable 这篇教学
方法二:送出的部分也走 websocket 路线,如同 Create a Chat App with Rails 5, ActionCable, and Devise 这篇教学
范例程式: https://github.com/ihower/rails-exercise-ac9/tree/rails5
在 ActionCable 中可以用 partial template 在 server-side 回传给浏览器,这样就可以重用服务器的 erb template。
注意到用 WebSocket 连线,如果浏览器有跳页的话,是会重新连线的。这就是为什么 SPA(Single-Page Application) 的形式比较适合做即时通讯,在那个应用页面中,所有的操作都是用 Ajax,这就才不会跳页。而在 Rails 中则是搭配了 Turbolinks 的 Ajax 换页效果,才不会每页都重连 Websocket (什么? 你听我在 Ajax 一章的建议已经拆除 Turbolinks 了??)
多个聊天室、整合 Devise 认证、Broadcast 的部分改成用 ActiveJob
参考 Create a Chat App with Rails 5, ActionCable, and Devise 这篇教学
当 Broadcast 的呼叫是发生在一般 request/response lifecycle 时,建议改放在 ActiveJob 里,因为需要通知的用户可能很多。
其他常见的即时通讯应用
- 使用者在线状态
- 1 对 1 通讯
- 即时通知
- 各种状态更新,例如异步操作的完成通知
- 即时资料更新,例如 facebook 留言
- 同步协作,例如 Trollo
- 即时游戏
其他 ActionCable 没有帮你作的事情
参考 https://blog.ably.io/rails-5-actioncable-the-good-and-bad-parts-1b56c3b31404
Abrupt failures can corrupt state, no ACK/NACK support, Message Ordering 等等都是没有保证的
例如如果你重新整理一下网页,WebSocket 会重新连线,那这中间断掉时间发布的新讯息,你就不会收到。
ActionCable 布署
- 修改 config/environments/production.rb 加上 production 网址
config.action_cable.allowed_request_origins = ["https://foobar.com"]
- Rails 5.0.1 之后变默认了 ActionCable now permits same-origin connections by default
- 记得装 Redis
sudo apt-get install redis-server
- websocket server 可以跟 rails process 放一起,也可以分开。前者比较简单,但后者可以分开 scale-up。
- 要拆开的话,砍掉
config.action_cable.mount_path
这个设定,改成设定config.action_cable.url
。但是如果拆开的 domain 和 port 不同的话,本来可以顺便用 cookie 做认证可能会有问题,因为 cookie 不会送来,你可以改config/initializers/session_store.rb
多一个参数是domain: :all
或domain: ["your_website.example.com", "your_ws.example.com"]
将 session cookie 扩大到跨 subdomain 都可以吃的到。
- 要拆开的话,砍掉
- Passenger 和 Nginx 设定
- CloudFlare 也有支援,但没有讲清楚到底可以接受多少连线数量
- iOS 当然也可以接 websocket,可以 google 看看 “ios websocket”
- Websocket Shootout: Clojure, C++, Elixir, Go, NodeJS, and Ruby 在一个 Process 下的效能 benchmark,很遗憾 Rails ActionCable 的效能敬陪末座。若依照 每mb的连线数 重新排序:
- C++ 55
- Node.js 43
- Go 30
- Clojure 18
- Elixir: 13
- Ruby: 3
- JRuby: 2
- AnyCable 可以关注看看,用其他语言的实作来取代 Rails 的 Websocket Server