谈谈 Rack 的协议与实现
2017-10-27 09:00
1336 查看
Rack 协议
Rack 的使用
中间件
Rack 的实现原理
rackup 命令
Server 的启动
初始化和配置
『包装』应用
中间件的实现
webserver 的选择
webserver 的启动
Rails 和 Rack
总结
相关文章
Reference
谈谈 Rack 协议与实现
浅谈 WEBrick 的多线程模型
浅谈 Thin 的事件驱动模型
浅谈 Unicorn 的多进程模型
浅谈 Puma 的并发模型与实现
Ruby Web 服务器的并发模型与性能
作为 Rails 开发者,基本上每天都与 Rails 的各种 API 以及数据库打交道,Rails 的世界虽然非常简洁,不过其内部的实现还是很复杂的,很多刚刚接触 Rails 的开发者可能都不知道 Rails 其实就是一个 Rack 应用,在这一系列的文章中,我们会分别介绍 Rack 以及一些常见的遵循 Rack 协议的 webserver 的实现原理。
不只是 Rails,几乎所有的 Ruby 的 Web 框架都是一个 Rack 的应用,除了 Web 框架之外,Rack 也支持相当多的 Web 服务器,可以说 Ruby 世界几乎一切与 Web 相关的服务都与 Rack 有关。
所以如果想要了解 Rails 或者其他 Web 服务底层的实现,那么一定需要了解 Rack 是如何成为应用容器(webserver)和应用框架之间的桥梁的,本文中介绍的是 2.0.3 版本的 rack。
在 Rack 的协议中,将 Rack 应用描述成一个可以响应
A Rack application is a Ruby object (not a class) that responds to call. It takes exactly one argument, the environment and returns an Array of exactly three values: The status, the headers, and the body.
Rack 在 webserver 和应用框架之间提供了一套最小的 API 接口,如果 webserver 都遵循 Rack 提供的这套规则,那么所有的框架都能通过协议任意地改变底层使用 webserver;所有的 webserver 只需要在
这个类方法接受两个参数,分别是一个 Rack 应用对象和一个包含各种参数的
最后的
我们在大致了解 Rack 协议之后,其实可以从一段非常简单的代码入手来了解 Rack 是如何启动 webserver 来处理来自用户的请求的,我们可以在任意目录下创建如下所示的 config.ru 文件:
因为
接下来,我们在同一目录使用
从命令的输出我们可以看到,使用 rackup 运行了一个 WEBrick 的进程,监听了 9292 端口,如果我们使用 curl 来访问对应的请求,就可以得到在 config.ru 文件中出现的
在这篇文章中,作者都会使用开源的工具 httpie 代替 curl 在命令行中发出 HTTP 请求,相比 curl 而言 httpie 能够提供与 HTTP 响应有关的更多信息。
从上述请求返回的 HTTP 响应头中的信息,我们可以看到 WEBrick 确实按照 config.ru 文件中的代码对当前的 HTTP 请求进行了处理。
Rack 协议和中间件是 Rack 能达到今天地位不可或缺的两个功能或者说特性,Rack 协议规定了 webserver 和 Rack 应用之间应该如何通信,而 Rack 中间件能够在上层改变 HTTP 的响应或者请求,在不改变应用的基础上为 Rack 应用增加新的功能。
Rack 的中间件是一个实现了两个方法
在所有的
如果需要使用某一个 Rack 中间件只需要在当前文件中使用
除了直接通过
上述中间件会在每次调用时都将 Rack 应用返回的
当我们再次使用 http 命令请求相同的 URL 时,就会获得不同的结果,同时由于我们保留了
Rack 的中间件的使用其实非常简单,我们只需要定义符合要求的类,然后在合适的方法中返回合适的结果就可以了,在接下来的部分我们将介绍 Rack 以及中间件的实现原理。
到这里,我们已经对 Rack 的使用有一些基本的了解了,包括如何使用
那么
从上述文件中的注释中可以看到当前文件是由 RubyGems 自动生成的,在文件的最后由一个
首先,通过
在最后打印了该文件的内容,到这里我们就应该知道
从这里开始,我们就已经从 rackup 命令的执行进入了 rack 的源代码,可以直接使用 pry 找到
在
在这个
上述两个方法中处理了一些对象本身定义的一些参数,比如默认的端口号 9292 以及默认的 config 文件,config 文件也就是
访问相同的 URL 能得到完全一致的结果,在这里就不再次展示了,有兴趣的读者可以亲自尝试一下。
当我们执行了
我们已经从上述方法中删除了很多对于本文来说不重要的代码实现,所以上述方法中最重要的部分就是
上述方法有两部分组成,分别是
整个方法在最终会执行
在
我们先暂时不管这段代码是如何执行的,我们只需要知道上述代码存储了所有的中间件以及 Proc 对象,最后通过
在这之后会使用
经过上述方法,我们在一个 Rack 应用周围一层一层包装上了所有的中间件,最后调用的中间件在整个调用栈中的最外层,当包装后的应用接受来自外界的请求时,会按照如下的方式进行调用:
所有的请求都会先经过中间件,每一个中间件都会在
在 Rack 中,中间件是由两部分的代码共同处理的,分别是
在这时,config.ru 文件中的代码就会在当前实例的环境下执行,文件中的
上述方法会将传入的参数组合成一个接受
它只是将传入的
上述方法将所有传入
上一小节提到的
在这里的
在
如果我们在运行
在这个方法中,调用
一部分常量是预先定义在 handler.rb 文件中的,另一部分是由各个 webserver 的开发者自己定义或者遵循一定的命名规范加载的:
在默认的情况下,如果不在启动服务时指定服务器就会按照 puma、thin 和 webrick 的顺序依次尝试加载响应的处理器。
当 Rack 已经使用中间件对应用进行包装并且选择了对应的 webserver 之后,我们就可以将处理好的应用作为参数传入
所有遵循 Rack 协议的 webserver 都会实现上述
在了解了 Rack 的实现之后,其实我们可以发现 Rails 应用就是一堆 Rack 中间件和一个 Rack 应用的集合,在任意的工程中我们执行
在这里包含了很多使用
除此之外,
Rack 协议可以说占领了整个 Ruby 服务端的市场,无论是常见的服务器还是框架都遵循 Rack 协议进行了设计,而正因为 Rack 以及 Rack 协议的存在我们在使用 Rails 或者 Sinatra 开发 Web 应用时才可以对底层使用的 webserver 进行无缝的替换,在接下来的文章中会逐一介绍不同的 webserver 是如何对 HTTP 请求进行处理以及它们拥有怎样的 I/O 模型。
谈谈 Rack 协议与实现
浅谈 WEBrick 的多线程模型
浅谈 Thin 的事件驱动模型
浅谈 Unicorn 的多进程模型
浅谈 Puma 的并发模型与实现
Ruby Web 服务器的并发模型与性能
Rack · A modular Ruby webserver interface
Rack: a Ruby Webserver Interface
Rack interface specification
Rails on Rack
Rack Middleware
Introducing Rack
Ruby on Rails Server options
本作品采用知识共享署名 4.0 国际许可协议进行许可。 转载时请注明原文链接,图片在使用时请保留图片中的全部内容,可适当缩放并在引用处附上图片所在的文章链接,图片使用 Sketch 进行绘制。
Rack 的使用
中间件
Rack 的实现原理
rackup 命令
Server 的启动
初始化和配置
『包装』应用
中间件的实现
webserver 的选择
webserver 的启动
Rails 和 Rack
总结
相关文章
Reference
谈谈 Rack 协议与实现
浅谈 WEBrick 的多线程模型
浅谈 Thin 的事件驱动模型
浅谈 Unicorn 的多进程模型
浅谈 Puma 的并发模型与实现
Ruby Web 服务器的并发模型与性能
作为 Rails 开发者,基本上每天都与 Rails 的各种 API 以及数据库打交道,Rails 的世界虽然非常简洁,不过其内部的实现还是很复杂的,很多刚刚接触 Rails 的开发者可能都不知道 Rails 其实就是一个 Rack 应用,在这一系列的文章中,我们会分别介绍 Rack 以及一些常见的遵循 Rack 协议的 webserver 的实现原理。
不只是 Rails,几乎所有的 Ruby 的 Web 框架都是一个 Rack 的应用,除了 Web 框架之外,Rack 也支持相当多的 Web 服务器,可以说 Ruby 世界几乎一切与 Web 相关的服务都与 Rack 有关。
所以如果想要了解 Rails 或者其他 Web 服务底层的实现,那么一定需要了解 Rack 是如何成为应用容器(webserver)和应用框架之间的桥梁的,本文中介绍的是 2.0.3 版本的 rack。
Rack 协议
在 Rack 的协议中,将 Rack 应用描述成一个可以响应 call方法的 Ruby 对象,它仅接受来自外界的一个参数,也就是环境,然后返回一个只包含三个值的数组,按照顺序分别是状态码、HTTP Headers 以及响应请求的正文。
A Rack application is a Ruby object (not a class) that responds to call. It takes exactly one argument, the environment and returns an Array of exactly three values: The status, the headers, and the body.
Rack 在 webserver 和应用框架之间提供了一套最小的 API 接口,如果 webserver 都遵循 Rack 提供的这套规则,那么所有的框架都能通过协议任意地改变底层使用 webserver;所有的 webserver 只需要在
Rack::Handler的模块中创建一个实现了
.run方法的类就可以了:
module Rack module Handler class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet def self.run(app, options={}) # .. end end end end
这个类方法接受两个参数,分别是一个 Rack 应用对象和一个包含各种参数的
options字典,其中可能包括自定义的 ip 地址和端口号以及各种配置,根据 Rack 协议,所有应用对象在接受到一个
#call方法并且传入
env时,都会返回一个三元组:
最后的
body响应体其实是一个由多个响应内容组成的数组,Rack 使用的 webserver 会将
body中几个部分的连接到一起最后拼接成一个 HTTP 响应后返回。
Rack 的使用
我们在大致了解 Rack 协议之后,其实可以从一段非常简单的代码入手来了解 Rack 是如何启动 webserver 来处理来自用户的请求的,我们可以在任意目录下创建如下所示的 config.ru 文件:# config.ru run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack\'d']] }
因为
Proc对象也能够响应
#call方法,所以上述的 Proc 对象也可以看做是一个 Rack 应用。
接下来,我们在同一目录使用
rackup命令在命令行中启动一个 webserver 进程:
$ rackup config.ru [2017-10-26 22:59:26] INFO WEBrick 1.3.1 [2017-10-26 22:59:26] INFO ruby 2.3.3 (2016-11-21) [x86_64-darwin16] [2017-10-26 22:59:26] INFO WEBrick::HTTPServer#start: pid=83546 port=9292
从命令的输出我们可以看到,使用 rackup 运行了一个 WEBrick 的进程,监听了 9292 端口,如果我们使用 curl 来访问对应的请求,就可以得到在 config.ru 文件中出现的
'get rack\'d'文本:
在这篇文章中,作者都会使用开源的工具 httpie 代替 curl 在命令行中发出 HTTP 请求,相比 curl 而言 httpie 能够提供与 HTTP 响应有关的更多信息。
$ http http://localhost:9292 HTTP/1.1 200 OK Connection: Keep-Alive Content-Type: text/html Date: Thu, 26 Oct 2017 15:07:47 GMT Server: WEBrick/1.3.1 (Ruby/2.3.3/2016-11-21) Transfer-Encoding: chunked get rack'd
从上述请求返回的 HTTP 响应头中的信息,我们可以看到 WEBrick 确实按照 config.ru 文件中的代码对当前的 HTTP 请求进行了处理。
中间件
Rack 协议和中间件是 Rack 能达到今天地位不可或缺的两个功能或者说特性,Rack 协议规定了 webserver 和 Rack 应用之间应该如何通信,而 Rack 中间件能够在上层改变 HTTP 的响应或者请求,在不改变应用的基础上为 Rack 应用增加新的功能。Rack 的中间件是一个实现了两个方法
.initialize和
#call的类,初始化方法会接受两个参数,分别是
app和
options字典,而
#call方法接受一个参数也就是 HTTP 请求的环境参数
env,在这里我们创建了一个新的 Rack 中间件
StatusLogger:
class StatusLogger def initialize(app, options={}) @app = app end def call(env) status, headers, body = @app.call(env) puts status [status, headers, body] end end
在所有的
#call方法中都应该调用
app.call让应用对 HTTP 请求进行处理并在方法结束时将所有的参数按照顺序返回。
use StatusLogger run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack\'d']] }
如果需要使用某一个 Rack 中间件只需要在当前文件中使用
use方法,在每次接收到来自用户的 HTTP 请求时都会打印出当前响应的状态码。
$ rackup [2017-10-27 19:46:40] INFO WEBrick 1.3.1 [2017-10-27 19:46:40] INFO ruby 2.3.3 (2016-11-21) [x86_64-darwin16] [2017-10-27 19:46:40] INFO WEBrick::HTTPServer#start: pid=5274 port=9292 200 127.0.0.1 - - [27/Oct/2017:19:46:53 +0800] "GET / HTTP/1.1" 200 - 0.0004
除了直接通过
use方法直接传入
StatusLogger中间件之外,我们也可以在
use中传入配置参数,所有的配置都会通过
options最终初始化一个中间件的实例,比如,我们有以下的中间件
BodyTransformer:
class BodyTransformer def initialize(app, options={}) @app = app @count = options[:count] end def call(env) status, headers, body = @app.call(env) body = body.map { |str| str[0...@count].upcase + str[@count..-1] } [status, headers, body] end end
上述中间件会在每次调用时都将 Rack 应用返回的
body中前
count个字符变成大写的,我们可以在 config.ru 中添加一个新的中间件:
use StatusLogger use BodyTransformer, count: 3 run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack\'d']] }
当我们再次使用 http 命令请求相同的 URL 时,就会获得不同的结果,同时由于我们保留了
StatusLogger,所以在 console 中也会打印出当前响应的状态码:
# session 1 $ rackup [2017-10-27 21:04:05] INFO WEBrick 1.3.1 [2017-10-27 21:04:05] INFO ruby 2.3.3 (2016-11-21) [x86_64-darwin16] [2017-10-27 21:04:05] INFO WEBrick::HTTPServer#start: pid=7524 port=9292 200 127.0.0.1 - - [27/Oct/2017:21:04:19 +0800] "GET / HTTP/1.1" 200 - 0.0005 # session 2 $ http http://localhost:9292 HTTP/1.1 200 OK Connection: Keep-Alive Content-Type: text/html Date: Fri, 27 Oct 2017 13:04:19 GMT Server: WEBrick/1.3.1 (Ruby/2.3.3/2016-11-21) Transfer-Encoding: chunked GET rack'd
Rack 的中间件的使用其实非常简单,我们只需要定义符合要求的类,然后在合适的方法中返回合适的结果就可以了,在接下来的部分我们将介绍 Rack 以及中间件的实现原理。
Rack 的实现原理
到这里,我们已经对 Rack 的使用有一些基本的了解了,包括如何使用 rackup命令启动一个 webserver,也包括 Rack 的中间件如何使用,接下来我们就准备开始对 Rack 是如何实现上述功能进行分析了。
rackup 命令
那么 rackup到底是如何工作的呢,首先我们通过
which命令来查找当前
rackup的执行路径并打印出该文件的全部内容:
$ which rackup /Users/draveness/.rvm/gems/ruby-2.3.3/bin/rackup $ cat /Users/draveness/.rvm/gems/ruby-2.3.3/bin/rackup #!/usr/bin/env ruby_executable_hooks # # This file was generated by RubyGems. # # The application 'rack' is installed as part of a gem, and # this file is here to facilitate running it. # require 'rubygems' version = ">= 0.a" if ARGV.first str = ARGV.first str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then version = $1 ARGV.shift end end load Gem.activate_bin_path('rack', 'rackup', version)
从上述文件中的注释中可以看到当前文件是由 RubyGems 自动生成的,在文件的最后由一个
load方法加载了某一个文件中的代码,我们可以在 pry 中尝试运行一下这个命令。
首先,通过
gem list命令得到当前机器中所有 rack 的版本,然后进入 pry 执行
.activate_bin_path命令:
$ gem list "^rack$" *** LOCAL GEMS *** rack (2.0.3, 2.0.1, 1.6.8, 1.2.3) $ pry [1] pry(main)> Gem.activate_bin_path('rack', 'rackup', '2.0.3') => "/Users/draveness/.rvm/gems/ruby-2.3.3/gems/rack-2.0.3/bin/rackup" $ cat /Users/draveness/.rvm/gems/ruby-2.3.3/gems/rack-2.0.3/bin/rackup #!/usr/bin/env ruby require "rack" Rack::Server.start
rackup命令定义在 rack 工程的 bin/rackup 文件中,在通过 rubygems 安装后会生成另一个加载该文件的可执行文建。
在最后打印了该文件的内容,到这里我们就应该知道
.activate_bin_path方法会查找对应 gem 当前生效的版本,并返回文件的路径;在这个可执行文件中,上述代码只是简单的
require了一下 rack 方法,之后运行
.start启动了一个
Rack::Server。
Server 的启动
从这里开始,我们就已经从 rackup 命令的执行进入了 rack 的源代码,可以直接使用 pry 找到 .start方法所在的文件,从方法中可以看到当前类方法初始化了一个新的实例后,在新的对象上执行了
#start方法:
$ pry [1] pry(main)> require 'rack' => true [2] pry(main)> $ Rack::Server.start From: lib/rack/server.rb @ line 147: Owner: #<Class:Rack::Server> def self.start(options = nil) new(options).start end
初始化和配置
在 Rack::Server启动的过程中初始化了一个新的对象,初始化的过程中其实也包含了整个服务器的配置过程:
From: lib/rack/server.rb @ line 185: Owner: #<Class:Rack::Server> def initialize(options = nil) @ignore_options = [] if options @use_default_options = false @options = options @app = options[:app] if options[:app] else argv = defined?(SPEC_ARGV) ? SPEC_ARGV : ARGV @use_default_options = true @options = parse_options(argv) end end
在这个
Server对象的初始化器中,虽然可以通过
options从外界传入参数,但是当前类中仍然存在这个
#options和
#default_options两个实例方法:
From: lib/rack/server.rb @ line 199: Owner: Rack::Server def options merged_options = @use_default_options ? default_options.merge(@options) : @options merged_options.reject { |k, v| @ignore_options.include?(k) } end From: lib/rack/server.rb @ line 204: Owner: Rack::Server def default_options environment = ENV['RACK_ENV'] || 'development' default_host = environment == 'development' ? 'localhost' : '0.0.0.0' { :environment => environment, :pid => nil, :Port => 9292, :Host => default_host, :AccessLog => [], :config => "config.ru" } end
上述两个方法中处理了一些对象本身定义的一些参数,比如默认的端口号 9292 以及默认的 config 文件,config 文件也就是
rackup命令接受的一个文件参数,文件中的内容就是用来配置一个 Rack 服务器的代码,在默认情况下为 config.ru,也就是如果文件名是 config.ru,我们不需要向
rackup命令传任何参数,它会自动找当前目录的该文件:
$ rackup [2017-10-27 09:00:34] INFO WEBrick 1.3.1 [2017-10-27 09:00:34] INFO ruby 2.3.3 (2016-11-21) [x86_64-darwin16] [2017-10-27 09:00:34] INFO WEBrick::HTTPServer#start: pid=96302 port=9292
访问相同的 URL 能得到完全一致的结果,在这里就不再次展示了,有兴趣的读者可以亲自尝试一下。
『包装』应用
当我们执行了 .initialize方法初始化了一个新的实例之后,接下来就会进入
#start实例方法尝试启动一个 webserver 处理 config.ru 中定义的应用了:
From: lib/rack/server.rb @ line 258: Owner: Rack::Server def start &blk # ... wrapped_app # .. server.run wrapped_app, options, &blk end
我们已经从上述方法中删除了很多对于本文来说不重要的代码实现,所以上述方法中最重要的部分就是
#wrapped_app方法,以及另一个
#server方法,首先来看
#wrapped_app方法的实现。
From: lib/rack/server.rb @ line 353: Owner: Rack::Server def wrapped_app @wrapped_app ||= build_app app end
上述方法有两部分组成,分别是
#app和
#build_app两个实例方法,其中
#app方法的调用栈比较复杂:
整个方法在最终会执行
Builder.new_from_string通过 Ruby 中元编程中经常使用的
eval方法,将输入文件中的全部内容与两端字符串拼接起来,并直接执行这段代码:
From: lib/rack/builder.rb @ line 48: Owner: Rack::Builder def self.new_from_string(builder_script, file="(rackup)") eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app", TOPLEVEL_BINDING, file, 0 end
在
eval方法中执行代码的作用其实就是如下所示的:
Rack::Builder.new {
use StatusLogger use BodyTransformer, count: 3 run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack\'d']] }}.to_app
我们先暂时不管这段代码是如何执行的,我们只需要知道上述代码存储了所有的中间件以及 Proc 对象,最后通过
#to_app方法返回一个 Rack 应用。
在这之后会使用
#build_app方法将所有的中间件都包括在 Rack 应用周围,因为所有的中间件也都是一个响应
#call方法,返回三元组的对象,其实也就是一个遵循协议的 App,唯一的区别就是中间件中会调用初始化时传入的 Rack App:
From: lib/rack/server.rb @ line 343: Owner: Rack::Server def build_app(app) middleware[options[:environment]].reverse_each do |middleware| middleware = middleware.call(self) if middleware.respond_to?(:call) next unless middleware klass, *args = middleware app = klass.new(app, *args) end app end
经过上述方法,我们在一个 Rack 应用周围一层一层包装上了所有的中间件,最后调用的中间件在整个调用栈中的最外层,当包装后的应用接受来自外界的请求时,会按照如下的方式进行调用:
所有的请求都会先经过中间件,每一个中间件都会在
#call方法内部调用另一个中间件或者应用,在接收到应用的返回之后会分别对响应进行处理最后由最先定义的中间件返回。
中间件的实现
在 Rack 中,中间件是由两部分的代码共同处理的,分别是 Rack::Builder和
Rack::Server两个类,前者包含所有的能够在 config.ru 文件中使用的 DSL 方法,当我们使用
eval执行 config.ru 文件中的代码时,会先初始化一个
Builder的实例,然后执行
instance_eval运行代码块中的所有内容:
From: lib/rack/builder.rb @ line 53: Owner: Rack::Builder def initialize(default_app = nil, &block) @use, @map, @run, @warmup = [], nil, default_app, nil instance_eval(&block) if block_given? end
在这时,config.ru 文件中的代码就会在当前实例的环境下执行,文件中的
#use和
#run方法在调用时就会执行
Builder的实例方法,我们可以先看一下
#use方法是如何实现的:
From: lib/rack/builder.rb @ line 81: Owner: Rack::Builder def use(middleware, *args, &block) @use << proc { |app| middleware.new(app, *args, &block) } end
上述方法会将传入的参数组合成一个接受
app作为入参的
Proc对象,然后加入到
@use数组中存储起来,在这里并没有发生任何其他的事情,另一个
#run方法的实现其实就更简单了:
From: lib/rack/builder.rb @ line 103: Owner: Rack::Builder def run(app) @run = app end
它只是将传入的
app对象存储到持有的
@run实例变量中,如果我们想要获取当前的
Builder生成的应用,只需要通过
#to_app方法:
From: lib/rack/builder.rb @ line 144: Owner: Rack::Builder def to_app fail "missing run or map statement" unless @run @use.reverse.inject(@run) { |a,e| e[a] } end
上述方法将所有传入
#use和
#run命令的应用和中间件进行了组合,通过
#inject方法达到了如下所示的效果:
# config.ru use MiddleWare1 use MiddleWare2 run RackApp # equals to MiddleWare1.new(MiddleWare2.new(RackApp)))
Builder类其实简单来看就做了这件事情,将一种非常难以阅读的代码,变成比较清晰可读的 DSL,最终返回了一个中间件(也可以说是应用)对象,虽然在
Builder中也包含其他的 DSL 语法元素,但是在这里都没有介绍。
上一小节提到的
#build_app方法其实也只是根据当前的环境选择合适的中间件继续包裹到这个链式的调用中:
From: lib/rack/server.rb @ line 343: Owner: Rack::Server def build_app(app) middleware[options[:environment]].reverse_each do |middleware| middleware = middleware.call(self) if middleware.respond_to?(:call) next unless middleware klass, *args = middleware app = klass.new(app, *args) end app end
在这里的
#middleware方法可以被子类覆写,如果不覆写该方法会根据环境的不同选择不同的中间件数组包裹当前的应用:
From: lib/rack/server.rb @ line 229: Owner: #<Class:Rack::Server> def default_middleware_by_environment m = Hash.new {|h,k| h[k] = []} m["deployment"] = [ [Rack::ContentLength], [Rack::Chunked], logging_middleware, [Rack::TempfileReaper] ] m["development"] = [ [Rack::ContentLength], [Rack::Chunked], logging_middleware, [Rack::ShowExceptions], [Rack::Lint], [Rack::TempfileReaper] ] m end
.default_middleware_by_environment中就包含了不同环境下应该使用的中间件,
#build_app会视情况选择中间件加载。
webserver 的选择
在 Server#start方法中,我们已经通过
#wrapped_app方法将应用和中间件打包到了一起,然后分别执行
#server和
Server#run方法选择并运行 webserver,先来看 webserver 是如何选择的:
From: lib/rack/server.rb @ line 300: Owner: Rack::Server def server @_server ||= Rack::Handler.get(options[:server]) unless @_server @_server = Rack::Handler.default end @_server end
如果我们在运行
rackup命令时传入了
server选项,例如
rackup -s WEBrick,就会直接使用传入的 webserver,否则就会使用默认的 Rack 处理器:
From: lib/rack/handler.rb @ line 46: Owner: #<Class:Rack::Handler> def self.default # Guess. if ENV.include?("PHP_FCGI_CHILDREN") Rack::Handler::FastCGI elsif ENV.include?(REQUEST_METHOD) Rack::Handler::CGI elsif ENV.include?("RACK_HANDLER") self.get(ENV["RACK_HANDLER"]) else pick ['puma', 'thin', 'webrick'] end end
在这个方法中,调用
.pick其实最终也会落到
.get方法上,在
.pick中我们通过遍历传入的数组尝试对其进行加载:
From: lib/rack/handler.rb @ line 34: Owner: #<Class:Rack::Handler> def self.pick(server_names) server_names = Array(server_names) server_names.each do |server_name| begin return get(server_name.to_s) rescue LoadError, NameError end end raise LoadError, "Couldn't find handler for: #{server_names.join(', ')}." end
.get方法是用于加载 webserver 对应处理器的方法,方法中会通过一定的命名规范从对应的文件目录下加载相应的常量:
From: lib/rack/handler.rb @ line 11: Owner: #<Class:Rack::Handler> def self.get(server) return unless server server = server.to_s unless @handlers.include? server load_error = try_require('rack/handler', server) end if klass = @handlers[server] klass.split("::").inject(Object) { |o, x| o.const_get(x) } else const_get(server, false) end rescue NameError => name_error raise load_error || name_error end
一部分常量是预先定义在 handler.rb 文件中的,另一部分是由各个 webserver 的开发者自己定义或者遵循一定的命名规范加载的:
register 'cgi', 'Rack::Handler::CGI' register 'fastcgi', 'Rack::Handler::FastCGI' register 'webrick', 'Rack::Handler::WEBrick' register 'lsws', 'Rack::Handler::LSWS' register 'scgi', 'Rack::Handler::SCGI' register 'thin', 'Rack::Handler::Thin'
在默认的情况下,如果不在启动服务时指定服务器就会按照 puma、thin 和 webrick 的顺序依次尝试加载响应的处理器。
webserver 的启动
当 Rack 已经使用中间件对应用进行包装并且选择了对应的 webserver 之后,我们就可以将处理好的应用作为参数传入 WEBrick.run方法了:
module Rack module Handler class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet def self.run(app, options={}) environment = ENV['RACK_ENV'] || 'development' default_host = environment == 'development' ? 'localhost' : nil options[:BindAddress] = options.delete(:Host) || default_host options[:Port] ||= 8080 @server = ::WEBrick::HTTPServer.new(options) @server.mount "/", Rack::Handler::WEBrick, app yield @server if block_given? @server.start end end end end
所有遵循 Rack 协议的 webserver 都会实现上述
.run方法接受
app、
options和一个 block 作为参数运行一个进程来处理所有的来自用户的 HTTP 请求,在这里就是每个 webserver 自己需要解决的了,它其实并不属于 Rack 负责的部门,但是 Rack 实现了一些常见 webserver 的 handler,比如 CGI、Thin 和 WEBrick 等等,这些 handler 的实现原理都不会包含在这篇文章中。
Rails 和 Rack
在了解了 Rack 的实现之后,其实我们可以发现 Rails 应用就是一堆 Rack 中间件和一个 Rack 应用的集合,在任意的工程中我们执行 rake middleware的命令都可以得到以下的输出:
$ rake middleware use Rack::Sendfile use ActionDispatch::Static use ActionDispatch::Executor use ActiveSupport::Cache::Strategy::LocalCache::Middleware use Rack::Runtime use ActionDispatch::RequestId use ActionDispatch::RemoteIp use Rails::Rack::Logger use ActionDispatch::ShowExceptions use ActionDispatch::DebugExceptions use ActionDispatch::Reloader use ActionDispatch::Callbacks use ActiveRecord::Migration::CheckPending use Rack::Head use Rack::ConditionalGet use Rack::ETag run ApplicationName::Application.routes
在这里包含了很多使用
use加载的 Rack 中间件,当然在最后也包含一个 Rack 应用,也就是
ApplicationName::Application.routes,这个对象其实是一个
RouteSet实例,也就是说在 Rails 中所有的请求在经过中间件之后都会先有一个路由表来处理,路由会根据一定的规则将请求交给其他控制器处理:
除此之外,
rake middleware命令的输出也告诉我们 Rack 其实为我们提供了很多非常方便的中间件比如
Rack::Sendfile等可以减少我们在开发一个 webserver 时需要处理的事情。
总结
Rack 协议可以说占领了整个 Ruby 服务端的市场,无论是常见的服务器还是框架都遵循 Rack 协议进行了设计,而正因为 Rack 以及 Rack 协议的存在我们在使用 Rails 或者 Sinatra 开发 Web 应用时才可以对底层使用的 webserver 进行无缝的替换,在接下来的文章中会逐一介绍不同的 webserver 是如何对 HTTP 请求进行处理以及它们拥有怎样的 I/O 模型。
相关文章
谈谈 Rack 协议与实现浅谈 WEBrick 的多线程模型
浅谈 Thin 的事件驱动模型
浅谈 Unicorn 的多进程模型
浅谈 Puma 的并发模型与实现
Ruby Web 服务器的并发模型与性能
Reference
Rack · A modular Ruby webserver interfaceRack: a Ruby Webserver Interface
Rack interface specification
Rails on Rack
Rack Middleware
Introducing Rack
Ruby on Rails Server options
关于图片和转载
本作品采用知识共享署名 4.0 国际许可协议进行许可。 转载时请注明原文链接,图片在使用时请保留图片中的全部内容,可适当缩放并在引用处附上图片所在的文章链接,图片使用 Sketch 进行绘制。
关于评论和留言
如果对本文 谈谈 Rack 的协议与实现相关文章推荐
- 基于AMQP协议实现的qpid 学习笔记(基础概念)
- 稳扎稳打Silverlight(55) - 4.0通信之对UDP协议的支持: 通过 UdpSingleSourceMulticastClient 实现 SSM(Source Specific Multicast),即“源特定多播”
- apache中使用mod_gnutls模块实现多个SSL站点配置(多个HTTPS协议的虚拟主机)
- Jmeter实现WebSocket协议的接口和性能测试方法
- 稳扎稳打Silverlight(55) - 4.0通信之对UDP协议的支持: 通过 UdpSingleSourceMulticastClient 实现 SSM(Source Specific Multicast),即“源特定多播”
- 如何采用MQTT协议实现android消息推送
- 稳扎稳打Silverlight(55) - 4.0通信之对UDP协议的支持: 通过 UdpSingleSourceMulticastClient 实现 SSM(Source Specific Multicast),即“源特定多播”
- 如何在ns2中实现一个简单的网络协议
- JAVA实现与Linux通信(通过SSH协议)
- 谈谈游戏产品微网站可实现的类型
- 什么是Mixin模式:带实现的协议
- 基于java实现tcp长链接,自定义消息协议
- OpenWrt安装迅雷成功,实现BT emule等多协议下载
- linux中ip tunnel的实现及协议简介
- java实现Http协议
- 利用socket自己实现基于HTTP协议的Web客户端
- 谈谈 INotifyPropertyChanged 的实现
- linux shell 脚本实现tcp/upd协议通讯(重定向应用)
- C#实现http协议GET、POST请求
- 基于HTTP协议之WEB消息实时推送技术原理及实现