1. 介绍

    前篇文章介绍了如何实现一个简易的聊天室,有时候,我们在rails应用中也是需要使用websocket的功能,比如,消息的通知,一些数据状态的通知等,所以这篇来介绍下如何简单地实现这个功能。

    1. rack hijack

    这篇文章主要介绍的是一个比较重要的概念,它是rack hijack。hijack是rack在1.5.0之后才支持,它出现的目的是为了能在rack层次对socket连接进行操作。能对底层的socket进行操作,也就能使用websocket。puma,unicorn等服务器都有它的实现。
    新建一个文件叫hijack.ru,内容如下:

    1. use Rack::Lint
    2. use Rack::ContentLength
    3. use Rack::ContentType, "text/plain"
    4. class DieIfUsed
    5. def each
    6. abort "body.each called after response hijack\n"
    7. end
    8. def close
    9. abort "body.close called after response hijack\n"
    10. end
    11. end
    12. run lambda { |env|
    13. case env["PATH_INFO"]
    14. when "/hijack_req"
    15. if env["rack.hijack?"]
    16. io = env["rack.hijack"].call
    17. if io.respond_to?(:read_nonblock) &&
    18. env["rack.hijack_io"].respond_to?(:read_nonblock)
    19. # exercise both, since we Rack::Lint may use different objects
    20. env["rack.hijack_io"].write("HTTP/1.0 200 OK\r\n\r\n")
    21. io.write("request.hijacked")
    22. io.close
    23. return [ 500, {}, DieIfUsed.new ]
    24. end
    25. end
    26. [ 500, {}, [ "hijack BAD\n" ] ]
    27. when "/hijack_res"
    28. r = "response.hijacked"
    29. [ 200,
    30. {
    31. "Content-Length" => r.bytesize.to_s,
    32. "rack.hijack" => proc do |io|
    33. io.write(r)
    34. io.close
    35. end
    36. },
    37. DieIfUsed.new
    38. ]
    39. end
    40. }

    其中env[‘rack.hijack’].call就是返回socket的文件描述符的对象,之后可以对这个对象进行像socket那样的操作,比如io.write(“request.hijacked”),就是返回“request.hijacked”。
    使用下面的指令运行这段代码:

    1. $ unicorn hijack
    2. I, [2016-04-12T15:44:53.197379 #18197] INFO -- : listening on addr=0.0.0.0:8080 fd=9
    3. I, [2016-04-12T15:44:53.197564 #18197] INFO -- : worker=0 spawning...
    4. I, [2016-04-12T15:44:53.201453 #18197] INFO -- : master process ready
    5. I, [2016-04-12T15:44:53.203755 #18226] INFO -- : worker=0 spawned pid=18226
    6. I, [2016-04-12T15:44:53.204682 #18226] INFO -- : Refreshing Gem list
    7. I, [2016-04-12T15:44:53.315295 #18226] INFO -- : worker=0 ready

    监听在8080端口,可以用浏览器访问。
    6、用 tubesock 在 Rails 实现聊天室 - 图1

    puma,unicorn等服务器对hijack的实现是很简单的,本来他们就是对socket的操作,现在只不过是提供了一个接口,把它放到请求的全局变量中罢了,还增加了一些状态判断。主要是这三个变量env[‘rack.hijack’],env[‘rack.hijack?’],env[‘rack.hijack_io’]。

    1. Tubesock

    tubesock是一个gem,它就是对上面的rack hijack进行封装,从而能实现websocket功能,它不仅能在rack中实现,也能在rails中的controller使用。
    现在我们来在rails中结合redis的pub/sub功能实现一个聊天室功能。
    首先安装,我们使用puma作为服务器。
    在Gemfile中添加下面几行。

    1. gem 'puma'
    2. gem 'redis-rails'
    3. gem 'tubesock'

    添加app/controllers/chat_controller.rb文件,内容如下:

    1. class ChatController < ApplicationController
    2. include Tubesock::Hijack
    3. def chat
    4. hijack do |tubesock|
    5. redis_thread = Thread.new do
    6. Redis.new.subscribe "chat" do |on|
    7. on.message do |channel, message|
    8. tubesock.send_data message
    9. end
    10. end
    11. end
    12. tubesock.onmessage do |m|
    13. Redis.new.publish "chat", m
    14. end
    15. tubesock.onclose do
    16. redis_thread.kill
    17. end
    18. end
    19. end
    20. end

    在config/routes.rb中添加路由。

    1. Rails.application.routes.draw do
    2. get "/chat", to: "chat#chat"
    3. end

    分别添加view和js。

    1. <h1>Tubesock Chat</h1>
    2. <form class="chat">
    3. <input placeholder="hello world" autofocus>
    4. </form>
    5. $ ->
    6. socket = new WebSocket "ws://#{window.location.host}/chat"
    7. socket.onmessage = (event) ->
    8. if event.data.length
    9. $("#output").append "#{event.data}<br>"
    10. $("body").on "submit", "form.chat", (event) ->
    11. event.preventDefault()
    12. $input = $(this).find("input")
    13. socket.send $input.val()
    14. $input.val(null)

    对上面的代码进行解析:
    假如有一个浏览器客户端打开了,就会运行new WebSocket “ws://#{window.location.host}/chat”。
    这样就到了ChatController中的chat方法。
    执行了下面的语句:

    1. redis_thread = Thread.new do
    2. Redis.new.subscribe "chat" do |on|
    3. on.message do |channel, message|
    4. tubesock.send_data message
    5. end
    6. end
    7. end

    将会开启一个新的线程,并会用Redis去订阅一个新的频道chat,进入到subscribe方法中,tubesock.send_data message表示一旦有消息过来就立即用tubesock这个socket把数据返回给客户端浏览器。

    1. tubesock.onmessage do |m|
    2. Redis.new.publish "chat", m
    3. end

    上面的代码表示一旦服务器接收到客户端浏览器的消息之后的动作,比如说,在聊天界面输入消息内容。接收到消息之后就立即发送到上面所说的chat通道,上面Redis中的subscribe动作就会被触发。因为所有的客户端一连上服务器就会执行Redis的subscribe功能,也就是说所有浏览器客户端都会触发subscribe里的动作,就会接收到服务器端的推送消息,这也正是聊天界面的效果。
    效果如下:
    6、用 tubesock 在 Rails 实现聊天室 - 图2

    本篇完结。