世界上最伟大的投资就是投资自己的教育
Websocket 学习笔记系列文章教程之 actioncable 进阶 (八)
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
的方法。
本篇完结。
本站文章均为原创内容,如需转载请注明出处,谢谢。
© 汕尾市求知科技有限公司 | Rails365 Gitlab | 知乎 | b 站 | csdn
粤公网安备 44152102000088号 | 粤ICP备19038915号
Top