rails s 啟動過程分析

學(xué)習(xí) ruby on rails 有一段時間了呜达,也寫過一些簡單的程序捺球。但對 rails 一直充滿神秘感,為什么我們把代碼填充到 controller班巩、view金吗、model 里,再執(zhí)行一下 rails s趣竣,就能得到想要的結(jié)果摇庙。rails 背后為我們隱藏了多少東西,如果一點都不清楚遥缕,這樣寫代碼不是像在搭建空中閣樓嗎卫袒?心里總覺得不牢靠。感覺要想進(jìn)一步提高水平单匣,我得看一下源代碼夕凝,至少可以滿足以下心里的好奇心。以前學(xué)些程序的時候户秤,比如 C/C++ 什么的码秉,都有一個程序入口點 main 函數(shù)。在 rails 里鸡号,rails s 貌似是一切開始的地方转砖,于是就從這里開始吧,看看它都干了什么鲸伴。
在看 railties 源碼時府蔗,發(fā)現(xiàn) caller_locations 方法可以顯示調(diào)用棧晋控,于是想用它來跟蹤了一下 rails s 的執(zhí)行過程,雖然它并不能顯示所有調(diào)用的方法姓赤,但是能大致知道程序經(jīng)過了哪些文件赡译,調(diào)用了哪些方法。我在 config/environment.rb 文件的末尾加上 caller_locations.each { |call| puts call.to_s } 一行不铆,然后執(zhí)行 rails s蝌焚,把輸出的結(jié)果作為模糊的地圖開始了 rails s 之旅。

環(huán)境說明
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 包下的可執(zhí)行文件综看,即 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 并在應(yīng)用程序目錄下舞吭,則加載 AppLoader 模塊并執(zhí)行 exec_app 方法啟動應(yīng)用泡垃,否則進(jìn)入新建 app 流程。

Rails::AppLoader.exec_app 方法用來找到應(yīng)用程序目錄下的 bin/rails 文件并執(zhí)行羡鸥。該方法通過一個 loop 循環(huán)蔑穴,從當(dāng)前目錄逐級向上查找 bin/rails ,所以即使在應(yīng)用程序的子目錄里也是可以執(zhí)行 rails 命令的惧浴。如果找到存和,并且文件中設(shè)置了 APP_PATH,則執(zhí)行該文件衷旅。
.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 設(shè)置為 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 路徑添加到加載路徑茄袖,以便后續(xù) 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 命令參數(shù)宪祥,根據(jù)不同的參數(shù)執(zhí)行不同的任務(wù)。

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)

因為這里我們的命令參數(shù)是 s (server)家乘,所以 run_command! 函數(shù)調(diào)用了 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 方法首先設(shè)置應(yīng)用程序目錄蝗羊,即包含 config.ru 文件的目錄。然后加載 Rails::Server 模塊仁锯。然后加載 APP_PATH耀找,即 config/application.rb 文件,該文件加載了 rails 的全部組件 require "rails/all"业崖,并定義了我們自己的 rails 應(yīng)用程序類涯呻,如 class Application < Rails::Application凉驻,然后啟動服務(wù)器。

.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 方法做了一些準(zhǔn)備和處理复罐,直接調(diào)用 super 進(jìn)入父類的 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 創(chuàng)建 app 和選擇服務(wù)器效诅。wrapped_app 方法通過調(diào)用 app 方法創(chuàng)建應(yīng)用程序?qū)嵗凸觯⒄{(diào)用 build_app 方法加載所有中間件。在 default_middleware_by_environment 方法里可以看到默認(rèn)的中間件乱投。
接著調(diào)用 server 方法選擇 server咽笼,然后調(diào)用 server.run 啟動服務(wù)器。

.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

由于在創(chuàng)建 Rails::Server 實例時沒有傳遞參數(shù)戚炫,所以初始化是調(diào)用 default_options 方法設(shè)置了 options 剑刑。其中有一項 optioins[:config] 被設(shè)置為 "config.ru",這決定了 app 的創(chuàng)建方式是根據(jù)該配置文件創(chuàng)建双肤。

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

具體的創(chuàng)建是通過調(diào)用 Rack::Builder.parse_file 方法實現(xiàn)施掏。該方法首先解析 config.ru 文件,然后實例化 Rack::Builder 對象茅糜,然后在實例上下文中執(zhí)行 config.ru 文件中的代碼七芭,然后調(diào)用 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 的實例上下文中執(zhí)行的蔑赘,所以 run Rails.application 實際上是執(zhí)行 Rack::Builder#run狸驳。它只是簡單的把 Rails.application,即我們自己的 Rails 應(yīng)用程序?qū)嵗x值給 Rack::Server@app缩赛。在此之前它先加載了 config/environment.rb 文件初始化應(yīng)用 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 對象已經(jīng)建立究西,Rails.application 實例被保存在 Rack::Server 實例的 @app 里。接著調(diào)用 server.run 物喷,并將 app 作為參數(shù)傳遞給它卤材。

server 方法通過 Rack::Handler 選擇服務(wù)器。由于我們創(chuàng)建 server 實例時沒有提供 options峦失,默認(rèn)的 options[:server] 不存在扇丛。所以,通過 Rack::Handler.default 方法進(jìn)行猜選尉辑,先查看 ENV 環(huán)境變量里有沒有設(shè)置帆精,如果沒有則按照 ['puma', 'thin', 'webrick'] 順序猜選。通過調(diào)用 Rack::Handler.get 方法獲取 server 類常量。首先查看@handlers 中是否有匹配的卓练,如沒有則嘗試通過 try_require('rack/handler', server) 加載隘蝎。比如對于 puma,該方法的加載路徑相當(dāng)于 rack/handler/puma襟企≈雒矗可能你會想這個路徑哪里來的,從 RAILS 5 開始顽悼,Gemfile 文件里有一行 gem 'puma', '~> 3.0'曼振,然后啟動應(yīng)用程序時,在 config/boot.rb 文件里已經(jīng)通過 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 方法里調(diào)用的 server.run wrapped_app, options, &blk 實際上是執(zhí)行了 Rack::Handler::Puma.run 。該方法在.rvm/gems/ruby-2.4.0/gems/puma-3.7.1/lib/rack/handler/puma.rb文件里木羹。到這里暫且不用往下挖了甲雅,可以想象 Puma 服務(wù)器進(jìn)程啟動起來,一切就緒坑填,進(jìn)入服務(wù)器端口監(jiān)聽循環(huán)抛人,隨時等待接收客戶端發(fā)來的請求,或者直到收到系統(tǒng)中斷信號穷遂,關(guān)閉服務(wù)器,回到最開始的位置娱据。還記得 rails s 嗎蚪黑?這時它正微笑著和你打招呼 “嘿,騷年我以為你迷路了呢?”

小結(jié)

從敲下 rails s 命令開始中剩,到服務(wù)器啟動起來忌穿,其實用很短的話就可以概括:rails 命令行解析,創(chuàng)建 Rails::Server 對象结啼,加載 Rails.application掠剑,啟動服務(wù)器監(jiān)聽用戶請求。繞了這么一個大圈子郊愧,似乎懂了點什么朴译,又好像沒懂什么,至少對 Rails 的神秘感減少了幾分吧属铁。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末眠寿,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子焦蘑,更是在濱河造成了極大的恐慌盯拱,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異狡逢,居然都是意外死亡宁舰,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進(jìn)店門奢浑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蛮艰,“玉大人,你說我怎么就攤上這事殷费∮±螅” “怎么了?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵详羡,是天一觀的道長仍律。 經(jīng)常有香客問我,道長实柠,這世上最難降的妖魔是什么水泉? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮窒盐,結(jié)果婚禮上草则,老公的妹妹穿的比我還像新娘。我一直安慰自己蟹漓,他們只是感情好炕横,可當(dāng)我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著葡粒,像睡著了一般份殿。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上嗽交,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天卿嘲,我揣著相機(jī)與錄音,去河邊找鬼夫壁。 笑死拾枣,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的盒让。 我是一名探鬼主播梅肤,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼邑茄!你這毒婦竟也來了凭语?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤撩扒,失蹤者是張志新(化名)和其女友劉穎似扔,沒想到半個月后吨些,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡炒辉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年豪墅,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片黔寇。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡偶器,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出缝裤,到底是詐尸還是另有隱情屏轰,我是刑警寧澤,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布憋飞,位于F島的核電站霎苗,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏榛做。R本人自食惡果不足惜唁盏,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望检眯。 院中可真熱鬧厘擂,春花似錦、人聲如沸锰瘸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽避凝。三九已至舞萄,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間恕曲,已是汗流浹背鹏氧。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工渤涌, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留佩谣,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓实蓬,卻偏偏與公主長得像茸俭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子安皱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容