Gem 为什么我们需要 Rack ?

suffering · 2014年09月13日 · 最后由 monkeygq 回复于 2017年04月21日 · 23768 次阅读
本帖已被管理员设置为精华贴

一切从 Rack 开始

Rails 就是一个 Rack app. 实际上,基本上所有的 Ruby web framework 都是rack app.

官网中列出的使用 Rack 的 web 框架:

  • Camping
  • Coset
  • Espresso
  • Halcyon
  • Mack
  • Maveric
  • Merb
  • Racktools::SimpleApplication
  • Ramaze
  • Ruby on Rails
  • Rum
  • Sinatra
  • Sin
  • Vintage
  • Waves
  • Wee

基本上,Ruby 世界的 web, rack 已经一统天下了。有兴趣的童鞋可以看看ruby-toolboxweb-app-framework, 看看有哪些没有用到 rack 的:https://www.ruby-toolbox.com/categories/web_app_frameworks

简介

Rack provides a minimal, modular and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between(the so-called middleware) into a single method call.

简单点说,rack 是 Ruby web 应用的简单的模块化的接口。它封装 HTTP 请求与响应,并提供大量的实用工具。它需要一个响应 call 方法的对象,接受 env. 返回三元素的数组:分别是 status code, header, body. 其中 status code 大于等于 100, 小于 600. header 是一个 hash, body 是一个响应 each 方法的数组。

理解上述接口标准,就可以写一个完整的 rack app 了。除了这些,rack 还提供了许多有工具。

gem install rack
gem install pry
pry
require 'rack'
cd Rack
ls

上述得到如下内容:

constants:
  Auth       CommonLogger    Deflater        Handler  MethodOverride  NullLogger  Runtime         ShowStatus
  BodyProxy  ConditionalGet  Directory       Head     Mime            Recursive   Sendfile        Static
  Builder    Config          ETag            Lint     MockRequest     Reloader    Server          URLMap
  Cascade    ContentLength   File            Lock     MockResponse    Request     Session         Utils
  Chunked    ContentType     ForwardRequest  Logger   Multipart       Response    ShowExceptions  VERSION
Rack.methods: release  version

以上内容,除少部分是 Rack 自身运行的依赖外,大部分都是 Rack 提供的可选模块,它们封装了许多简单实用的方法,比如说处理静态文件,缓存,log, 非法内容 sanitize . 使用它们,Ruby web 开发才不会那么痛苦. 另外,还有许多 Rack middleware. 这里有详细列表: https://github.com/rack/rack/wiki/List-of-Middleware

为什么需要 Rack

这里不防用反证法,带大家看看若是没有 Rack, 我们应该如何开发一个 Ruby web app.

如果从零开始了解 web 世界。我们知道,浏览器与服务端通过 HTTP 协议交互。这是一个 request 与 response 的过程。

request 从客户端发出,包含了字符串的头文件,它包括请求的地址,请求方式 (get/post/put/delete 等). 浏览器发送的是格式化的字符串,作为服务端,我们需要分析这段字符串,如果没有 Rack, 我们得自己程序来分析这个 orgin string header.

同样的,按照 http 协议,Response 也是类似的字符串。浏览器接收到它们后,分析并 render, 最终生成页面。没有 rack, 这字符串也得自己来生成。

这里看一个例子,非常有意思的博文。博主去面试,让他写一个简单的 Ruby web server, 要求如下:

  1. Web server returns “Hello World”.
  2. Web server returns the list of files in the base directory.
  3. Web server allows to navigate the directory structure.
  4. If a user click on a file the browser should display it.

总计 4 条要求,其中第一二条实现了,后面两条却失败了 .事后好好研究了下这个,并写出了博客。这个是第三条的实现,就是自己手动输出 response string :

require "socket"
webserver = TCPServer.new('localhost', 2000)
base_dir = Dir.new(".")
while(session = webserver.accept)
  session.print "HTTP/1.1 200/OK\r\nContent-type:text/html\r\n\r\n"

  request = session.gets
  trimmedrequest = request.gsub(/GET\ \//, '').gsub(/\ HTTP.*/, '')
  if trimmedrequest.chomp != ""
    base_dir = Dir.new("./#{trimmedrequest}".chomp)
  end
  session.print "
#{trimmedrequest}

"

  session.print("#{base_dir}")
  if Dir.exists? base_dir
     base_dir.entries.each do |f|
       if File.directory? f
         session.print("<a href="#{f}"> #{f}</a>")
       else
        session.print("
#{f}

")
       end
     end
  else
    session.print("Directory does not exists!")
  end
  session.close
end

原文地址:A Simple Web Server in Ruby

就像terminal端的 Ruby code 运行时通过 gets 方法来获取用户的输入一样,真是简陋得可以。 仔细看一下博主的实现,就是将request当字符串处理 (其实 request 本来就是格式化的字符串), 通过正则来获取其REQUEST_METHOD, PATH_INFO等等,将Content-Type设置为text/html, 最后将信息打包,作为response发送给客户端. 至于第四条,则需要手动去设置mimi-type,这样才能以完整的方式将图片等内容在 web 页面中正常展现。

看看 http request header fields: http://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Request_fields 数十条,各有其含义。从头至尾写一个 web server, 你得分析它们,作不同的响应。

再看看 http response fields: http://en.wikipedia.org/wiki/List_of_HTTP_header_fields#Response_fields 同样数十条,浏览器会根据你返回的 header 来决定如何 render page. 如果手动去生成,那是头皮发麻的工作量。

但是,使用 Rack, 以及它提供的工具的话,实现博主接受的考题要求,我们可以这样做,只需要两行代码:

require 'rack'
Rack::Handler::Thin.run Rack::Directory.new('./'), :Port => 9292 

当然,是否允许这样做就不得而知了。

数行代码的rack app

gem install rack
cd to/your/path
touch app.rb

app.rb内容如下;

#app.rb
require 'rack'
class HelloWorld
  def call(env)
    [200, {"Content-Type" => "text/html"}, ["Hello Rack!"]]
  end
end
Rack::Handler::Mongrel.run HelloWorld.new, :Port => 9292

在 terminal 里运行ruby app.rb, 而后在浏览器里打开http://localhost:9292就可以看到返回的内容了。

使用 middleware stack 的 rack app

#config.ru

# 将 body 标签的内容转换为全大写.
class ToUpper
  def initialize(app)
    @app = app
  end
  def call(env)
    status, head, body = @app.call(env)
    upcased_body = body.map{|chunk| chunk.upcase }
    [status, head, upcased_body]
  end
end
# 将 body 内容置于标签, 设置字体颜色为红色, 并指明返回的内容为 text/html.
class WrapWithRedP
  def initialize(app)
    @app = app
  end
  def call(env)
    status, head, body = @app.call(env)
    red_body = body.map{|chunk| "<p style='color:red;'>#{chunk}</p>" }
    head['Content-type'] = 'text/html'
    [status, head, red_body]
  end
end

# 将 body 内容放置到 HTML 文档中.
class WrapWithHtml
  def initialize(app)
    @app = app
  end

  def call(env)
    status, head, body = @app.call(env)
    wrap_html = <<-EOF
       <!DOCTYPE html>
       <html>
         <head>
         <title>hello</title>
         <body>
         #{body[0]}
         </body>
       </html>
    EOF
    [status, head, [wrap_html]]
  end
end

# 起始点, 只返回一行字符的 rack app.
class Hello
  def initialize
    super
  end
  def call(env)
    [200, {'Content-Type' => 'text/plain'}, ["hello, this is a test."]]
  end
end
use WrapWithHtml
use WrapWithRedP
use ToUpper
run Hello.new

直接运行rackup就可以运行上述 app.

use 与 run 本质上没有太大的差别,只是 run 是最先调用的。它们生成一个 statck, 本质上是先调用 Hello.new#call, 而后返回 ternary-array, 而后再将之交给另一个 ToUpper, ToUpper 干完自己的活,再交给 WrapWithRedP, 如此一直到 stack 调用完成。

use ToUpper; run Hello.new本质上是完成如下调用:

ToUpper.new(Hello.new.call(env)).call(env)

以上只是简单的举例,实际的 web 项目中,有无数的场景需求。 比如说,你可以随时需要更改 status code, 或者,你需要判断当前请求是什么类型的,比如说是 get 还是 post, 在 rails 中 resource 生成的同样的 path,如 /products/:id可以是get/put/delete. 但是 Rails 如何知道调用show/update/destroy中的哪一个?这时,这个时候可以去看看 env['REQUEST_METHOD'], 而后判断。这们做虽然不用去分析原始的 head 文件,但是也是愚蠢的行为,这个时候就可以直接用 rack 提供的一些工具了。

require 'rack'
class Hello
    def get_request
       @request ||= Rack::Request.new(@env)
    end

    def response(text, status=200, head={})
      raise "respond" if @respond
      text = [text].flatten
      @response = Rack::Response.new(text, status, head)
    end

    def get_response
      @response || response([])
    end

    %W{get? put? post? delete? patch? trace?}.each do |md|
        define_method md do
            get_request.send(md.intern)
        end
    end
    %W{body headers length= status=  body= header length 
       redirect status content_length content_type}.each do |md|
        define_method md do |*arg|
            get_response.send(md.intern, *arg)
        end
   end

    def call(env)
      @env = env
      content_type   = 'text/plain'
      if get?
        body= ['you send a get request']
      else
        status= 403
        body= ['we do not support request method except get, please try another.']
      end
      [status, headers, body]
    end
end
Rack::Handler::Thin.run Hello.new, :Port => 9292

若想测试非 get 方法,可以通过curl -X POST http://localhost:9292/来测试. 以上示例是对 Rack::Request, Rack::Response 的非常愚蠢的封装,只是展示下使用它们的便捷之处。想看高质的封装代码,不仿看看 sinatra 的: https://github.com/bmizerany/sinatra/blob/work/lib/sinatra/base.rb#L15

Rails On Rack

Rails 中使用的 rack middleware stack:

cd to/your/rails/project/path
rake middleware

得到的内容如下:

use Rack::Sendfile
use ActionDispatch::Static
use Rack::Lock
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x000000029a0838>
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::Migration::CheckPending
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
run Rails.application.routes

可以看出,Rails 重度依赖 Rack. 理解 Rails, 不防从 Rack 开始。按上述列表,先从Rails.application.routes开始 一直走到最后一步. 关于在 Rails 如何使用 rack, 具体请参照 http://guides.rubyonrails.org/rails_on_rack.html 另外这里有xdite一篇博文,讲的是 Rails 如何支持 Rack, 又为什么要支持 Rack, 非常清晰明了。详细可以看看这里:http://wp.xdite.net/?p=1557

结论

:)

更多文档资料

想更深入了解 Rack, 可以参见: http://rack.github.io/ http://wp.xdite.net/?p=1557 http://m.onkey.org/ruby-on-rack-1-hello-rack http://guides.rubyonrails.org/rails_on_rack.html http://rubylearning.com/blog/a-quick-introduction-to-rack/ https://www.digitalocean.com/community/tutorials/a-comparison-of-rack-web-servers-for-ruby-web-applications http://codecondo.com/12-small-ruby-frameworks/ 这里特别推荐 railscasts-china.com 的一篇演讲:http://railscasts-china.com/episodes/the-rails-initialization-process-by-kenshin54

楼主关注贡献干货 20 年 👍

4 楼 已删除

赞,楼主讲解的非常好

必须点赞:thumbsup:

#1 楼 @suffering

Kernel: 呵呵 CPU: 呵呵 半导体电路:呵呵 爱因斯坦:呵呵

#9 楼 @layerssss , 所以,最后说一点页面闪都不闪一下就打开了. 但是其实在后台, 无数的协议, 无数的进程, 这后面为你服务的家伙, 哪怕把名字列出来, 都够你看上二十小时. 那评论只是吐嘈,仅 web 相关,都不知道漏了个大环节多少。

必须点赞!

很疑惑。 我们平时打的命令如:

rake db:migarte

rake routes

和这个 Rake 有什么关系吗?

#12 楼 @linjunzhugg 不一样哦,楼主说的是 Rack,你说的是 Rake。Rack 是 http 服务中间件,Rake 则可以理解为 Ruby make

#12 楼 @linjunzhugg Rake 和 Rack 是两个完全不同的东西,Rack 就不多说了,Rake 的 github 主页上写着:Rake is a Make-like program implemented in Ruby. Tasks and dependencies are specified in standard Ruby syntax.

Rack 的 Ruby Web 开发的基石,可惜 Rack 已经很少更新了:Rack 可能不会发布 2.0 核心团队有些人有了孩子,有些人去用别的技术了,然后 Rack 在架构层面开始落后于其它技术。 看过 Rack 的代码,不是那么好维护呀😅,有时间和能力的诸位要加油 🙏

这个些的真好

再补上一份 Rack 的文档,详细,清晰,想深入研究的童鞋不要错过: http://www.doc88.com/p-209931998825.html

很多应用直接写 rack 就可以了。我们经常被各种 framework 弄的眼花缭乱,却忘记了解最本真的东西。

#14 楼 @dotcomXY #13 楼 @xieyu33333

抱歉抱歉。。。眼瞎了。。。。把 Rake 看成 Rack 了,让我郁闷了半天。谢谢谢谢!!!!

学习了。谢谢。

#15 楼 @ChrisLoong 功能简单,没啥问题,就不需要维护,API 稳定才是王道。

赞赞赞!!!

#21 楼 @sevk 呃,目前的 rack 并非没有 bug,最新的 release 版本 v1.5.2,对包含特殊字符的文件名处理,就有 bug,在 v1.6.2 中修复了,但还是 beta。 HTTP/2明年就成为正式标准了,起码也要跟上HTTP协议的更新速度。

#22 楼 @hbin 哈哈,之前在 twitter 上看到他 show 了 redhat 的工牌。

真不错~学习了~

Ruby 有 Rack 其实是抄袭 Python 的 WSGI。就是一个规范 Web 请求响应的接口。可惜抄袭把 WSGI 不好的地方也抄袭过来了。比如这个接口是同步阻塞的。。。要挑战 nodejs 就得各种 hack。。。。。。。。。。。。。。

写的很棒,赞一个

WrapWithHtml -> [[status, head, [wrap_html]] #左侧多了个方括号

#29 楼 @wikimo , 呃,是呢,谢谢 😄 马上改过来。

文章好长啊,写得不错。 Ruby 的 rack 其实跟 Python 的 wsgi 相似。

nice, mark 一下

#1 楼 @suffering C 再交给 M 加工包装 应该是 C 再交给 V 加工包装,,讲的不错,对于我这半道转入 web 的很适合!

#34 楼 @huopo125 嗯,谢谢提醒呢,已经改过来了。

主楼和 1 楼都写得很好,是很不错的入门读物。

mark 一下,楼主是有心人

有没有什么历史发展上的文章看看?

看了之后感觉以前好多事情柳暗花明,哈哈,夸张了些,但的确是干货!

回归原始,感悟中!

好文章!

文章很清晰,点赞

大神,你讲得这么透彻,让我娶了你吧~~~ ^_^

suffering 初步深入 Rack (一) 提及了此话题。 06月21日 18:28
suffering Rack 中间件简单理解及例子 提及了此话题。 09月09日 10:53

不防从 Rack 开始

错了一个字。

ToUpper.new(Hello.new.call(env)).call(env)

这个地方我自己试验了一下,我觉得是

ToUpper.new(Hello.new).call(env)
mr_zou123 Rack Middleware 的理解 提及了此话题。 03月16日 23:04
需要 登录 后方可回复, 如果你还没有账号请 注册新账号