# 5. 用 tubesock 在 Rails 实现聊天室
#### 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端口,可以用浏览器访问。
![](https://box.kancloud.cn/dd02efb4fcc4ce82ae9bdff00e762824_394x129.png)
puma,unicorn等服务器对hijack的实现是很简单的,本来他们就是对socket的操作,现在只不过是提供了一个接口,把它放到请求的全局变量中罢了,还增加了一些状态判断。主要是这三个变量`env['rack.hijack']`,`env['rack.hijack?']`,`env['rack.hijack_io']`。
#### 3. Tubesock
[tubesock](https://github.com/ngauthier/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>
<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`里的动作,就会接收到服务器端的推送消息,这也正是聊天界面的效果。
效果如下:
![](https://box.kancloud.cn/1443db8d973f9b06d1d78f061c361814_801x420.png)
本篇完结。
下一篇:[websocket之message\_bus(六)](http://www.rails365.net/articles/websocket-message-bus-liu)