美文网首页ruby on railsRuby
rails s 启动过程分析

rails s 启动过程分析

作者: z_k | 来源:发表于2017-03-17 18:24 被阅读0次

    学习 ruby on rails 有一段时间了,也写过一些简单的程序。但对 rails 一直充满神秘感,为什么我们把代码填充到 controller、view、model 里,再执行一下 rails s,就能得到想要的结果。rails 背后为我们隐藏了多少东西,如果一点都不清楚,这样写代码不是像在搭建空中阁楼吗?心里总觉得不牢靠。感觉要想进一步提高水平,我得看一下源代码,至少可以满足以下心里的好奇心。以前学些程序的时候,比如 C/C++ 什么的,都有一个程序入口点 main 函数。在 rails 里,rails s 貌似是一切开始的地方,于是就从这里开始吧,看看它都干了什么。
    在看 railties 源码时,发现 caller_locations 方法可以显示调用栈,于是想用它来跟踪了一下 rails s 的执行过程,虽然它并不能显示所有调用的方法,但是能大致知道程序经过了哪些文件,调用了哪些方法。我在 config/environment.rb 文件的末尾加上 caller_locations.each { |call| puts call.to_s } 一行,然后执行 rails s,把输出的结果作为模糊的地图开始了 rails s 之旅。

    环境说明
    ruby 2.4.0, Rails 5.0.2

    ruby gems 的位置
    cd \gem environment gemdir rails`/gems`

    rails 命令在这里 ~/.rvm/gems/ruby-2.4.0/bin/rails

    load Gem.activate_bin_path('railties', 'rails', version)
    

    Gem.activate_bin_path 的主要作用是找到 gem 包下的可执行文件,即 bin 或 exe 目录下的文件。这里找到的是 /.rvm/gems/ruby-2.4.0/gems railties-5.0.2/exe/rails,这个文件的主要作用是加载 require "rails/cli",该文件位于 .rvm/gems/ruby-2.4.0/gems railties-5.0.2/lib/rails/cli.rb

    require 'rails/app_loader'
    
    # If we are inside a Rails application this method performs an exec and thus
    # the rest of this script is not run.
    Rails::AppLoader.exec_app
    
    require 'rails/ruby_version_check'
    Signal.trap("INT") { puts; exit(1) }
    
    if ARGV.first == 'plugin'
      ARGV.shift
      require 'rails/commands/plugin'
    else
      require 'rails/commands/application'
    end
    

    如果已新建了 rails app 并在应用程序目录下,则加载 AppLoader 模块并执行 exec_app 方法启动应用,否则进入新建 app 流程。

    Rails::AppLoader.exec_app 方法用来找到应用程序目录下的 bin/rails 文件并执行。该方法通过一个 loop 循环,从当前目录逐级向上查找 bin/rails ,所以即使在应用程序的子目录里也是可以执行 rails 命令的。如果找到,并且文件中设置了 APP_PATH,则执行该文件。
    .rvm/gems/ruby-2.4.0/gems railties-5.0.2/lib/rails/app_loader.rb

    def exec_app
      original_cwd = Dir.pwd
    
      loop do
        if exe = find_executable
          contents = File.read(exe)
    
          if contents =~ /(APP|ENGINE)_PATH/
            exec RUBY, exe, *ARGV
            break # non reachable, hack to be able to stub exec in the test suite
          elsif exe.end_with?('bin/rails') && contents.include?('This file was generated by Bundler')
            $stderr.puts(BUNDLER_WARNING)
            Object.const_set(:APP_PATH, File.expand_path('config/application', Dir.pwd))
            require File.expand_path('../boot', APP_PATH)
            require 'rails/commands'
            break
          end
        end
    
        # If we exhaust the search there is no executable, this could be a
        # call to generate a new application, so restore the original cwd.
        Dir.chdir(original_cwd) and return if Pathname.new(Dir.pwd).root?
    
        # Otherwise keep moving upwards in search of an executable.
        Dir.chdir('..')
      end
    end
    

    bin/rails 文件将 APP_PATH 设置为 config/application,然后加载 config/bootrails/commands

    #!/usr/bin/env ruby
    APP_PATH = File.expand_path('../config/application', __dir__)
    require_relative '../config/boot'
    require 'rails/commands'
    

    config/boot 文件,主要作用是通过 Bundler.setup 将 Gemfile 中的 gem 路径添加到加载路径,以便后续 require。

    ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
    
    require 'bundler/setup' # Set up gems listed in the Gemfile.
    

    .rvm/gems/ruby-2.4.0/gems/railties-5.0.2/lib/rails/commands.rb,这里解析 rails 命令参数,根据不同的参数执行不同的任务。

    ARGV << '--help' if ARGV.empty?
    
    aliases = {
      "g"  => "generate",
      "d"  => "destroy",
      "c"  => "console",
      "s"  => "server",
      "db" => "dbconsole",
      "r"  => "runner",
      "t"  => "test"
    }
    
    command = ARGV.shift
    command = aliases[command] || command
    
    require 'rails/commands/commands_tasks'
    
    Rails::CommandsTasks.new(ARGV).run_command!(command)
    

    因为这里我们的命令参数是 s (server),所以 run_command! 函数调用了 server 方法
    .rvm/gems/ruby-2.4.0/gems/railties-5.0.2/lib/rails/commands/commands_tasks.rb

    def server
      set_application_directory!
      require_command!("server")
    
      Rails::Server.new.tap do |server|
        # We need to require application after the server sets environment,
        # otherwise the --environment option given to the server won't propagate.
        require APP_PATH
        Dir.chdir(Rails.application.root)
        server.start
      end
    end
    

    server 方法首先设置应用程序目录,即包含 config.ru 文件的目录。然后加载 Rails::Server 模块。然后加载 APP_PATH,即 config/application.rb 文件,该文件加载了 rails 的全部组件 require "rails/all",并定义了我们自己的 rails 应用程序类,如 class Application < Rails::Application,然后启动服务器。

    .rvm/gems/ruby-2.4.0/gems/railties-5.0.2/lib/rails/commands/server.rb

    def start
      print_boot_information
      trap(:INT) { exit }
      create_tmp_directories
      setup_dev_caching
      log_to_stdout if options[:log_stdout]
    
      super
    ensure
      # The '-h' option calls exit before @options is set.
      # If we call 'options' with it unset, we get double help banners.
      puts 'Exiting' unless @options && options[:daemonize]
    end
    

    start 方法做了一些准备和处理,直接调用 super 进入父类的 start 方法。Rails::Server 继承自 Rack::Server。

    def start &blk
      ....
    
      check_pid! if options[:pid]
    
      # Touch the wrapped app, so that the config.ru is loaded before
      # daemonization (i.e. before chdir, etc).
      wrapped_app
    
      daemonize_app if options[:daemonize]
    
      write_pid if options[:pid]
    
      trap(:INT) do
        if server.respond_to?(:shutdown)
          server.shutdown
        else
          exit
        end
      end
    
      server.run wrapped_app, options, &blk
    end
    

    在这里通过 Rack 分别通过两个模块 Rack::Builder Rack::Handler 创建 app 和选择服务器。wrapped_app 方法通过调用 app 方法创建应用程序实例,并调用 build_app 方法加载所有中间件。在 default_middleware_by_environment 方法里可以看到默认的中间件。
    接着调用 server 方法选择 server,然后调用 server.run 启动服务器。

    .rvm/gems/ruby-2.4.0/gems/rack-2.0.1/lib/rack/server.rb

    def wrapped_app
      @wrapped_app ||= build_app app
    end
    
    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
    
    def app
      @app ||= options[:builder] ? build_app_from_string : build_app_and_options_from_config
    end
    

    由于在创建 Rails::Server 实例时没有传递参数,所以初始化是调用 default_options 方法设置了 options 。其中有一项 optioins[:config] 被设置为 "config.ru",这决定了 app 的创建方式是根据该配置文件创建。

    def build_app_and_options_from_config
      if !::File.exist? options[:config]
        abort "configuration #{options[:config]} not found"
      end
    
      app, options = Rack::Builder.parse_file(self.options[:config], opt_parser)
      @options.merge!(options) { |key, old, new| old }
      app
    end
    

    具体的创建是通过调用 Rack::Builder.parse_file 方法实现。该方法首先解析 config.ru 文件,然后实例化 Rack::Builder 对象,然后在实例上下文中执行 config.ru 文件中的代码,然后调用 to_app 方法返回 app 对象。

    def self.parse_file(config, opts = Server::Options.new)
      options = {}
      if config =~ /\.ru$/
        cfgfile = ::File.read(config)
        if cfgfile[/^#\\(.*)/] && opts
          options = opts.parse! $1.split(/\s+/)
        end
        cfgfile.sub!(/^__END__\n.*\Z/m, '')
        app = new_from_string cfgfile, config
      else
        require config
        app = Object.const_get(::File.basename(config, '.rb').split('_').map(&:capitalize).join(''))
      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
    
    def initialize(default_app = nil, &block)
      @use, @map, @run, @warmup = [], nil, default_app, nil
      instance_eval(&block) if block_given?
    end
    

    config.ru 文件中的代码是在 Rack::Builder 的实例上下文中执行的,所以 run Rails.application 实际上是执行 Rack::Builder#run。它只是简单的把 Rails.application,即我们自己的 Rails 应用程序实例赋值给 Rack::Server@app。在此之前它先加载了 config/environment.rb 文件初始化应用 Rails.application.initialize!,而后者又加载了 config/application.rb,该文件中加载了 rails 的全部组件 require 'rails/all' 和定义了我们自己 Application 类 class Application < Rails::Application

    def run(app)
      @run = app
    end
    
    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
    
    以下是 config.ru 文件代码
    # This file is used by Rack-based servers to start the application.
    
    require_relative 'config/environment'
    
    # run is a method of Rack::Handler that used for setting up rack server.
    run Rails.application
    
    以下是 config/environment.rb 文件代码
    # Load the Rails application.
    require_relative 'application'
    
    # Initialize the Rails application.
    Rails.application.initialize!
    

    到此 Rack app 对象已经建立,Rails.application 实例被保存在 Rack::Server 实例的 @app 里。接着调用 server.run ,并将 app 作为参数传递给它。

    server 方法通过 Rack::Handler 选择服务器。由于我们创建 server 实例时没有提供 options,默认的 options[:server] 不存在。所以,通过 Rack::Handler.default 方法进行猜选,先查看 ENV 环境变量里有没有设置,如果没有则按照 ['puma', 'thin', 'webrick'] 顺序猜选。通过调用 Rack::Handler.get 方法获取 server 类常量。首先查看@handlers 中是否有匹配的,如没有则尝试通过 try_require('rack/handler', server) 加载。比如对于 puma,该方法的加载路径相当于 rack/handler/puma。可能你会想这个路径哪里来的,从 RAILS 5 开始,Gemfile 文件里有一行 gem 'puma', '~> 3.0',然后启动应用程序时,在 config/boot.rb 文件里已经通过 Bundler.setup 加载了所有依赖的路径。最终 get 方法会返回 Rack::Handler::Puma 这样一个类。

    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
    
    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
    
    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
    

    Rack::Server#start 方法里调用的 server.run wrapped_app, options, &blk 实际上是执行了 Rack::Handler::Puma.run 。该方法在.rvm/gems/ruby-2.4.0/gems/puma-3.7.1/lib/rack/handler/puma.rb文件里。到这里暂且不用往下挖了,可以想象 Puma 服务器进程启动起来,一切就绪,进入服务器端口监听循环,随时等待接收客户端发来的请求,或者直到收到系统中断信号,关闭服务器,回到最开始的位置。还记得 rails s 吗?这时它正微笑着和你打招呼 “嘿,骚年我以为你迷路了呢?”

    小结

    从敲下 rails s 命令开始,到服务器启动起来,其实用很短的话就可以概括:rails 命令行解析,创建 Rails::Server 对象,加载 Rails.application,启动服务器监听用户请求。绕了这么一个大圈子,似乎懂了点什么,又好像没懂什么,至少对 Rails 的神秘感减少了几分吧。

    相关文章

      网友评论

        本文标题:rails s 启动过程分析

        本文链接:https://www.haomeiwen.com/subject/jtulgttx.html