世界上最伟大的投资就是投资自己的教育

首页WebSocket
随风 · 练气

Websocket 学习笔记系列文章教程之 actioncable 进阶 (八)

随风发布于1802 次阅读

1. 介绍

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

2. 使用

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

2.1 跨域

之前在本地测试环境,应用都是跑在 3000 端口上的,现在把它跑在 4000 端口上,看会是怎样的。

后台会不断地提示下面这行信息。

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

其实跑在 4000 端口的时候,websocket 是连不上的。

因为 actioncable 默认只在 3000 端口上开放 websocket 服务,这个可以查看其源码得到:

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

actioncable 也提供了机制来解决这个问题。

比如在配置文件 (比如: application.rb) 中添加一行:

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

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

config.action_cable.disable_request_forgery_protection = true

源码可见于此处:

#https://github.com/rails/rails/blob/71657146374595b6b9b04916649922fa4f5f512d/actioncable/lib/action_cable/connection/base.rb#L195
def allow_request_origin?
  return true if server.config.disable_request_forgery_protection

  if Array(server.config.allowed_request_origins).any? { |allowed_origin|  allowed_origin === env['HTTP_ORIGIN'] }
    true
  else
    logger.error("Request origin not allowed: #{env['HTTP_ORIGIN']}")
    false
  end
end
2.2 用户系统

刚才从整个网站的安全出发讲解了 websocket 的安全问题,现在要从刚细颗粒的地方讲解安全,那就是用户系统,意思就是说,不是每个使用网站的用户都能使用 websocket,比如登录的用户才能使用,不登录的用户就过滤掉。

做一切的关键的文件在于app/channels/application_cable/connection.rb这个文件。

现在我们把其改写一下:

# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    protected
      def find_verified_user
        cookies.signed[:username] || reject_unauthorized_connection
      end
  end
end

意思就是说,带有cookies.signed[:username]的用户才是被允许的,不然就是拒绝连接reject_unauthorized_connection

现在我们先创建一个登录界面:

# app/views/sessions/new.html.erb
<%= form_for :session, url: sessions_path do |f| %>
  <%= f.label :username, 'Enter a username' %><br/>
  <%= f.text_field :username %><br/>
  <%= f.submit 'Start chatting' %>
<% end %>
# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
  def create
    cookies.signed[:username] = params[:session][:username]
    redirect_to "/rooms/show"
  end
end
# config/routes.rb
root 'sessions#new'

登录界面是这样的:

登录之后,后台的日志信息就会多了这行:

Registered connection (随风)

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

An unauthorized connection attempt was rejected
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文件,内容如下:

# Action Cable uses Redis by default to administer connections, channels, and sending/receiving messages over the WebSocket.
production:
  adapter: redis
  url: redis://localhost:6379/1

development:
  adapter: redis
  url: redis://localhost:6379/1
  # adapter: async

test:
  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文件的内容是这样的:

class RoomChannel < ApplicationCable::Channel
  def subscribed
    stream_from "room_channel"
  end
  ...
end

我们也定义了一个叫room_channel的通道,所以跟之前 redis 的数据对应起来。

现在我们键入聊天信息,并回车。

monitor界面会出现类似下面的信息:

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 机制。

比如下面的代码:

def chat
  hijack do |tubesock|
    redis_thread = Thread.new do
      Redis.new.subscribe "chat" do |on|
        on.message do |channel, message|
          tubesock.send_data message
        end
      end
    end

    tubesock.onmessage do |m|
      Redis.new.publish "chat", m
    end

    tubesock.onclose do
      redis_thread.kill
    end
  end
end

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

2.4 服务器运行

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

正如上文所说的,可以在路由中挂载,比如:

# config/routes.rb
Rails.application.routes.draw do
  mount ActionCable.server : '/cable'
end

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

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

另一种运行 websocket 的方式是Standalone。它的意思是把 websocket 服务运行在另一个进程中,因为它仍然是一个 rack 应用程序,只要支持Rack socket hijacking API的应用程序都可以运行,比如 puma,unicorn 等。

新建cable/config.ru文件,内容如下:

require ::File.expand_path('../../config/environment', __FILE__)
Rails.application.eager_load!

run ActionCable.server

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

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

使用bin/cable命令可运行。

关于 websocket 的服务器部署后续有另外的章节介绍。

2.5 js 客户端

浏览器要与客户端保持链接,必须像之前那样主动发送 websocket 请求。

rails 5中默认生成了一个文件,叫app/assets/javascripts/cable.coffee,把下面两行注释拿掉:

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

默认情况下,websocket 服务器的地址是/cable

可以从源码上看到这个实现。

# https://github.com/rails/rails/blob/52ce6ece8c8f74064bb64e0a0b1ddd83092718e1/actioncable/app/assets/javascripts/action_cable.coffee.erb#L8
@ActionCable =
  INTERNAL: <%= ActionCable::INTERNAL.to_json %>

  createConsumer: (url) ->
    url ?= @getConfig("url") ? @INTERNAL.default_mount_path
    new ActionCable.Consumer @createWebSocketURL(url)

其中,@INTERNAL.default_mount_path就是/cable

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

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

actioncable 也提供了一个简单的配置参数。

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

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

<%= action_cable_meta_tag %>

它的源码是这样的:

def action_cable_meta_tag
  tag "meta", name: "action-cable-url", content: (
    ActionCable.server.config.url ||
    ActionCable.server.config.mount_path ||
    raise("No Action Cable URL configured -- please configure this at config.action_cable.url")
  )
end

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

本篇完结。

下一篇:websocket 之 actioncable 实现重新连接功能 (九)

本站文章均为原创内容,如需转载请注明出处,谢谢。

0 条回复
暂无回复~~
相关小书
websocket教程

websocket教程

从websocket的介绍开始,从入门到精通

发表于

喜欢
我的微信官网服务号精品文章订阅号微信视频号
程序员随风
统计信息
    学员: 22263
    视频数量: 1467
    文章数量: 461

© 汕尾市求知科技有限公司 | 专业版网站 | 关于我们 | 在线学员:1150

粤公网安备 44152102000088号粤公网安备 44152102000088号 | 粤ICP备19038915号

Top