1. 介绍

    上一篇讲了actioncable的基本使用,也搭建了一个简易的聊天室。但是只是照着代码敲那是不行的,要知其然并知其所以然,这节来讲讲actioncable进阶方面的内容,对actioncable有一个更高的理解。

    1. 使用

    下面会分别从几个方面来讲解actioncable,首先从安全领域来说说。
    2.1 跨域

    之前在本地测试环境,应用都是跑在3000端口上的,现在把它跑在4000端口上,看会是怎样的。
    后台会不断地提示下面这行信息。

    1. Request origin not allowed: http://localhost:4000

    其实跑在4000端口的时候,websocket是连不上的。
    因为actioncable默认只在3000端口上开放websocket服务,这个可以查看其源码得到:

    1. #https://github.com/rails/rails/blob/master/actioncable/lib/action_cable/engine.rb#L25
    2. options.allowed_request_origins ||= "http://localhost:3000" if ::Rails.env.development?

    actioncable也提供了机制来解决这个问题。
    比如在配置文件(比如: application.rb)中添加一行:

    1. config.action_cable.allowed_request_origins = ['http://rubyonrails.com', /http:\/\/ruby.*/]

    或者干脆关闭了跨域的检查

    1. config.action_cable.disable_request_forgery_protection = true

    源码可见于此处:

    1. #https://github.com/rails/rails/blob/71657146374595b6b9b04916649922fa4f5f512d/actioncable/lib/action_cable/connection/base.rb#L195
    2. def allow_request_origin?
    3. return true if server.config.disable_request_forgery_protection
    4. if Array(server.config.allowed_request_origins).any? { |allowed_origin| allowed_origin === env['HTTP_ORIGIN'] }
    5. true
    6. else
    7. logger.error("Request origin not allowed: #{env['HTTP_ORIGIN']}")
    8. false
    9. end
    10. end

    2.2 用户系统

    刚才从整个网站的安全出发讲解了websocket的安全问题,现在要从刚细颗粒的地方讲解安全,那就是用户系统,意思就是说,不是每个使用网站的用户都能使用websocket,比如登录的用户才能使用,不登录的用户就过滤掉。
    做一切的关键的文件在于app/channels/application_cable/connection.rb这个文件。
    现在我们把其改写一下:

    1. # Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
    2. module ApplicationCable
    3. class Connection < ActionCable::Connection::Base
    4. identified_by :current_user
    5. def connect
    6. self.current_user = find_verified_user
    7. end
    8. protected
    9. def find_verified_user
    10. cookies.signed[:username] || reject_unauthorized_connection
    11. end
    12. end
    13. end

    意思就是说,带有cookies.signed[:username]的用户才是被允许的,不然就是拒绝连接reject_unauthorized_connection。
    现在我们先创建一个登录界面:

    1. # app/views/sessions/new.html.erb
    2. <%= form_for :session, url: sessions_path do |f| %>
    3. <%= f.label :username, 'Enter a username' %><br/>
    4. <%= f.text_field :username %><br/>
    5. <%= f.submit 'Start chatting' %>
    6. <% end %>
    7. # app/controllers/sessions_controller.rb
    8. class SessionsController < ApplicationController
    9. def create
    10. cookies.signed[:username] = params[:session][:username]
    11. redirect_to "/rooms/show"
    12. end
    13. end
    14. # config/routes.rb
    15. root 'sessions#new'

    登录界面是这样的:
    9、actioncable 进阶 - 图1

    登录之后,后台的日志信息就会多了这行:
    Registered connection (随风)

    如果把cookies信息清掉,也就是没有登录的情况,后台就会提示下面的信息:

    1. An unauthorized connection attempt was rejected
    2. Failed to upgrade to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)

    表示无法连接到websocket服务。
    这个就解决了用户系统登录的问题的。
    2.3 适配器

    在actioncable源码里定义了好几种适配器,比如redis的pub/sub,还有postgresql的notify。
    源码可见于:https://github.com/rails/rails/tree/master/actioncable/lib/action_cable/subscription_adapter。
    先不管什么是适配器,我们先用redis来试试。
    改变config/cable.yml文件,内容如下:

    1. # Action Cable uses Redis by default to administer connections, channels, and sending/receiving messages over the WebSocket.
    2. production:
    3. adapter: redis
    4. url: redis://localhost:6379/1
    5. development:
    6. adapter: redis
    7. url: redis://localhost:6379/1
    8. # adapter: async
    9. test:
    10. adapter: async

    在Gemfile文件里添加下面这行:
    gem ‘redis’

    执行bundle并重启服务器。
    再用redis-cli monitor命令进入redis的终端界面,并监控redis的运行情况。
    当我登录聊天室的时候,monitor监控界面会出现下面一行:
    1461656197.311821 [1 127.0.0.1:58177] “subscribe” “room_channel”

    表示在订阅room_channel这个通道。
    因为我们之前app/channels/room_channel.rb文件的内容是这样的:

    1. class RoomChannel < ApplicationCable::Channel
    2. def subscribed
    3. stream_from "room_channel"
    4. end
    5. ...
    6. end

    我们也定义了一个叫room_channel的通道,所以跟之前redis的数据对应起来。
    现在我们键入聊天信息,并回车。
    monitor界面会出现类似下面的信息:

    1. 1461656387.284232 [1 127.0.0.1:58179] "publish" "room_channel" "{\"message\":\"\\u003cdiv class=\xe2\x80\x9cmessage\xe2\x80\x9d\\u003e\\n \\u003cp\\u003e11111\\u003c/p\\u003e\\n\\u003c/div\\u003e\\n\"}"

    表示正在room_channel通道上广播消息。
    redis的pub/sub机制就是一种广播机制,它能够把一个消息向多个客户端传递,我们实现聊天室正是需要这样的功能,所以actioncable就可以利用它来当适配器,类似的机制也可以使用postgresql的notify机制,也是一样的道理,就是多个客户端订阅一个通道,能够同时接收通道的信息。
    不像actioncable自己封装了redis的pub/sub机制,在websocket之用tubesock在rails实现聊天室(五)这篇文章有介绍过直接用redis的pub/sub机制。
    比如下面的代码:

    1. def chat
    2. hijack do |tubesock|
    3. redis_thread = Thread.new do
    4. Redis.new.subscribe "chat" do |on|
    5. on.message do |channel, message|
    6. tubesock.send_data message
    7. end
    8. end
    9. end
    10. tubesock.onmessage do |m|
    11. Redis.new.publish "chat", m
    12. end
    13. tubesock.onclose do
    14. redis_thread.kill
    15. end
    16. end
    17. end

    也可以自己实现最简单的适配器,其实就是用一个数组。比如默认的async适配器,就是用类似的方法实现的。原理是这样的,比如一个websocket连接进到服务器来了,就把这个socket存进数组中,每个数组的内容都是socket的连接,比如要广播消息的话,就是直接循环这个数据,分别往里面发送信息即可,比如,socket.write(“hello”)。
    2.4 服务器运行

    可以有两种方式来运行actioncable提供的websocket服务。第一种是以Rack socket hijacking API的方式来运行,这个跟之前tubesock这个gem是一样的,它跟web进程集成在一起,以挂载的方式挂载到一个路径中。
    正如上文所说的,可以在路由中挂载,比如:

    1. # config/routes.rb
    2. Rails.application.routes.draw do
    3. mount ActionCable.server => '/cable'
    4. end

    还有另外一种是在配置文件中修改。

    1. # config/application.rb
    2. class Application < Rails::Application
    3. config.action_cable.mount_path = '/websocket'
    4. end

    另一种运行websocket的方式是Standalone。它的意思是把websocket服务运行在另一个进程中,因为它仍然是一个rack应用程序,只要支持Rack socket hijacking API的应用程序都可以运行,比如puma,unicorn等。
    新建cable/config.ru文件,内容如下:

    1. require ::File.expand_path('../../config/environment', __FILE__)
    2. Rails.application.eager_load!
    3. run ActionCable.server

    然后再新建bin/cable可执行文件,内容如下:

    1. #!/bin/bash
    2. bundle exec puma -p 28080 cable/config.ru

    使用bin/cable命令可运行。
    关于websocket的服务器部署后续有另外的章节介绍。
    2.5 js客户端

    浏览器要与客户端保持链接,必须像之前那样主动发送websocket请求。
    rails 5中默认生成了一个文件,叫app/assets/javascripts/cable.coffee,把下面两行注释拿掉:

    1. @App ||= {}
    2. App.cable = ActionCable.createConsumer()

    默认情况下,websocket服务器的地址是/cable。
    可以从源码上看到这个实现。

    1. # https://github.com/rails/rails/blob/52ce6ece8c8f74064bb64e0a0b1ddd83092718e1/actioncable/app/assets/javascripts/action_cable.coffee.erb#L8
    2. @ActionCable =
    3. INTERNAL: <%= ActionCable::INTERNAL.to_json %>
    4. createConsumer: (url) ->
    5. url ?= @getConfig("url") ? @INTERNAL.default_mount_path
    6. new ActionCable.Consumer @createWebSocketURL(url)

    其中,@INTERNAL.default_mount_path就是/cable。

    1. # https://github.com/rails/rails/blob/7f043ffb427c1beda16cc97a991599be808fffc3/actioncable/lib/action_cable.rb#L38
    2. INTERNAL = {
    3. message_types: {
    4. welcome: 'welcome'.freeze,
    5. ping: 'ping'.freeze,
    6. confirmation: 'confirm_subscription'.freeze,
    7. rejection: 'reject_subscription'.freeze
    8. },
    9. default_mount_path: '/cable'.freeze,
    10. protocols: ["actioncable-v1-json".freeze, "actioncable-unsupported".freeze].freeze
    11. }

    按照前文所说,可以把服务器部署到另外一台主机上,或者说,我不想用默认的/cable路径,有时候,开发环境和生产环境的情况根本就是两码事,本地可以随意,但线上也许是另外的服务器,或者说,本地可以是ws协议,线上是wss协议。
    actioncable也提供了一个简单的配置参数。

    1. config.action_cable.url = "ws://example.com:28080"

    不过,这个需要在layout上加上这行:

    1. <%= action_cable_meta_tag %>
    2. 它的源码是这样的:
    3. def action_cable_meta_tag
    4. tag "meta", name: "action-cable-url", content: (
    5. ActionCable.server.config.url ||
    6. ActionCable.server.config.mount_path ||
    7. raise("No Action Cable URL configured -- please configure this at config.action_cable.url")
    8. )
    9. end

    就只是生成一个html的标签,被js的createConsumer利用,具体可以看createConsumer的方法。
    本篇完结。