Ruby Middleware

Rack官网对于Rack的介绍比较简单,只是介绍了Rack的作用和基本的使用。虽然我们不用了解middleware的调用原理也可以开发出能使用的middleware,但是总有点不知所以然的感觉,所以抽空总结了下Rack中middleware的调用原理。

装饰者模式

  • 首先理解下装饰者模式,装饰者模式中有装饰者和被装饰者,就像套娃,在外面套上一层,外面那层就是装饰者,里面那层就是被装饰者。可能这个比喻有点不太合理,因为装饰者模式其实就是在之前的功能上添加装饰多点功能上去,而套娃是全部盖住了。但是按照代码的表面上看调用方式确实是盖住了,所以暂时用这个比喻了。这两个概念要区分好,要不容易绕晕。在装饰者初始化时,被装饰者一般作为参数传递给装饰者,作为装饰者的成员。装饰者和被装饰者一般会有相同的行为,在装饰者的行为发生时会通过他的成员去调用被装饰者的行为,从而达到被装饰的目的。其实装饰者模式就是利用类有相似的行为这种方式,用装饰者去替代一下被装饰者,但是又不影响被装饰者的行为调用,同时在装饰者的行为发生时加些额外的功能。可以当作是被装饰者在外面加上了一层外壳,然后外壳发生变化的时候,会顺带着调用被装饰者的行为,Rack就是用到了这种模式。开始作为run方法调用的middleware就是最初的被装饰者,装饰者也有可能会成为被装饰者。而use方法调用的middleware就是接下来的装饰者了(也有可能作为被装饰者用)。对于上面要有相同的行为这点,其实感觉也不是很必要,统一定义成那个行为只是为了方便定义统一的接口模式,方便开发。理解这种模式对下面的调用思路比较有帮助。

  • 实现类似代码如下:

    class A
      def call
        puts "be decorator"
      end
    end
    
    class B
      attr_reader :a
    
      def initialize
        @a = A.new
      end
    
      def call
        puts "do something before"
        @a.call
        puts "do something after"
      end
    end
    
    B.new.call
    

    A为被装饰者,B为装饰着,在@a.call之前和之后的部分为装饰的内容。

Rack middleware的使用

  1. 配置config.ru文件,定义好要用到的middleware和要run的middleware。下面是一个简单的调用。
      # ./config.ru
      app = Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack\'d']] }
      run app
    

    接着执行rackup命令应用就可以跑起来了。

  2. 同时还可以把middleware定义成一个类,但是要在初始化实例的时候初始化@app和定义一个call方法,并且在call方法中需要调用@app.call(env),如下:

     # rack_demo.rb
     require 'rack'
    
     class Timing
       def initialize(app)
         @app = app
       end
    
       def call(env)
         ts = Time.now
         status, headers, body = @app.call(env)
         elapsed_time = Time.now - ts
         puts "Timing: #{env['REQUEST_METHOD']} #{env['REQUEST_URI']} #{elapsed_time.round(3)}"
         return [status, headers, body]
       end
     end
    
     app = proc do |env|
       ['200', {'Content-Type' => 'text/html'}, ['Hello, Rack!']]
     end
    
     Rack::Handler::WEBrick.run(Timing.new(app), :Port => 9292, :Host => '0.0.0.0')
    

    执行ruby rack_demo.rb就可以跑起一个服务了。

middleware的具体使用和返回格式要求这里不详细介绍,可以参考Ruby Rack 及其应用。同时上面的两种使用方式的作用原理是一样的。下面再详细分析。

Rack Middleware实现的原理

定义好config.ru配置文件后在当前目录执行rackup命令,会去到ruby对应的bin目录执行文件。一般在ruby安装好后都会有这个可执行文件的,在我本地的位置是 ~/.rvm/gems/ruby-2.5.3/bin/rackup

# ~/.rvm/gems/ruby-2.5.3/bin/rackup
require 'rubygems'

version = ">= 0.a"
...

if Gem.respond_to?(:activate_bin_path)
  load Gem.activate_bin_path('rack', 'rackup', version)
else
  gem "rack", version
  load Gem.bin_path("rack", "rackup", version)
end

上面源码会执行后面的rack gem下面的rackup二进制文件,其中的源码为:

#!/usr/bin/env ruby
# ~/.rvm/gems/ruby-2.5.3/gems/rack-2.0.6/bin/rackup
require "rack"
Rack::Server.start

主要是为了启动Rack Server,那启动的过程又做了什么东西呢? 调用栈从 def self.start => initialize => parse_options 这一系列的调用只是为了初始化一个Server,然后加上一些默认的Options配置,初始化后主要是加了如下的默认配置:

{
  :environment => "development",
  :pid         => nil,
  :Port        => 9292,
  :Host        => "localhost",
  :AccessLog   => [],
  :config      => "config.ru"
}

其中config中的值config.ru就是默认的配置文件,然后就是实例执行run方法了。run方法中调用了wrapped_app方法,这个方法主要是把那些我们自己定义的middleware和Rack自己提供的middleware合并成一个app对象。沿着方法调用栈继续查看,其中build_app方法比较重要。源码如下:

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) # 这一步把参数app当作被装饰者了
  end
  app
end

middleware[options[:environment]]求得的值是Rack中默认的middleware。遍历的块中每个middleware都会去创建一个实例,以app变量作为参数传入,这就是为什么每个middleware在initialize的时候都需要传入一个app变量,并初始化赋值给@app实例变量的原因。初始化的过程也就是被装饰者要被装饰的过程。上面的迭代遍历过程最后方法返回的app会变成如下的链式反应:

#<Rack::ContentLength:0x00007ff72d568200
 @app=
  #<Rack::Chunked:0x00007ff72d568ef8
   @app=
    #<Rack::CommonLogger:0x00007ff72d569ce0
     @app=
      #<Rack::ShowExceptions:0x00007ff72e940c28
       @app=
        #<Rack::Lint:0x00007ff72e941ee8
         @app=
          #<Rack::TempfileReaper:0x00007ff72e943018
           @app=
            #<StatusLogger:0x00007ff72d4a0570
             @app=
              #<StatusLoggear:0x00007ff72d4a1088
               @app=
                #<Proc:0x00007ff72d4a2820@/Users/Cain/code/ruby/rack/config.ru:30>>>>

这样通过调用app.call可以调用到所有middleware的call方法。 举个例子:

#config.ru
class FirstMidd
  def initialize(app)
    @app = app
  end

  def call(env)
    puts "1"
    status, head, body = @app.call(env)
    puts "7"
    [status, head, body]
  end
end

class SecondMidd
  def initialize(app)
    @app = app
  end

  def call(env)
    puts "2"
    status, head, body = @app.call(env)
    puts "6"
    [status, head, body]
  end
end

class ThirdMidd
  def initialize(app)
    @app = app
  end

  def call(env)
    puts "3"
    status, head, body = @app.call(env)
    puts "5"
    [status, head, body]
  end
end

class Top
  def call(env)
    puts "4"
    [200, {'Content-Type' => 'text/plain'}, ["hello, this is a test."]]
  end
end

use FirstMidd
use SecondMidd
use ThirdMidd
run Top.new

发送一个请求curl http://localhost:9292后,服务器会会按照 => 1, 2, 3, 4, 5, 6, 7 的顺序输出。Top作为最开始的被装饰者app,会先用ThirdMidd去装饰,返回app1,然后app1又作为被装饰者被SecondMidd装饰,直到FirstMidd,返回app3对象,这时候调用app2.call的时候,就会先调用FirstMidd的call方法,然后调用app3的call方法,然后调用直到app的call方法。 详细的调用过程如下:

  # ~/.rvm/gems/ruby-2.5.3/gems/rack-2.0.6/lib/rack/server.rb
  def build_app_and_options_from_config
    ...

    app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
    @options.merge!(options) { |key, old, new| old }
    app
  end

  # ~/.rvm/gems/ruby-2.5.3/gems/rack-2.0.6/lib/rack/builder.rb
  def self.parse_file(config, opts = Server::Options.new)
    options = {}
    if config =~ /\.ru$/
      ...
      app = new_from_string cfgfile, config
    else
      ...
    end
    return app, options
  end

  def self.new_from_string(builder_script, file="(rackup)")
    eval "Rack::Builder.new {\n" + builder_script + "\n}.to_app",
        TOPLEVEL_BINDING, file, 0
  end

最后会调用new_from_string这个方法。这个方法就是通过eval执行之前config.ru中的内容。其中Builder#to_app方法执行 app = @use.reverse.inject(app) { |a,e| e[a] }config.ru 中run方法和use方法调用的那些middleware实例逐个迭代作为参数嵌入到app变量中,app最终的效果如上面build_app方法中最后的app变量的形式一样。最终这个app值作为build_app方法的实参调用,最终合并成最终链条。

rack对middleware的处理使用了装饰者模式,不需要包含被装饰者的类就是初始的被装饰者,如上面例子的Top类和赋值proc给app变量的都是初始的被装饰者。逐级装饰嵌套后,最后会调用最外层的装饰者,调用call方法,这时会发生一系列的连锁反应,一直调用其它装饰者(也是被装饰者)的call方法,直到最初的那个被装饰者。这时候就会有内容返回了。

Rack和应用服务器的对接

上面的过程只是启动一个服务的时候准备的一些middlewa的过程。那应用服务器和middleware的连接桥梁是怎么实现的呢?其实Rack只是按照一定的规则去找出应用服务器,然后通过执行其中定义好的run方法,把上面链接好的迭代app传入服务器中去执行,从而达到中间桥梁的作用,主要的代码在Rack::Server#start的时候run了应用服务器。如下:

# ~/.rvm/gems/ruby-2.5.3/gems/rack-2.0.6/lib/rack/server.rb

def start
  ...
  server.run wrapped_app, options, &blk
end

其中server的调用过程如下:

# ~/.rvm/gems/ruby-2.5.3/gems/rack-2.0.6/lib/rack/server.rb

def server
  @_server ||= Rack::Handler.get(options[:server])

  unless @_server
    @_server = Rack::Handler.default

    # We already speak FastCGI
    @ignore_options = [:File, :Port] if @_server.to_s == 'Rack::Handler::FastCGI'
  end

  @_server
end

如果没有配置对应的:server选项,调用Rack::Handler.get会返回nil,然后调用Rack::Handler.default去查找对应的server。会通过pick ['puma', 'thin', 'webrick']按照默认服务器名字的顺序去查找对应的服务器handler。 如果require了puma,而puma因为定义了一个覆盖Handler中default的方法,如下:

# /Users/Cain/.rvm/gems/ruby-2.5.3/gems/puma-3.12.0/lib/puma/rack_default.rb
require 'rack/handler/puma'

module Rack::Handler
  def self.default(options = {})
    Rack::Handler::Puma
  end
end

这时 Rack::Handler.default 返回的就是 Rack::Handler::Puma 这个类。这时调用server.run wrapped_app, options, &blk就相当于调用了Rack::Handler::Puma.run,从而在puma中接到可以处理的请求后再执行app.call(env),就会出现一系列的链式调用。从而保证middleware都可以被调用到call方法,然后再次返回处理逻辑。

总结:Rack还做了很多的其它处理工作,把app和app server给串联起来只是其中的一部分。总的来说就是当一个请求过来时,可以通过一个个的middleware链式的调用,完成一些列的功能调用。从而细分了各个功能点,方便了开发人员更好的去扩展。

Ref: