1. 介绍
前篇文章介绍了如何实现一个简易的聊天室,有时候,我们在rails应用中也是需要使用websocket的功能,比如,消息的通知,一些数据状态的通知等,所以这篇来介绍下如何简单地实现这个功能。
2. rack hijack
这篇文章主要介绍的是一个比较重要的概念,它是rack hijack。hijack是rack在1.5.0之后才支持,它出现的目的是为了能在rack层次对socket连接进行操作。能对底层的socket进行操作,也就能使用websocket。puma,unicorn等服务器都有它的实现。
新建一个文件叫hijack.ru,内容如下:
use Rack::Lint
use Rack::ContentLength
use Rack::ContentType, "text/plain"
class DieIfUsed
def each
abort "body.each called after response hijack\n"
end
def close
abort "body.close called after response hijack\n"
end
end
run lambda { |env|
case env["PATH_INFO"]
when "/hijack_req"
if env["rack.hijack?"]
io = env["rack.hijack"].call
if io.respond_to?(:read_nonblock) &&
env["rack.hijack_io"].respond_to?(:read_nonblock)
# exercise both, since we Rack::Lint may use different objects
env["rack.hijack_io"].write("HTTP/1.0 200 OK\r\n\r\n")
io.write("request.hijacked")
io.close
return [ 500, {}, DieIfUsed.new ]
end
end
[ 500, {}, [ "hijack BAD\n" ] ]
when "/hijack_res"
r = "response.hijacked"
[ 200,
{
"Content-Length" => r.bytesize.to_s,
"rack.hijack" => proc do |io|
io.write(r)
io.close
end
},
DieIfUsed.new
]
end
}
其中env['rack.hijack'].call
就是返回socket的文件描述符的对象,之后可以对这个对象进行像socket那样的操作,比如io.write("request.hijacked")
,就是返回“request.hijacked”。
使用下面的指令运行这段代码:
$ unicorn hijack
I, [2016-04-12T15:44:53.197379 #18197] INFO -- : listening on addr=0.0.0.0:8080 fd=9
I, [2016-04-12T15:44:53.197564 #18197] INFO -- : worker=0 spawning...
I, [2016-04-12T15:44:53.201453 #18197] INFO -- : master process ready
I, [2016-04-12T15:44:53.203755 #18226] INFO -- : worker=0 spawned pid=18226
I, [2016-04-12T15:44:53.204682 #18226] INFO -- : Refreshing Gem list
I, [2016-04-12T15:44:53.315295 #18226] INFO -- : worker=0 ready
监听在8080端口,可以用浏览器访问。
puma,unicorn等服务器对hijack的实现是很简单的,本来他们就是对socket的操作,现在只不过是提供了一个接口,把它放到请求的全局变量中罢了,还增加了一些状态判断。主要是这三个变量env['rack.hijack']
,env['rack.hijack?']
,env['rack.hijack_io']
。
3. Tubesock
tubesock是一个gem,它就是对上面的rack hijack
进行封装,从而能实现websocket功能,它不仅能在rack中实现,也能在rails中的controller使用。
现在我们来在rails中结合redis的pub/sub功能实现一个聊天室功能。
首先安装,我们使用puma作为服务器。
在Gemfile中添加下面几行。
gem 'puma'
gem 'redis-rails'
gem 'tubesock'
添加app/controllers/chat_controller.rb
文件,内容如下:
class ChatController < ApplicationController
include Tubesock::Hijack
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
end
在config/routes.rb
中添加路由。
Rails.application.routes.draw do
get "/chat", to: "chat#chat"
end
分别添加view和js。
<h1>Tubesock Chat</h1>
<pre id="output"></pre>
<form class="chat">
<input placeholder="hello world" autofocus>
</form>
$ ->
socket = new WebSocket "ws://#{window.location.host}/chat"
socket.onmessage = (event) ->
if event.data.length
$("#output").append "#{event.data}<br>"
$("body").on "submit", "form.chat", (event) ->
event.preventDefault()
$input = $(this).find("input")
socket.send $input.val()
$input.val(null)
对上面的代码进行解析:
假如有一个浏览器客户端打开了,就会运行new WebSocket "ws://#{window.location.host}/chat"
。
这样就到了ChatController
中的chat
方法。
执行了下面的语句:
redis_thread = Thread.new do
Redis.new.subscribe "chat" do |on|
on.message do |channel, message|
tubesock.send_data message
end
end
end
将会开启一个新的线程,并会用Redis去订阅一个新的频道chat
,进入到subscribe
方法中,tubesock.send_data message
表示一旦有消息过来就立即用tubesock这个socket把数据返回给客户端浏览器。
tubesock.onmessage do |m|
Redis.new.publish "chat", m
end
上面的代码表示一旦服务器接收到客户端浏览器的消息之后的动作,比如说,在聊天界面输入消息内容。接收到消息之后就立即发送到上面所说的chat
通道,上面Redis中的subscribe
动作就会被触发。因为所有的客户端一连上服务器就会执行Redis的subscribe
功能,也就是说所有浏览器客户端都会触发subscribe
里的动作,就会接收到服务器端的推送消息,这也正是聊天界面的效果。
效果如下:
本篇完结。