Ruby进阶之Rack深入

之前讲述了基础的Rack使用,现在我们来试试深入Rack,如果不了解Rack,可以去看看之前一篇最基础的 Ruby进阶之Rack入门

分析

  • Rack通过rackup命令启动,是如何选择应该启动哪个web容器?
  • Rack的中间件是如何调用的,如何确定中间件调用顺序?
  • 常用的几个模块介绍

如何确定启动的web容器

我们来看看rackup启动流程

# rackup是Rack提供的命令,我们可以在gem包的bin目录找到该命令

#!/usr/bin/env ruby
require "rack"
Rack::Server.start

# 如果我们不用rackup命令,也可以直接在代码里指定启动参数
#  Rack::Server.start(
#    :app => lambda do |e|
#      [200, {'Content-Type' => 'text/html'}, ['hello world']]
#    end,
#    :server => 'cgi'
#  )

rackup只有简单的两行代码,帮我们引入了rack,所以我们写代码时不用引入

  • Rack::Server.start 也是我们之前编码中省略的一部分,和我们之前省略 Rack:Builder.app 一样,在进行简单操作的时候,Rack允许我们不写这些繁琐代码

  • 其实只有在运行类似hello world这种简单代码时我们才省略并简写,正式使用过程中,还是建议按正常步骤写,尤其是在对Rack不是特别熟悉的时候,老老实实地Rack::Builder.new和Rack::Server.start吧

  • 顺便提一下,Rack::Builder.new 和 Rack::Builderf.app 是一样的,没有区别

我们进入 lib/rack/server.rb 中,代码较多,我们看关键部分


module Rack
  class Server
    #  Rack::Server.start(
    #    :app => lambda do |e|
    #      [200, {'Content-Type' => 'text/html'}, ['hello world']]
    #    end,
    #    :server => 'cgi'
    #  )
    # 实例化Server对象,Server对象接收类似上方的参数
    def self.start(options = nil)
      new(options).start
    end
  end
end

由于我们是通过rackup直接启动,start方法中我们是没传递参数的,所以来看看Rack如何自己确定选用哪个服务器

module Rack
  class Server
    # server方法中,执行了服务器的选择逻辑, 我们可以看到,调用了Rack::Handler中的get方法
    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
  end
end

上面这段代码,表明了如果没指定server,会自行选择,并且,如果Rack::Handler.get没能选出server,默认将会以FastCGI的方式运行

我们进入lib/rack/handler.rb看看关键代码


module Rack
  class Handler
    # 默认接受选择的server种类
    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
  end
end

  • 首先会被选择的依然是FastCGI,这种方式很高效,不过我还没发现哪个ruby框架采用的是FastCGI,不明白为什么
  • 然后被选择的是CGI方式启动
  • 然后看看用户有没有指定需要启动的server
  • 最后在 puma\thin\webrick 中依次选择

换句话说,如果我本地装有puma和thin的gem,默认情况下会启动puma

调用栈

之前我们在Rack中间件的时候提到过调用栈,现在我们来分析下Rack的调用栈是如何工作的

我们进入lib/rack/builder.rb 看看use方法的定义

module Rack
  class Builder
    # 这就是我们在使用中间件时调用的use方法
    def use(middleware, *args, &block)
      # ....此处省略小部分代码....
      @use << proc { |app| middleware.new(app, *args, &block) }
    end
  end
end

我们可以看到,有一个数组@use,收集我们传递的中间件,并以proc的方式包装了起来

我们讲过,Rack运行前都会通过Rack::Builder实例化一个app对象,在实例化app对象的时候,就会对@use里面包含的中间件进行组合,形成调用栈

我们看以下代码

module Rack
  class Builder
    # app对象产生过程
    def to_app
      app = @map ? generate_map(@run, @map) : @run
      fail "missing run or map statement" unless app
      # 关键点就下面这一句话
      app = @use.reverse.inject(app) { |a,e| e[a] }
      @warmup.call(app) if @warmup
      app
    end

    # 运行前会生成app对象
    def call(env)
      to_app.call(env)
    end
  end
end

不知道大家有没有理解app = @use.reverse.inject(app) { |a,e| e[a] } ,这段代码将所有中间件组合在了一起
由于proc和中间件都是响应call方法的对象,一层一层嵌套,在调用to_app.call(env) 的时候,所有call方法都会依次执行

可以看看一个调用方式类似的demo代码

#! /usr/bin/env ruby
#  encoding: utf-8

use = []
run = 'hello'

5.times do |i|
  use << proc { |item| "#{item}-#{i}" }
end

# 将字符串按顺序连接
app = use.reverse.inject(run) { |a, e| e[a] }

puts app

# 最后将会输出
"hello-4-3-2-1-0"

常用模块 - Rack::Request

这个模块几乎所有基于rack的框架都用啦,基本把Request相关的信息都封装好了,使用更加简单

#! /usr/bin/env ruby
#  encoding: utf-8

class MyApp
  def call(env)
    # 创建一个request对象,就能享受Rack::Request的便捷操作了
    @request = Rack::Request.new env
    puts @request.request_method
    puts @request.path_info
    [200, {}, ['hello world']]
  end
end

# 我们创建一个 rack app
app = Rack::Builder.new do 
  run MyApp.new
end

# 启动 server
Rack::Server.start(:app => app, :server => 'webrick')

更多的Rack::Request操作可以参照文档

常用模块 - Rack:Response

这个模块也是几乎所有基于rack的框架都用,把Response相关的操作都做了封装

#! /usr/bin/env ruby
#  encoding: utf-8

class MyApp
  def call(env)
    @response = Rack::Response.new
    @response.set_cookie('token', 'xxxxxxx')
    @response.headers['Content-Type'] = 'text/plain'
    [200, {}, ['hello world']]
  end
end

# 我们创建一个 rack app
app = Rack::Builder.new do 
  run MyApp.new
end

# 启动 server
Rack::Server.start(:app => app, :server => 'webrick')

更多的Rack::Response操作可以参照文档

** Rack暂且讲到这里,很多有用的工具模块可以多去看看Rack源码或者文档,后面找时间用Rack实现一个自己的框架 **

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 121,247评论 16 134
  • 简介 基本上所有的 Ruby web framework 都是Rack App,web框架大多都是基于rack之上...
    感觉被掏空阅读 2,041评论 0 8
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 157,792评论 24 688
  • 学习 ruby on rails 有一段时间了,也写过一些简单的程序。但对 rails 一直充满神秘感,为什么我们...
    z_k阅读 1,766评论 1 7
  • 话不多说,直接上代码 UIImagePickerControllerDelegate协议里面的方法,注意先去遵循协...
    立刻就爽阅读 672评论 0 2