首页ruby

rack 介绍与原理

hfpp2012发布于38 次阅读

1. 什么是rack?

rails应该部署到线上生产环境时,我们可能会选择unicornpuma,姑且将unicornpuma称为应用容器,它是用来运行ruby代码的。而railssinatra称为ruby的web框架。我们在本地跑rails应用的时候,也就是执行rails c命令,可以是用webrick来跑的,在线上却又是unicornpuma,之所以如此灵活,就是因为rack。因为railssinatra也是实现了rack的机制,而unicornpuma等也是,所以它们轻易地换,而不影响整个应用。rack也可以称为一套协议,只要你的web应用容器都实现了rack的机制,而rails本身也是实现了rack的机制的,所以可以用任意一套实现rack机制的应用容器来跑rails应用,例如thinunicornpuma等。

rack本身是一个gem,我们可以跑一个hello world。

# rack.rb
require 'rack'

app = Proc.new do |env|
  ['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']]
end

Rack::Handler::WEBrick.run app

我们先来运行这个程序。

$ ruby rack.rb
[2016-01-24 12:32:04] INFO  WEBrick 1.3.1
[2016-01-24 12:32:04] INFO  ruby 2.2.2 (2015-04-13) [x86_64-darwin14]
[2016-01-24 12:32:04] INFO  WEBrick::HTTPServer#start: pid=3204 port=8080

WEBrick是一个应用容器,是ruby标准库提供的功能,它提供了一个http server的功能,用来可以来跑rack应用。

可以看到,程序监听在8080端口。

我们用浏览器测试一下。

['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']]是rack返回的数据,它包括三个内容:

  • 状态码200
  • 响应头部信息
  • 响应内容

我们用curl工具来测试它返回的响应的头部信息。

$ curl -i http://localhost:8080
HTTP/1.1 200 OK 
Content-Type: text/html
Server: WEBrick/1.3.1 (Ruby/2.2.2/2015-04-13)
Date: Sun, 24 Jan 2016 04:50:36 GMT
Content-Length: 21
Connection: Keep-Alive

A barebones rack app.%

除了上述这种方法,还有另外一种方法可以运行rack。

# config.ru

run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack\'d']] }

要运行的话可以这样:

$ rackup config.ru 
[2016-01-24 13:26:50] INFO  WEBrick 1.3.1
[2016-01-24 13:26:50] INFO  ruby 2.2.2 (2015-04-13) [x86_64-darwin14]
[2016-01-24 13:26:50] INFO  WEBrick::HTTPServer#start: pid=5975 port=9292

2. 原理

可以看到,rackup也是用webrick来加载rack程序的。上面的代码是指定了用webrick,而这里没有指定,它也会默认去找webrick。

为什么呢?这个可以来研究一下rack的源码。

像上述所说的webrickunicornpumathin等,在官方有个称谓,叫handlers

而默认情况下rack支持的handlers有下面几种:

  • WEBrick

  • FCGI

  • CGI

  • SCGI

  • LiteSpeed

  • Thin

相关的源码可见于https://github.com/rack/rack/tree/master/lib/rack/handler

而为什么没有指定handler的情况下,会去找webrick,可以看下面的代码:

# https://github.com/rack/rack/blob/master/lib/rack/handler.rb
def self.pick(server_names)
  server_names = Array(server_names)
  server_names.each do |server_name|
    begin
      return get(server_name.to_s)
    rescue LoadError, NameError
    end
  end

  raise LoadError, "Couldn't find handler for: #{server_names.join(', ')}."
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 ['thin', 'puma', 'webrick']
  end
end

主要是这一句在发挥作用:pick ['thin', 'puma', 'webrick']

也就是说系统会优先选择thin,之后是puma,最后才是webrick

现在来验证一下我们的想法。

先来安装一下puma

$ gem install puma

然后运行rack程序。

rackup config.ru 
Puma 2.15.3 starting...
* Min threads: 0, max threads: 16
* Environment: development
* Listening on tcp://localhost:9292

果然,rack程序用puma来跑了,而不选择webrick,因为webrick排在puma之后。

回到刚才的问题,为什么指定webrick的时候就跑在8080端口,没指定的时候就跑在9292端口呢?

指定webrick的时候,是这样指定的:

Rack::Handler::WEBrick.run app

来看相关的rack源码:

# https://github.com/rack/rack/blob/master/lib/rack/handler/webrick.rb
module Rack
  module Handler
    class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet
      def self.run(app, options={})
        environment  = ENV['RACK_ENV'] || 'development'
        default_host = environment == 'development' ? 'localhost' : nil

        options[:BindAddress] = options.delete(:Host) || default_host
        options[:Port] ||= 8080
        @server = ::WEBrick::HTTPServer.new(options)
        @server.mount "/", Rack::Handler::WEBrick, app
        yield @server  if block_given?
        @server.start
      end
    end
  end
end

上面的options[:Port] ||= 8080显示的就是8080端口的。

而没有指定的时候就去跑9292端口,也来看下源码:

# https://github.com/rack/rack/blob/028438ffffd95ce1f6197d38c04fa5ea6a034a85/lib/rack/server.rb#L203
def default_options
  environment  = ENV['RACK_ENV'] || 'development'
  default_host = environment == 'development' ? 'localhost' : '0.0.0.0'

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

也就是说,用rackup运行的时候会去跑9292端口。

先来分析一下为什么执行rackup config.ru的时候,会发生这么神的事。

# bin/rackup
#!/usr/bin/env ruby

require "rack"
Rack::Server.start

rackup程序的源码只有几行,我们找到Rack::Server.start的部分。

第一步,要先解析那个config.ru文件的内容。

# https://github.com/rack/rack/blob/028438ffffd95ce1f6197d38c04fa5ea6a034a85/lib/rack/server.rb#L313
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,这里可以放一放。

先来看看Rack::Server.start做了什么事?

# https://github.com/rack/rack/blob/028438ffffd95ce1f6197d38c04fa5ea6a034a85/lib/rack/server.rb#L257
def start &blk
  if options[:warn]
    $-w = true
  end

  if includes = options[:include]
    $LOAD_PATH.unshift(*includes)
  end

  if library = options[:require]
    require library
  end

  if options[:debug]
    $DEBUG = true
    require 'pp'
    p options[:server]
    pp wrapped_app
    pp app
  end

  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

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

看到start方法的最后一行server.run wrapped_app, options, &blk,就是去执行server方法,而server会通过Rack::Handler.get去找handler,例如webrick,就是上面所说的。

找到之后会运行run方法,比如我们以webrick为例。

# https://github.com/rack/rack/blob/master/lib/rack/handler/webrick.rb
module Rack
  module Handler
    class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet
      def self.run(app, options={})
        environment  = ENV['RACK_ENV'] || 'development'
        default_host = environment == 'development' ? 'localhost' : nil

        options[:BindAddress] = options.delete(:Host) || default_host
        options[:Port] ||= 8080
        @server = ::WEBrick::HTTPServer.new(options)
        @server.mount "/", Rack::Handler::WEBrick, app
        yield @server  if block_given?
        @server.start
      end
    end
  end
end

::WEBrick::HTTPServer.new(options)这一行是关键,默认情况下,webrick是开启tcp服务的,::WEBrick::HTTPServer.new(options)开启的是http服务,我们来看下webrick相关的源码:

require 'socket'

module WEBrick
  class GenericServer
    def start(&block)
      raise ServerError, "already started." if @status != :Stop
      server_type = @config[:ServerType] || SimpleServer

      setup_shutdown_pipe

      server_type.start{
        @logger.info \
          "#{self.class}#start: pid=#{$$} port=#{@config[:Port]}"
        call_callback(:StartCallback)

        shutdown_pipe = @shutdown_pipe

        thgroup = ThreadGroup.new
        @status = :Running
        begin
          while @status == :Running
            begin
              sp = shutdown_pipe[0]
              if svrs = IO.select([sp, *@listeners], nil, nil, 2.0)
                if svrs[0].include? sp
                  # swallow shutdown pipe
                  buf = String.new
                  nil while String ===
                            sp.read_nonblock([sp.nread, 8].max, buf, exception: false)
                  break
                end
                svrs[0].each{|svr|
                  @tokens.pop          # blocks while no token is there.
                  if sock = accept_client(svr)
                    unless config[:DoNotReverseLookup].nil?
                      sock.do_not_reverse_lookup = !!config[:DoNotReverseLookup]
                    end
                    th = start_thread(sock, &block)
                    th[:WEBrickThread] = true
                    thgroup.add(th)
                  else
                    @tokens.push(nil)
                  end
                }
              end
            ...
          end
        ensure
          ...
        end
      }
    end

  end
end

可见,webrick的是基于socket开发的,IO.select([sp, *@listeners], nil, nil, 2.0)sock = accept_client(svr)接收了客户的链接,并把socket放到sock变量中。

module WEBrick
  class HTTPServer < ::WEBrick::GenericServer
    def run(sock)
      while true
        res = HTTPResponse.new(@config)
        req = HTTPRequest.new(@config)
        server = self
        ...
      end
    end
  end
end

为什么会接收到socket之后去运行run方法呢,可以查看start_thread方法中的这一行block ? block.call(sock) : run(sock)

HTTPServer还会对请求的信息进行处理和封装,比如请求的参数,头部信息等,就是这行HTTPRequest.new(@config)代码发挥的作用。

我们可以来验证一下。

require 'rack'
require 'pp'

app = Proc.new do |env|
  pp "*" * 50
  pp env
  pp "*" * 50
  pp env["rack.input"].string
  pp "*" * 50
  ['200', {'Content-Type' => 'text/html'}, ['A barebones rack app.']]
end

Rack::Handler::WEBrick.run app

然后我们给这个rack程序发送请求,并指定参数和头部信息。

$ curl -v localhost:8080 -X POST \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{"movie": {"title": "Star Wars: A New Hope"}}'

在log上看到的输出大约是下面这样的。

$ ruby rack.rb 
[2016-01-24 15:47:56] INFO  WEBrick 1.3.1
[2016-01-24 15:47:56] INFO  ruby 2.2.2 (2015-04-13) [x86_64-darwin14]
[2016-01-24 15:47:56] INFO  WEBrick::HTTPServer#start: pid=26645 port=8080
"**************************************************"
{"CONTENT_LENGTH"=>"45",
 "CONTENT_TYPE"=>"application/json",
 "GATEWAY_INTERFACE"=>"CGI/1.1",
 "PATH_INFO"=>"/",
 "QUERY_STRING"=>"",
 "REMOTE_ADDR"=>"::1",
 "REMOTE_HOST"=>"localhost",
 "REQUEST_METHOD"=>"POST",
 "REQUEST_URI"=>"http://localhost:8080/",
 "SCRIPT_NAME"=>"",
 "SERVER_NAME"=>"localhost",
 "SERVER_PORT"=>"8080",
 "SERVER_PROTOCOL"=>"HTTP/1.1",
 "SERVER_SOFTWARE"=>"WEBrick/1.3.1 (Ruby/2.2.2/2015-04-13)",
 "HTTP_USER_AGENT"=>"curl/7.37.1",
 "HTTP_HOST"=>"localhost:8080",
 "HTTP_ACCEPT"=>"application/json",
 "rack.version"=>[1, 3],
 "rack.input"=>#<StringIO:0x007fafa29c3de0>,
 "rack.errors"=>#<IO:<STDERR>>,
 "rack.multithread"=>true,
 "rack.multiprocess"=>false,
 "rack.run_once"=>false,
 "rack.url_scheme"=>"http",
 "rack.hijack?"=>true,
 "rack.hijack"=>
  #<Proc:0x007fafa29c3c78@/Users/macintosh1/.rbenv/versions/2.2.2/lib/ruby/gems/2.2.0/gems/rack-1.6.4/lib/rack/handler/webrick.rb:76 (lambda)>,
 "rack.hijack_io"=>nil,
 "HTTP_VERSION"=>"HTTP/1.1",
 "REQUEST_PATH"=>"/"}
"**************************************************"
"{\"movie\": {\"title\": \"Star Wars: A New Hope\"}}"
"**************************************************"

它能把我使用的请求协议,端口,地址,请求参数,还有头部信息全部得到,这又是如何办到的呢?

所有的一切都是在这个文件里实现的https://github.com/ruby/ruby/blob/3e92b635fb5422207b7bbdc924e292e51e21f040/lib/webrick/httprequest.rb

比如:

def meta_vars
  meta = Hash.new

  cl = self["Content-Length"]
  ct = self["Content-Type"]
  meta["CONTENT_LENGTH"]    = cl if cl.to_i > 0
  meta["CONTENT_TYPE"]      = ct.dup if ct
  meta["GATEWAY_INTERFACE"] = "CGI/1.1"
  meta["PATH_INFO"]         = @path_info ? @path_info.dup : ""
 #meta["PATH_TRANSLATED"]   = nil      # no plan to be provided
  meta["QUERY_STRING"]      = @query_string ? @query_string.dup : ""
  meta["REMOTE_ADDR"]       = @peeraddr[3]
  meta["REMOTE_HOST"]       = @peeraddr[2]
 #meta["REMOTE_IDENT"]      = nil      # no plan to be provided
  meta["REMOTE_USER"]       = @user
  meta["REQUEST_METHOD"]    = @request_method.dup
  meta["REQUEST_URI"]       = @request_uri.to_s
  meta["SCRIPT_NAME"]       = @script_name.dup
  meta["SERVER_NAME"]       = @host
  meta["SERVER_PORT"]       = @port.to_s
  meta["SERVER_PROTOCOL"]   = "HTTP/" + @config[:HTTPVersion].to_s
  meta["SERVER_SOFTWARE"]   = @config[:ServerSoftware].dup

  self.each{|key, val|
    next if /^content-type$/i =~ key
    next if /^content-length$/i =~ key
    name = "HTTP_" + key
    name.gsub!(/-/o, "_")
    name.upcase!
    meta[name] = val
  }

  meta
end

和输出的内容很相似吧!

完结。

本站文章均为原创内容,如需转载请注明出处,谢谢。

0 条回复
暂无回复~~
喜欢

© Rails365 | 隐私条款 | 服务条款 | 粤ICP备15004902号 | 在线学员:24

Top