使用Nginx、Redis和MySQL,创建一个简单而实用的Rails ActionCable实战案例,并制作一个涵盖整体内容的iOS/Android示例应用程序〜后端篇〜
开场白
我想要使用Rails ActionCable实现双向通信。我创建了一个示例应用程序,想要在移动应用程序中制作实时通信应用程序。有关每个细节的解释已经有相关的文章了,所以这里只介绍环境设置和源代码的概述。下面是三个部分的结构。
-
- バックエンド(本記事)
-
- iOS
- Android
介绍玩耍样本

在此应用程序中,有两种主要的ActionCable用法。
-
- 同じルーム内の全ユーザーにブロードキャスト
ルームに入る
現在ルーム内にいるユーザー(以下、アクティブユーザー)にルームに入ったことを通知し、アクティブユーザーを取得します。そして、各ユーザーのアクティブ状況を表示します。
「ワンワン」ボタンと「ワオーン」ボタン
文字入力で任意の文字が送れないだけで、チャットでいうところの「メッセージ」とほぼ同義です。ボタンに対応したメッセージを送信します。
ルームから出る
iOSなら「キャンセル」、Androidなら「←」をタップしたり、アプリを閉じたりするとルームから出たことにします。ルームに入るときと同様にアクティブユーザーを取得して、各ユーザーのアクティブ状況を表示します。
自分にブロードキャスト
「独り言」
「独り言」ということで自分のみメッセージを受信します。
构成
我们正在使用Docker基于以前创建的开发环境进行构建。这个配置也已在GitHub上公开,请查看Dockerfile和Docker Compose。在这里,我们将为每个服务挑选主要的设置。
MySQL 是一种开源的关系型数据库管理系统,它使用SQL语言进行管理和操作数据库。
用于在连接到ActionCable时获取用户信息的处理。没有什么特别需要注意的,但要强调的是,默认字符编码已设为utf8mb4。
Nginx
-
- ポート (nginx.conf / docker-compose.yml)
-
- 自己署名証明書(所謂オレオレ証明書)ではSSLハンドシェイクの関係でうまく通信出来ませんでしたので、この環境では平文のWebSocket通信(ws://)を行うため80番ポートを許可します。なお、筆者環境では独自ドメインとLet’ EncryptのSSL証明書でも動作確認しています。その場合は443番ポードで想定通り暗号化されたWebSocketの通信(wss://)ができることを確認しています。
-
- WebSocket (nginx.conf)
- ActionCableのエンドポイントである「location /cable」はhttp(https)ではなくWebSocket(ws/wss)として通信できるようにします。
铁路
-
- WebAPIモード
-
- WebのViewやフロントエンドは不要なのでWebAPIとしてプロジェクトを構築します。
-
- MySQLのデフォルト文字コード: utf8mb4
- このサンプルアプリでは動作上の意味はありません。折角なので導入しただけです。
由于本文的重点是Rails,因此稍后会另行解释。
Redis是一种开源的内存数据结构存储系统。
-
- サブスクリプションアダプター
-
- Railsのサブスクリプションアダプターには「Async」ではなく「Redis」を使います。
-
- キャッシュ
- 簡易的なデータストアに使います。
用Rails构建WebAPI。
如前所述,本环境已在GitHub上公开,因此本文仅介绍要点。
创建一个项目 yī gè
当初启动Docker Compose时,将执行以下操作。
bundle exec rails new . -d mysql -f -T --api --skip-bundle
设置ActionCable
-
- 允許origin。
-
- 如同在Nginx配置中提到的,由于使用自签名证书,无法正确进行SSL握手,使得通信无法正常进行。在这种环境下,允许明文的WebSocket通信(ws://)。如果导入了正规的SSL证书,则只需指定wss://即可解决问题。
-
- 为了在Android模拟器中运行,将disable_request_forgery_protection设置为true,放宽发送端的限制。如果不进行这个设置,会出现以下错误。
-
- 作者是先开发了iOS版,然后再开发Android版。由于在iOS模拟器中没有出现这个问题,所以忽略了Rails端的日志调查,导致调查花费了一些时间。
log/development.log
请求来源不允许:
无法升级到WebSocket(请求方法:GET,HTTP连接:升级,HTTP升级:Websocket)。
```ruby:volumes/app/config/environments/development.rb
config.action_cable.allowed_request_origins = [ /wss?:\/\/.*/, /ws?:\/\/.*/ ]
config.action_cable.disable_request_forgery_protection = true
-
- 指定订阅适配器为Redis
-
- 开发环境的适配器默认为async。生产环境推荐使用Redis(不推荐使用async)。在这里我们决定使用Redis。
volumes/app/config/cable.yml
default: &default
适配器: redis
URL: <%= ENV.fetch(“REDIS_URL”) { “redis://cache:6379/0” } %>
频道前缀: app_production
development:
<<: *default
## Redisを設定する
ActionCableとは別にキャッシュの用途でもRedisを使うことにします。
1. Redis Gemをインストール
Gemfileのコメントアウトを外します。
```ruby:volumes/app/Gemfile
gem 'redis', '~> 4.0'
-
- 在Redis中进行设置
-
- 由于在ActionCable中指定了Redis的DB编号为0,因此在缓存目的中,我们将DB编号设为了1。
卷/应用程序/配置/初始设定器/Redis.rb
REDIS = Redis.new(host: ENV.fetch(“REDIS_HOST”) { “cache” }, port: ENV.fetch(“REDIS_PORT”) { “6379” }, db: ENV.fetch(“REDIS_DB”) { “0” })
REDIS = Redis.new(host: ENV.fetch(“REDIS_HOST”) { “cache” }, port: ENV.fetch(“REDIS_PORT”) { “6379” }, db: ENV.fetch(“REDIS_DB”) { “0” })
:warning::warning:2019/3/24 更新:warning::warning:
当初はRailsのキャッシュストア(Rails.cache)を経由してRedisを使用していました。直接Redisを利用したほうが可読性が高いと判断し、本記事とGitHubに公開しているソースコードを更新しました。
## モデルとマイグレーション
ほんの少し実践的にユーザーの情報がRDBに格納されていることを想定してモデルを作成します。
```sh:コンソール
$ bundle exec rails g model user account:string name:string
$ bundle exec rails db:migrate
另外,对迁移进行了一些限制,并得到了以下结果。然而,即使没有添加限制,操作也不会受到影响。
ActiveRecord::Schema.define(version: 2019_02_23_052441) do
create_table "users", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC", force: :cascade do |t|
t.string "account", null: false
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account"], name: "index_users_on_account", unique: true
end
end
将示例数据(初始数据)输入
您非ActionCable部分的说明有所增加,为了避免这种情况,我们简化了应用程序代码。因此,除了以下三个用户,其他人无法运行该应用。对不起。
User.create(account: 'chiyo', name: '千代')
User.create(account: 'eru', name: 'エル')
User.create(account: 'otome', name: '乙女')
$ bundle exec rails db:seed
设定连接
公式几乎没有改变。为了使应用程序和WebAPI无状态,用户信息将从参数中获取,而不是从Cookie中获取。实际上,我们会验证像OpenID Connect之类的访问令牌的有效性来判断连接是否可行。
request.params[:account]
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
def disconnect
end
private
def find_verified_user
if verified_user = User.find_by(account: request.params[:account])
verified_user
else
reject_unauthorized_connection
end
end
end
end
创建频道
$ bundle exec rails g channel room
class RoomChannel < ApplicationCable::Channel
:
:
end
我将介绍默认创建的方法和为示例应用程序创建的方法(仅限公共方法)。
订阅了
我們將獲取APP端請求的房間參數。在示例應用程序中,我們將無條件訂閱該房間,但也可以進行用戶和房間的驗證,以防止非法訪問並進行相應的處理。接下來,我們將指定stream_for。雖然不確定這個表達是否正確,但它可以用於指定發送到的分組。
-
- room全体
-
- @room(params[:room])を指定します。同じroomを指定(以下、同じルーム)しているアクティブユーザーにブロードキャストすることができます。
-
- 自身のみ
- ユーザーのアカウントとroomを連結して自身のみを指定します。一意になれば何でも良いと思います。ブロードキャストではあるものの実質的に自身のみが宛先になります。
room_in是一个简单的方法,用于添加每个房间的活跃用户。由于这个方法只需要简要说明其功能即可,所以实现得比较简陋。
def subscribed
@room = params[:room]
@user = self.current_user.id.to_s + @room
stream_for @room
stream_for @user
room_in(key: @room, account: self.current_user.account)
end
取消订阅
无论是暗示或明示地取消订阅,当取消订阅时,与room_in相反,将删除每个房间的活跃用户。这个方法的实现可以只是简单地描述其功能,所以实现方式较为简单。
同时,将离开房间的消息广播给活跃用户。
def unsubscribed
room_out(key: @room, account: self.current_user.account)
# 全員に送ります。
RoomChannel.broadcast_to(@room, account: self.current_user.account, type: :out)
end
问候
每个用户在订阅后,将会通知活跃用户他们已经”进入了房间”。同时,利用roommate(※)发送活跃用户列表。通过该列表,我们在应用程序中更新每个用户的活跃状态。
※ roommate是一个方法,用于根据房间获取活跃用户。我们通过room_in/room_out来管理它。
def greeting
# 全員に送ります。
RoomChannel.broadcast_to(@room, roommate: roommate(key: @room), account: self.current_user.account, type: :in)
end
咕哝不清
通过「独白」按钮只向自己广播。
出于”想要增加只针对自己通知的功能”的理由而进行了实现。
def mumbling
# 独り言です。
RoomChannel.broadcast_to(@user, content: '(゚Д゚;)', account: self.current_user.account, type: :mumbling)
end
吠叫
通过点击”汪汪”和”喵喵”按钮,将这些动物的叫声发送给同一房间中的活跃用户。
对于那些提供了许多示例的聊天应用来说,这个方法非常关键。
def bark(data)
# 全員に送ります。
RoomChannel.broadcast_to( \
@room, content: data["content"], account: self.current_user.account, type: :bark)
end
开始

我想确认一下动作,但是……
由于本文设想使用应用程序作为访问前提,在创建项目时选择了API模式,因此没有提供适用于Web的CoffeeScript等内容。我将把这部分内容留给iOS/Android部分进行介绍。
最后
我认为ActionCable的文档很易懂。然而,案例和信息量都很少,而且还混合了Beta版本的信息,让人难以确定哪个是正确的。虽然我没有建立经验,但这一点可以说是”Socket.IO强大”。希望本次构建对您有所参考。