2017-10-11

WEBrick::HTTPServerコードリーディング

WEBrick::HTTPServerのコードを読んだので備忘録。rackup時の挙動に関しても書きました。

動かし方

4行くらいでWebサーバとして動いてくれる。
require 'webrick'
server = WEBrick::HTTPServer.new({:DocumentRoot => '/home/username/public_html/',
                               :BindAddress => '127.0.0.1',
                               :Port => 10080})
Signal.trap(:INT){ server.shutdown }
server.start

マウントすることで動的コンテンツも配信可能。

# cgiをマウント
server.mount('/cgi', WEBrick::HTTPServlet::CGIHandler, './cgi.rb')

# procをマウント
server.mount_proc '/proc' do |req, res|
  res.body = 'hogehoge'
end

# rackをマウント
server.mount "/", Rack::Handler::WEBrick, app

コードリーディング

WEBrick::HTTPServerはWEBrick::GenericServerを継承しています。コンストラクタでMountTableが初期化され、DocumentRootが引数に設定されている場合、HTTPServlet::FileHandlerがルートパスにマウントされます。
module WEBrick
  class HTTPServer < ::WEBrick::GenericServer
    def initialize(config={}, default=Config::HTTP)
      # snip...

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

mountメソッドは@mount_tabにディレクトリパスをキーとしてサーブレットとオプションを保存します

def mount(dir, servlet, *options)
  @logger.debug(sprintf("%s is mounted on %s.", servlet.inspect, dir))
  @mount_tab[dir] = [ servlet, options ]
end

WEBrick::GenericServer#startでは実際のTCP、HTTPのハンドリングが行われます。

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

  setup_shutdown_pipe

  server_type.start{
    # snip...
    shutdown_pipe = @shutdown_pipe

    thgroup = ThreadGroup.new
    @status = :Running
    begin
      while @status == :Running
        begin
          sp = shutdown_pipe[0]
# snip...

@config[:ServerType]がnilの場合は、SimpleServerが使われます。SimpleServer#startは引数として与えられたブロックを単純にyieldするだけの責務を持っています。

setup_shutdown_pipeではshutdown用のpipeを作成しています

def setup_shutdown_pipe
  return @shutdown_pipe ||= IO.pipe
end

この@shutdown_pipeは文字通りシャットダウン用のパイプになるので、外部からシャットダウンをする場合にはこの@shutdown_pipeのwrite側に"\0"を書き込むことでサーバを止めれるようになります。

例えばINTシグナルとトラップしてserver.shutdownするように指示することでINTシグナルでサーバが止まるようになります。

Signal.trap(:INT){ server.shutdown }

shutdownメソッドは内部的に@shutdown_pipeのwrite側に"\0"を書きこんでいます。

alarm_shutdown_pipe {|f| f.write_nonblock("\0")}

サーバの処理はこんな感じで、IO.selectでI/O多重化しつつshutdownのread側とlistenポートを監視します。

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

@listenersはコンストラクタ内で呼び出されるlistenメソッドによってセットされており、TCPのソケットを生成しています。

def listen(address, port)
  @listeners += Utils::create_listeners(address, port)
end

def create_listeners(address, port)
  # snip...
  sockets = Socket.tcp_server_sockets(address, port)
  sockets = sockets.map {|s|
    s.autoclose = false
    ts = TCPServer.for_fd(s.fileno)
    s.close
    ts
  }
  return sockets
end

shutdownのread側へwriteが来たらループを抜けます。そうでない場合は、TCP接続のハンドリングしつつ、HTTPのリクエストを処理します。svrs[0]は読取り待ちのファイルディスクリプタの配列が入ります。@tokensはMaxClient制御用のトークンでMaxClientを越えるアクセスが来たときに適切にブロックするための制御用のスタックです。制御用なので値にはnilしか入りません。

accept_clientで接続要求を取り出し、そのソケットをstart_threadで処理します。

def start_thread(sock, &block)
  Thread.start{
    begin
      # snip...
      block ? block.call(sock) : run(sock)

server#startでブロックが渡されない場合は、run(sock)が呼ばれます。runはWEBrick::HTTPServerで定義されています。

    def run(sock)
      while true
        res = HTTPResponse.new(@config)
        req = HTTPRequest.new(@config)
        server = self
        begin
          timeout = @config[:RequestTimeout]
          while timeout > 0
            break if sock.to_io.wait_readable(0.5)
            break if @status != :Running
            timeout -= 0.5
          end
          raise HTTPStatus::EOFError if timeout <= 0 || @status != :Running
          raise HTTPStatus::EOFError if sock.eof?
          req.parse(sock)
          res.request_method = req.request_method
          res.request_uri = req.request_uri
          res.request_http_version = req.http_version
          res.keep_alive = req.keep_alive?
          server = lookup_server(req) || self
          if callback = server[:RequestCallback]
            callback.call(req, res)
          elsif callback = server[:RequestHandler]
            msg = ":RequestHandler is deprecated, please use :RequestCallback"
            @logger.warn(msg)
            callback.call(req, res)
          end
          server.service(req, res)
# snip...
        ensure
          if req.request_line
            if req.keep_alive? && res.keep_alive?
              req.fixup()
            end
            res.send_response(sock)
            server.access_log(@config, req, res)
          end
        end
# snip...

やっていることをざっくり説明すると以下になります。

HTTPServer#serviceはsearch_servletでパスに応じたservletを@mount_tabから取り出します。取り出したservletをインスタンス化してserviceメソッドを呼び出し、処理をservletに委譲します。
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

WEBrick::HTTPServlet::FileHandlerの場合は 最終的にWEBrick::HTTPServlet::DefaultFileHandler#do_GETが呼び出されます

def do_GET(req, res)
  st = File::stat(@local_path)
  mtime = st.mtime
  res['etag'] = sprintf("%x-%x-%x", st.ino, st.size, st.mtime.to_i)

  if not_modified?(req, res, mtime, res['etag'])
    res.body = ''
    raise HTTPStatus::NotModified
  elsif req['range']
    make_partial_content(req, res, @local_path, st.size)
    raise HTTPStatus::PartialContent
  else
    mtype = HTTPUtils::mime_type(@local_path, @config[:MimeTypes])
    res['content-type'] = mtype
    res['content-length'] = st.size
    res['last-modified'] = mtime.httpdate
    res.body = open(@local_path, "rb")
  end
end

 

RackアプリでアプリサーバとしてWEBrickを使う場合

rackupでwebrickを指定した場合は、Rack::Handler::WEBrick.runが呼び出されます。このメソッド内で::WEBrick::HTTPServerがRack::Handler::WEBrickをマウントした状態で起動します。
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

RackではCGI環境変数(env)を引数に渡す必要があります。Rack::Handler::WEBrick#serviceではHTTPRequest#meta_varsでCGI 1.1形式のハッシュをenvに設定しつつ、Rackアプリケーションのcallメソッドをenv引数付きで渡しています。

def service(req, res)
  res.rack = true
  env = req.meta_vars
  env.delete_if { |k, v| v.nil? }

  rack_input = StringIO.new(req.body.to_s)
  rack_input.set_encoding(Encoding::BINARY)

  env.update(
    RACK_VERSION      => Rack::VERSION,
    RACK_INPUT        => rack_input,
    # snip...
  )

  env[HTTP_VERSION] ||= env[SERVER_PROTOCOL]
  env[QUERY_STRING] ||= ""
  unless env[PATH_INFO] == ""
    path, n = req.request_uri.path, env[SCRIPT_NAME].length
    env[PATH_INFO] = path[n, path.length-n]
  end
  env[REQUEST_PATH] ||= [env[SCRIPT_NAME], env[PATH_INFO]].join

  status, headers, body = @app.call(env)
# snip...

call後はHTTPResponseにstatus, headers, bodyの内容を元にプロパティをセットします。

 

このエントリーをはてなブックマークに追加