Rack 学习笔记

Posted on December 21, 2013

一. 简介

  • Rack是Ruby应用服务器(Thin WeBrick …)和Rack应用程序(Rails Sinatra…)之间的一个接口

  • Rack通过一种叫做句柄(handler)的机制实现对应用服务器的支持

    所有句柄位于命名空间位于Rack::Handler下,如Rack::Handler::WEBrick

    查看所有句柄Rack::Handler.constants

  • 几乎所有的主流Web框架都支持Rack接口, 这些框架都包含一个Rack适配器(adapter)

  • Rack利用中间件实现了最大程度的模块化,

    不同的Web框架之间可以重用中间件

    可以通过不同的中间件组合组装出同一个Web框架的不同变种

  • rack也是一个gem,安装rack[sudo] gem install rack

    在irb引入: require 'rubygems'; require 'rubygems'

    每个Handler都有一个run方法来运行Rake服务器,如Rack::Handler:: Mongrel.run(rack_app, 参数hash)

  • 一个Rack程序需要符合什么条件:

    能够响应call的ruby对象(lambda Proc method对象 任何包含call方法的对象)

    返回一个有三个成员的数组:

    • 一个状态(status),即http协议定义的状态码

      它不一定必须是整数,但是它必须能够响应to_i方法并返回一个整数,这个整数必须大于等于100

    • 一个头(headers),它可能是一个hash,其中包含所有的http头

      必须能够响应each方法,并且每次产生一个key和一个value

      关键字(key)也有明确的规定,所有的关键字必须是字符串(所以不可以是symbol类型)。value也必须是字符串,可以包括多行。

    • 一个体(body),它可能是一个字符串数组

      必须能够响应each方法,而且每次必须产生一个字符串

      irb> rack_app = lambda{|env| [200,{},["hello from lambda"]]}
      irb> Rack::Handler::WEBrick.run rack_app ,:Port=>3000
      

    可以在http://localhost:3000获得响应


二. Rack初探

  • Rack调用rack_app时会以hash参数的形式传入很多环境变量, rack_app可以对其捕获:

    • Rack相关变量, 这些变量都是rack.xxxx的形式

    • CGI头, 几个非常重要的key:

      REQUEST_METHOD 这是HTTP请求的方法

      PATH_INFO 路径

      QUERY_STRING 查询串

  • Rack::Request

    创建一个Request对象request = Rack::Request.new(env)

    读取请求参数reqest.params[somekey]

    了询问当前HTTP请求类型的简便方法: request_method get? post?等等

  • Rack::Response

    • 响应体: Request提供了两种方法来生成响应体:

      直接设置response.body。此时你必须自己设置响应头中Content-Length的值

      用response.write增量写入内容,自动填充Content-Length的值

      最后用response.finish完成,nish将装配出符合Rack规范的一个数组–这个数组有三个成员:状态码、响应头和响应体

      #直接设置response.body
      response.body = [body_string]
      response.headers['Content-Length'] = body.bytesize.to_s #必须是字符串
      response.finish
      
      #用response.write增量写入内容
      response.write("some body string")
      response.finish
      
    • 状态码

      状态码默认是200,可以设置response.status = 200

      重定向Response#redirect(target, status=302)

    • 响应头

      设置响应头 response.headers['Content-Type'] = 'text/plain'


三. 中间件

  • 中间件就是在Ruby应用服务器和Rack应用程序之间执行的代码

  • 中间件也是一个rack_app,并能接受并(递归)调用其他rak_app, 非常像设计模式中的装饰者模式

  • response.finish 会把自己(Response对象)作为body返回,他可以响应each方法

  • 自动设置content length的中间件Rack::ContentLength

  • 使用ruby DSL实现的中间件装配例子

      class Builder
        def initialize(&block)
          @middlewares = []
          self.instance_eval(&block) #在new的时候执行ruby语句,这是使用instance_eval的好地方
        end
        def use(middleware_class,*options, &block)
          @middlewares << lambda {|app| middleware_class.new(app,*options, &block)}
        end
        def run(app)
          @app = app
        end
        def to_app
          @middlewares.reverse.inject(@app) { |app, middleware| middleware.call(app)}
        end
      end
    
      使用
    
      app = Builder.new {
        use Rack::ContentLength
        use Decorator , :header => "****************header****************<br/>"
        run lambda {|env| [200, {"Content-Type"=>"text/html"}, ["hello world"]]}
      }.to_app
      Rack::Handler::WEBrick.run app, :Port => 3000
    

四. 最简单的Web框架

  • Rack 自带Rack::Builder, 实现和上面类似的功能外,还使用Rack::URLMap来处理路由

      app = Rack::Builder.new {
        map '/hello' do
          run lambda {|env| [200, {"Content-Type" => "text/html"}, ["hello"]] }
        end
        map '/world' do
          run lambda {|env| [200, {"Content-Type" => "text/html"}, ["world"]] }
        end
        map '/' do
          run lambda {|env| [200, {"Content-Type" => "text/html"}, ["all"]] }
        end
      }.to_app
    

    实现:

      #run 把app直接放入中间件
      def run(app)
        @ins << app #lambda { |nothing| app } #和map有一定冲突,都作用于数组最后
      end
    
      # to_app处理最后一个可能的hash
      def to_app
        @ins[-1] = Rack::URLMap.new(@ins.last) if Hash === @ins.last
        inner_app = @ins.last
        @ins[0...-1].reverse.inject(inner_app) { |a, e| e.call(a) }
      end
    
      #最后的hash是有map加上的,存path/rack_app键值对
      def map(path, &block)
        if @ins.last.kind_of? Hash
          @ins.last[path] = self.class.new(&block).to_app
        else
          @ins << {}
          map(path, &block)
        end
      end
    
  • rackup

    Rack提供的最简单的rackup命令允许用一个配置文件去运行我们的应用程序。

    rackup做的事情很简单,如果你提供一个配置文件config.ru(你可以取任何名字,但后缀必须为ru),然后运行 rackup config.ru

    那么它所做的事情相当于: app = Rack::Builder.new { ... 配置文件... }.to_app 然后运行这个app。

    然后使用rackup启动app,如rackup -s thin -p 3000,对应之前的Rack::Handler::WEBrick.run app, :Port => 3000

  • rackup 实现

    Rack::Server接口

    类方法

    • self.start

      def self.start
        new.start
      end
      
    • self.middleware

      rackup 根据不同的环境(可以用-E开关选择环境)装载不同的中间件

    实例方法

    • options

      def options
        @options ||= parse_options(ARGV)
      end
      

      parse_options 是的私有实例方法:

      Rack::Server.new.send(:parse_options, ['-s', 'Thin', 'config.ru'])
      => {:environment=>"development", :pid=>nil, :Port=>9292, :Host=>"0.0.0.0", :AccessLog=>[], :config=>"/var/www/sites/testusers/ued/txx800_fire/config.ru", :server=>"Thin"}
      
    • app

      调用parse_file 生成app app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)

      parse_file的实现:

      app = eval "Rack::Builder.new {( " + cfgfile + "\n )}.to_app" #貌似生成了不带中间件的app,类似装饰者中的原始组件

    • sever

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

    • middleware

      self.class.middleware

    • build_app

      通过反转循环中间件,给原始app加上所有中间件,区分了2中不同的中间件形式:lambda型和数组型

    • wrapped_app

      @wrapped_app ||= build_app app

    • start

      关键语句是server.run wrapped_app, options 好比

      Rack::Handler:XXX.run app, options


五. 中间件:第二轮