# 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端口,可以用瀏覽器訪問。

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`里的動作,就會接收到服務器端的推送消息,這也正是聊天界面的效果。
效果如下:

本篇完結。
下一篇:[websocket之message\_bus(六)](http://www.rails365.net/articles/websocket-message-bus-liu)