Jonny Zheng

I'm an idealist, not perfectionist

Rails 源码学习 2 (Rack and Webrick)

先来一个rack程序的简单例子

1
2
3
4
5
6
7
8
9
10
11
require 'rubygems'
require 'rack'


class HelloWorld
  def call(env)
    return [200, {}, ["Hello world!"]]
  end
end

Rack::Handler::WEBrick.run(HelloWorld.new,:Port => 9000)

HelloWorld 方法里定义了一个call方法,call方法只有一个参数env, 方法会return一个数组,里面包含了http请求的response. 运行一下这个文件看看:

1
2
3
4
$ ruby myserver.rb
[2012-08-24 12:24:28] INFO  WEBrick 1.3.1
[2012-08-24 12:24:28] INFO  ruby 1.9.3 (2012-04-20) [x86_64-darwin11.4.0]
[2012-08-24 12:24:28] INFO  WEBrick::HTTPServer#start: pid=17920 port=9000

我们通过 WebBrick的run方法启动WebBrick Server, 并且传递了HelloWorld的实例, 现在端口在9000, 打开网页访问 http://localhost:9000 就会看到页面里显示的是 Hello world!.

rackup

rack 本身就提供了一个 rackup 工具, 我们可以用rackup工具启动一个文件,不过这个文件要是 .ru后缀结尾的,默认名称是config.ru, 这个文件是不是很眼熟, 其实在每个rails项目的根目录下都有这个文件。让我们来写一个config.ru文件

1
run Proc.new {|env| [200, {"Content-Type" => "text/html"}, ["Hello Rack!"]]}

好了,在文件目录里运行一下

1
2
3
4
$ rackup config.ru
[2012-08-24 14:56:34] INFO  WEBrick 1.3.1
[2012-08-24 14:56:34] INFO  ruby 1.9.3 (2012-04-20) [x86_64-darwin11.4.0]
[2012-08-24 14:56:34] INFO  WEBrick::HTTPServer#start: pid=18286 port=9292

这回是绑定在9292端口,rack的默认端口上。

middleware

rack 的middleware 提供对任何请求的filter功能,比如我们可以为每次请求记录log, 给每个html页面里加入一些信息,有了middleware接口, 实现起来就非常方便了,而且我们可以堆叠所有的middleware,每一个middleware处理完后传给下一个,直到没有middleware了才会把结果输出出来, 把上面的例子再改一改,为每次输出里面都增加一个字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class InsertName
    def initialize(app)
      @app = app
    end

    def call(env)
      status, headers, response = @app.call(env)
      [status,headers, [response[0]+ ' jonny']]
    end
end

use InsertName

run Proc.new {|env| [200, {"Content-Type" => "text/html"}, ["Hello Rack!"]]}

重新用rackup启动,访问 localhost:9292, 会看到结果是 Hello Rack! jonny

middleware的应用非常灵活强大,rails的很多功能都是基于rack middleware实现的,在rails项目下输入 rack middleware 会列出项目用到的所有middleware, 下面是我的一个项目的middleware列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
use ActionDispatch::Static
use Rack::Lock
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007facbb14adb0>
use Rack::Runtime
use Rack::MethodOverride
use ActionDispatch::RequestId
use Rails::Rack::Logger
use ActionDispatch::ShowExceptions
use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
use ActionDispatch::Head
use Rack::ConditionalGet
use Rack::ETag
use ActionDispatch::BestStandardsSupport
use Warden::Manager
use OmniAuth::Strategies::Weibo
run Opinion::Application.routes

rack handler

rack handler 是rack链接server的桥梁,rack 库自带了很多server端的handler, 我们用webrick的handler做示例,在第一个例子里我们就是用的handler启动的Webrick server: Rack::Handler::WEBrick.run(HelloWorld.new,:Port => 9000)

  • rack/lib/rack/handler/webrick.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module Rack
  module Handler
    class WEBrick < ::WEBrick::HTTPServlet::AbstractServlet
	  def self.run(app, options={})
        options[:BindAddress] = options.delete(:Host) if options[:Host]
        @server = ::WEBrick::HTTPServer.new(options)
        @server.mount "/", Rack::Handler::WEBrick, app
        yield @server  if block_given?
        @server.start
      end

	  ...

  	end
  end
end

WEBrick类是继承于 WEBrick::HTTPServlet::AbstractServlet 这个类的,在 self.run 方法中首先是 new 了一个 WEBrick::HTTPServer,server的mount方法 加载了WEBrick handler类,接下来server处理完block里面的内容后进行启动,看来所有的操作都是 WEBrick::HTTPServer 类完成的,下面就到这个类的源码里看看。

Webrick是ruby的标准函数库,不在gems目录里,我的机器上在 .rvm/rubies/ruby-1.9.3-p194/lib/ruby/1.9.1/webrick 目录下面:

  • webrick/httpserver.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class HTTPServer < ::WEBrick::GenericServer
    def initialize(config={}, default=Config::HTTP)
      super(config, default)
      @http_version = HTTPVersion::convert(@config[:HTTPVersion])

      @mount_tab = MountTable.new
      if @config[:DocumentRoot]
        mount("/", HTTPServlet::FileHandler, @config[:DocumentRoot],
              @config[:DocumentRootOptions])
      end

      unless @config[:AccessLog]
        @config[:AccessLog] = [
          [ $stderr, AccessLog::COMMON_LOG_FORMAT ],
          [ $stderr, AccessLog::REFERER_LOG_FORMAT ]
        ]
      end

      @virtual_hosts = Array.new
    end

    ...
end

父类GenericServer主要做了一些config初始化,端口绑定的工作,mount serverlet 到 相应的路径。

然后看看 start 方法, httpserver里并没有start方法,而是调用了父类的start:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def start(&block)
      raise ServerError, "already started." if @status != :Stop
      server_type = @config[:ServerType] || SimpleServer

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

        thgroup = ThreadGroup.new
        @status = :Running
        while @status == :Running
          begin
            if svrs = IO.select(@listeners, nil, nil, 2.0)
              svrs[0].each{|svr|
                @tokens.pop          # blocks while no token is there.
                if sock = accept_client(svr)
                  sock.do_not_reverse_lookup = config[:DoNotReverseLookup]
                  th = start_thread(sock, &block)
                  th[:WEBrickThread] = true
                  thgroup.add(th)
                else
                  @tokens.push(nil)
                end
              }
            end
          rescue Errno::EBADF, IOError => ex
            # if the listening socket was closed in GenericServer#shutdown,
            # IO::select raise it.
          rescue Exception => ex
            msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
            @logger.error msg
          end
        end

        @logger.info "going to shutdown ..."
        thgroup.list.each{|th| th.join if th[:WEBrickThread] }
        call_callback(:StopCallback)
        @logger.info "#{self.class}#start done."
        @status = :Stop
      }
end

start方法内通过@status 状态一直做循环监听,如果有请求就进入处理过程,在start_thread方法中调用httpserver 的 run方法, run方法种初始化了 HTTPResponseHTTPRequest 最后传递给了service方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def service(req, res)
  if req.unparsed_uri == "*"
    if req.request_method == "OPTIONS"
      do_OPTIONS(req, res)
      raise HTTPStatus::OK
    end
    raise HTTPStatus::NotFound, "`#{req.unparsed_uri}' not found."
  end

  servlet, options, script_name, path_info = search_servlet(req.path)
  raise HTTPStatus::NotFound, "`#{req.path}' not found." unless servlet
  req.script_name = script_name
  req.path_info = path_info
  si = servlet.get_instance(self, *options)
  @logger.debug(format("%s is invoked.", si.class.name))
  si.service(req, res)
end

servlet 变量就是之前在webrick handler里mount的类名,在这里取回来后通过 servlet.get_instance 创建了 Rack::Handler::WEBrick 实例, 最终实例调用自己的service方法,我们又回到了 webrick handler 里面:

1
2
3
4
5
def service(req, res)
  ...
  status, headers, body = @app.call(env)
  ...
end

在这里我们一开始传进去的app被调用到,并返回对应的内容,到此为止一个request通过rack 再到WebBrick的处理完成.

Comments