# 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)