Ruby China

Gem 为什么我们需要 Rack ?

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

一切从 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

附上之前写的另一篇入门吐嘈文:

这天你坐在电脑前,打开浏览器,并发送请求到 http://test.example.com/cons/1.

你的浏览器根据实际情况生成了一个完整的Request. 根据全球邮政管理体系TCP/IP协议,这个请求被打包交给包裹公司,查询过一个叫DNS server的路人后,路人表示 text.example.com 指向 ip 是个邮编是 111.111.111.111 的大楼地址,于是这个请求信息的包裹被交给快递公司,经历无数次的中转 (路由/代理), 终于到了 111.111.111.111 大楼。因为你没有指定端口号,默认指向 80 端口。表示自己要找 80 号房的家伙。

这个大楼简称 111 大楼,进楼时颇废周拆,一个叫防火墙的变态狂对包裹进行了全身检查,X 光照遍全身,以确认它安全健康,不含任何病毒,不带三聚氢铵等等危险成份。

111 大楼总接待回头看了看业务表,确实有 80 号客房,而且对外开放,于是说,准! 而后客户到了 80 号房,敲了敲门,这个时候 80 号出来一个叫 nginx 的家伙,经过仔细的盘问作了来访登记 (log), 实际它对一些它自己能处理的东西,像化妆品 stylesheet, 图片之类的东西,它就直接帮你包好了。

而后你的请求就进了 80 号房。80 号房按你写的邮编地址查询自身的来访规则,根据访问的前缀 test 表示有个家伙是专门处理你的请求的.(nginx virtual host) 它叫 passenger, 而后你的请求就交给了 passenger, passenger 转手扔给某个 app project 地址。

这个时候,这个叫 hello 的 rails 或者说 rack app 已经等得快哭了。 因为 hello 经常遭遇恐怖袭击,所以你的请求需要经常严格的审核。这审核过程相当之严谨,主要由一个个职业叫middleware的家伙来完成。很有趣的是这帮家伙长着一样的脸:它们都响应一个叫 call 的方法,这个方法接受一个叫 env 的对象。无论中间做了什么,到最后他们必须返回一个盒子,里面装着万年好基友三剑客,它们的站位相当严格,永远是status code , head, body的纵队。

它们有独特的品味:status code必须是整数在 100 到 599 之间。head是个穿着花裙子的hash, 里面放着各种奇怪的东西,除了几种常见的像Content-Type之类的东西,你时常会发现一些连大侦探 google 都查不出根脚的家伙。而且body必须响应一个叫each的方法。为什么 middleware 们的工作这样艰苦,限制这么多呢?因为有个叫Rack的家伙强制做了这样的要求,这家伙是法,是天,是规则,是道,贯穿着hello部门的始终。前面的家伙 (passenger/thin/mongrel/Webrick/Fast-cgi) 听它的,后面的家伙 (Rails/sinatra/Camping) 也听它的,如果 middleware 们不按它的要求来,就会被程序员们扯出肠子来打上传说中最最恶毒的猴子补丁以保证它们按 Rack 老大的规矩来办事. 这一个个的 middleware 排进一个叫 stack 的队伍中,严格按先进后出的原则努力工作。最后一个家伙大叫一声call(env)而后将自己丢给前一位的家伙,生成一个新的对象,而后再大叫一声call(env), 如此循环。

这些苦逼的家伙们有的负责检查你的请求文件里有没有炸弹 (听说最近很流行邮寄炸弹),有的负责去掉非法的请求 (很明显你若是请求这栋楼的钥匙或是其他客户的信息是不可以),有的则分析你请求里的地址,而后决定交给哪个部门来处理。有的则会根据你请求的是get/post/put/delete/head/options等等来交给全不同的部门。

总之,这个 111 大楼的 80 号房 passenger 分区的 hello 往下也分了无数个部门。简直是个俄罗斯套娃,一层包着一层无穷无尽。好在这个审请的过程不像有关部门的业务一样需要你亲自到场一个一个去打他们签字,包裹发出去就不用你干啥了。

业务继续,middleware 们对你的请求逐词逐句地分析,某此必须请求未写的,给你补上默认的,保你格式规范。写错的若是小错误帮你改改,若是大错,就驳回了请重新请求。

经过重重审核,你的请求文件已经变得他妈的都不认识他了。 终于,最后一道审核通过了。这个时候,80 号房 passenger 分区的 hello app 部门的各个负责回复的部门开始忙碌起来。它们一头扎进大楼的各地去给你找东西。比如说到楼下的楼下的楼下去一个叫数据库的地方去给你找来各条件的东西,带回来层层处理。这个过程甚至比请求进来时的层层安检处处审批都要复杂麻烦。比如说那个叫 ActiveRecord 的家伙在取回东西后要对它做各种包装,要去掉很多你不需要或者不方便看到的东西,封装成一件件货品。有时你表示你只需要一个罐头 (/show), 这种工作似乎相对容易,直接把罐头打包,而后丢给 view 车间,进入流水线,view 为它打上生产日期,过期时间,印上大幅的广告,而后给你就可以了。有时候你表示要一箱子,还要附带其客户的留言打分评价,那就是个相当繁琐的过程。

详细点说:不同部门的处理是不一样的。比如说,有的部门会要求你交出身份证 (authtication), 有的会看看你是不是会员,还要查看你的权限 (Role based priority. member?/customer?/admin?), 有的会检查下你付过钱没有 (payed?).

一般到了这里,就是称之为Logic的地方了。前面的都是外包的安全公司业务流程 (protocol, abstract logic), 到这里,就是到了 hello 的核心部门了。

hello有五花八门部门。其中,最出名的有四个部门,分别是router, model, view, controller 简称万人迷 F4 组合。后三个家伙更出名,它们取各自名字的首字母,简称MVC超级无敌人气组合,它们名声在外,超出了 Rails 社区,超出发 Ruby 大陆,走向了更高的领域,无数的程序猿是它的粉丝,日日为之不眠,它们猩红的眼里一会儿是 M, 一会是 C, 一会是 V, 来来回回,永无止境。大体而言,router 根据请求找到合适的 C, C 负责接客,M 负责提供干货并交给 C, (削除) C 再交给 M 加工包装 (削除ここまで), C 再交给 V 加工包装,而后再由 C 发给客户。

hello里有无数的临时工,临时工一般被称之为gem, 它们活跃在各种地方,做着最细致最精巧的活。但是也有不好的地方,它们拉帮结派,有时你只想雇佣其中一个家伙,结果却是得连他的亲朋好友加十代血亲的雇过来 (dependecies).有时候这些临时工会乱来,一个不好就会弄崩一条流水线。有时候是房子 (Rails/sinatra) 本身出了问题,也会造成它们的不适应并且造成很大的乱子,但是一般人都不会去怪房子本身,而会直接找它们的麻烦 (换掉或打上恶毒的猴子补丁). 好吧,这个时候它们是背黑锅的,有时候临时工协会等于背锅协会。

在你与 111 大楼的 hello 公司进行一次交易后,为了保持以后合作愉快,它们在背后做了许多工作。比如说建议长期的联系,什么cookie啊,session啊,cache之类的干了无数的活。有的让你提交请求时不用每次都要查身份证,验指纹,核对瞳孔; 有的记录下你的喜好方便下次直接拿出你喜欢的酒; 有的则与你的浏览器热情交流以保证你下次访问可以更快更好; 还有的直接与你的浏览器搭上线,建立不断线通话,不再单纯地玩问答游戏,主动推送内容 (asyn), 如此种种不胜枚举。

最后,东西终于到你手上了。但是实际上它们只是头文件字符串。这个时候,你亲切的管家 (浏览器) 开始工作了。它干的活比你想像中的还复杂,不过我已经无力吐嘈,看看这篇文章吧 [http://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/]

你表示,这次的请求相当愉快。这个网站的速度似乎还可以,由于你用的是 100M 光纤网络,一点页面闪都不闪一下就打开了。但是其实在后台,无数的协议,无数的进程,这后面为你服务的家伙,哪怕把名字列出来,都够你看上二十小时。

楼主关注贡献干货 20 年 👍

3 楼 已删除

赞,楼主讲解的非常好

必须点赞: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 , 呃,是呢,谢谢 😄 马上改过来。

#30 楼 @suffering 神速啊......😳

文章好长啊,写得不错。 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
需要 登录 后方可回复, 如果你还没有账号请 注册新账号
我爱爱我的人,我恨恨我的人。

Bindo
深圳



共收到 46 条回复

AltStyle によって変換されたページ (->オリジナル) /