使用Nginx、Redis和MySQL,创建一个简单而实用的Rails ActionCable实战案例,并制作一个涵盖整体内容的iOS/Android示例应用程序〜后端篇〜

开场白

我想要使用Rails ActionCable实现双向通信。我创建了一个示例应用程序,想要在移动应用程序中制作实时通信应用程序。有关每个细节的解释已经有相关的文章了,所以这里只介绍环境设置和源代码的概述。下面是三个部分的结构。

    • バックエンド(本記事)

 

    • iOS

 

    Android

介绍玩耍样本

ios_demo.gif

在此应用程序中,有两种主要的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

    1. 允許origin。

 

    1. 如同在Nginx配置中提到的,由于使用自签名证书,无法正确进行SSL握手,使得通信无法正常进行。在这种环境下,允许明文的WebSocket通信(ws://)。如果导入了正规的SSL证书,则只需指定wss://即可解决问题。

 

    1. 为了在Android模拟器中运行,将disable_request_forgery_protection设置为true,放宽发送端的限制。如果不进行这个设置,会出现以下错误。

 

    1. 作者是先开发了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
    1. 指定订阅适配器为Redis

 

    1. 开发环境的适配器默认为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'
    1. 在Redis中进行设置

 

    1. 由于在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

开始

image

我想确认一下动作,但是……

由于本文设想使用应用程序作为访问前提,在创建项目时选择了API模式,因此没有提供适用于Web的CoffeeScript等内容。我将把这部分内容留给iOS/Android部分进行介绍。

最后

我认为ActionCable的文档很易懂。然而,案例和信息量都很少,而且还混合了Beta版本的信息,让人难以确定哪个是正确的。虽然我没有建立经验,但这一点可以说是”Socket.IO强大”。希望本次构建对您有所参考。

bannerAds