23
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

RubyAdvent Calendar 2020

Day 9

たのしいOSSコードリーディング: Let’s read WEBrick🏠

Last updated at Posted at 2020-12-08

Ruby Advent Calendar 2020 9日目の記事です。昨日はmtsmfmさんのensure は実行されるとは限らないでした。

Let’s read WEBrick🏠

この記事では、Ruby製HTTPサーバーフレームワークであるWEBrickのソースコードリーディングを行います。

経緯

去る2020年11月26日、地域RubyコミュニティTama.rbの公開イベントとしてTama.rb OSSコードリーディング部 WEBrick編を開催しました。
こちらは、原作者の一人である高橋征義さん @takahashim をお招きしてソースコードをわいわい読み、その様子をZoomでライブ配信するという試みでした。

当日学んだWEBrick内部の全体の処理の流れを振り返りつつ整理していきたいと思います。
長い記事ですが、どうぞお付き合いください🙏

WEBrickとは?

汎用HTTPサーバーフレームワークです。HTTPサーバが簡単に作れます。

現在はCRuby本体に同梱のHTTPサーバーツールキットとして知られているWEBrickですが、高橋さんにお話を伺ったところ、元々は2000年頃雑誌の連載のために開発されたプログラムだったそう。
現在のWEBrickの骨格は開発当時とほとんど変わっていないそうで、開発当初から綺麗な設計を目指していたことが伺えます。

WEBrick はサーブレットによって機能します。サーブレットとはサーバの機能をオブジェクト化したものです。ファイルを読み込んで返す・forkしてスクリプトを実行する・テンプレートを適用するなど、「サーバが行なっている様々なこと」を抽象化しオブジェクトにしたものがサーブレットです。サーブレットは WEBrick::HTTPServlet::AbstractServlet のサブクラスのインスタンスとして実装されます。

WEBrickの一番の特徴は、何と言ってもサーブレット(サーバー機能)と組み合わせることで簡単にHTTPサーバーを構築できてしまうという点でしょう。

以下はるりまに掲載されているサンプルコード(抜粋)です。
わずかな行数ですが、サーバープロセスとCGIを扱うためのサーブレットを結びつけてHTTPサーバーを構築し、起動しています。

require 'webrick'
srv = WEBrick::HTTPServer.new({ :DocumentRoot => './',
                                :BindAddress => '127.0.0.1',
                                :Port => 20080 })
srv.mount('/view.cgi', WEBrick::HTTPServlet::CGIHandler, 'view.rb')
srv.start
  1. サーバのパス /view.cgi と CGIHandler がマウントにより結びつけられます。
  2. パス /view.cgi にアクセスがあるたびにサーバは 'view.rb' を引数として CGIHandler オブジェクトを生成します。
  3. サーバはリクエストオブジェクトを引数として CGIHandler#service メソッドを呼びます。
  4. CGIHandler オブジェクトは view.rb を CGI スクリプトとして実行します。

このコード例では、

① サーバーインスタンスの生成
WEBrick::HTTPServerクラスのインスタンスをつくる)

② サーブレットのマウント
WEBrick::HTTPServerクラスのインスタンスにおいて、パス'/view.cgi'WEBrick::HTTPServlet::CGIHandlerクラスを結びつける)

③ サーバーの起動
WEBrick::HTTPServerクラスのインスタンスに対してWEBrick::HTTPServer#startを呼ぶ)

④ パスに対してリクエストが発生
(クライアントが'/view.cgi'にアクセスする)

⑤ サーブレットが処理を実行
WEBrick::HTTPServlet::CGIHandlerクラスがインスタンスを作り、WEBrick::HTTPServlet::CGIHandler#serviceメソッドを呼ぶ)

という流れでHTTPサーバーを構築し、実際に処理を行っています。

ちなみに、サーブレットはWEBrickに組み込まれているもの(Ex.WEBrick::HTTPServlet::CGIHandler)だけでなく、抽象サーブレットクラスであるWEBrick::HTTPServlet::AbstractServletを継承することによってオリジナルのサーブレットを記述することができるようになっています。便利!

それでは、このサンプルコードの流れに沿ってWEBrickのコードを読んでいきましょう!

対象

この記事では、 ruby/webrickリポジトリ https://github.com/ruby/webrick より、
2020年12月2日時点でのmasterブランチを参照しました。

❗️注意❗️

今記事では、上記のサンプルコードの流れに沿ってコードリーディングを進めていきますが、都合上必要なソースコードの該当箇所のみを抜粋してご紹介します。
場合により元のコードを大幅に割愛した上掲載している箇所がありますので、予めご了承ください。

サンプルコード

require 'webrick'
srv = WEBrick::HTTPServer.new({ :DocumentRoot => './',
                                :BindAddress => '127.0.0.1',
                                :Port => 20080 })
srv.mount('/view.cgi', WEBrick::HTTPServlet::CGIHandler, 'view.rb')
srv.start

① サーバーインスタンスの生成

まずはサンプルコードの一行目をご覧ください。

srv = WEBrick::HTTPServer.new({ :DocumentRoot => './',
                                :BindAddress => '127.0.0.1',
                                :Port => 20080 })

ここではWEBrick::HTTPServerクラスのインスタンスを生成しています。
WEBrick::HTTPServer#initializeを読んでみましょう。

module WEBrick
  class HTTPServer < ::WEBrick::GenericServer
    def initialize(config={}, default=Config::HTTP)
      super(config, default)
      # ...

いきなりですがWEBrick::HTTPServerクラスはWEBrick::GenericServerクラスを継承しており、superでスーパークラスの#initializeを呼んでいます。

またこのとき、
引数configには{ :DocumentRoot => './', :BindAddress => '127.0.0.1', :Port => 20080 }
引数defaultにはConfig::HTTPが渡されています。

先にConfig::HTTPを確認しておきましょう。

module WEBrick
  module Config
    # ...
    HTTP = General.dup.update(
      :Port           => 80,
      :RequestTimeout => 30,
      :HTTPVersion    => HTTPVersion.new("1.1"),
      :AccessLog      => nil,
      :MimeTypes      => HTTPUtils::DefaultMimeTypes,
      :DirectoryIndex => ["index.html","index.htm","index.cgi","index.rhtml"],
      :DocumentRoot   => nil,
     # ...
    )
    # ...

ここで#dupのレシーバーになっているWEBrick::Config::Generalも見てみましょう。

    General = Hash.new { |hash, key|
      case key
      when :ServerName
        hash[key] = Utils.getservername
      else
        nil
      end
    }.update(
      :BindAddress    => nil,   # "0.0.0.0" or "::" or nil
      :Port           => nil,   # users MUST specify this!!
      :MaxClients     => 100,   # maximum number of the concurrent connections
      :ServerType     => nil,   # default: WEBrick::SimpleServer
      :Logger         => nil,   # default: WEBrick::Log.new
      :ServerSoftware => "WEBrick/#{WEBrick::VERSION} " +
                         "(Ruby/#{RUBY_VERSION}/#{RUBY_RELEASE_DATE})",
      :TempDir        => ENV['TMPDIR']||ENV['TMP']||ENV['TEMP']||'/tmp',
      :DoNotListen    => false,
      :StartCallback  => nil,
      :StopCallback   => nil,
      :AcceptCallback => nil,
      :DoNotReverseLookup => true,
      :ShutdownSocketWithoutClose => false,
    )

どうやら、サーバーの動作設定のために必要な情報が格納されたハッシュのようです。

続けて、これらの引数に取るWEBrick::GenericServer#initializeのコードを確認します。

class WEBrick
  # ...
  class GenericServer
    def initialize(config={}, default=Config::General)
      @config = default.dup.update(config)
      # ...
      @listeners = []
      # ...
      unless @config[:DoNotListen]
        # ...
        listen(@config[:BindAddress], @config[:Port])
        # ...
      end
    end
    # ...

引数defaultWEBrick::Config::Generalが入っていますが、ここはサブクラスのWEBrick::HTTPServer#initializeWEBrick::Config::HTTPによって上書きされています。

それを踏まえると、インスタンス変数@configにはWEBrick::Config::HTTPdupし、{ :DocumentRoot => './', :BindAddress => '127.0.0.1', :Port => 20080 }で上書きしたハッシュが代入されていることになります。

また、インスタンス変数@listenersには空配列が代入されています。

ここではそれ以外にも色々な設定事項が記述されていますが、一番大事なのは最後のlisten(@config[:BindAddress], @config[:Port])です。

#listenは同じWEBrick::GenericServerクラスに実装されています。

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

インスタンス変数@listenersは空配列でした。
ここにWEBrick::Utils.create_listenersの返り値の配列を代入しています。
WEBrick::Utils.create_listenersの実装は次の通りです。

module WEBrick
  module Utils
    # ...
    def create_listeners(address, port)
      # ...
      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
    # ...

ここで、面白いことをしています。

sockets = Socket.tcp_server_sockets(address, port)という行において、Socket.tcp_server_socketsの返り値は、IPv4とIPv6のソケットを一つずつ格納した配列です。

この配列の要素である二つのソケットは、いずれも汎用的なソケットの概念を扱うSocketクラスのインスタンスとなっています。

しかしRubyは汎用的なソケットの概念を扱うSocketクラス以外にも、TCP/IPストリーム型接続のサーバーに特化したソケットの概念を扱うTCPServerクラスを持っています。
いずれも同じソケットを扱うクラスではありますが、WEBrickのようなHTTPサーバーの概念を扱う場合には、後者の方が都合が良いのです。

そのため、ここではSocket.tcp_server_socketsの返り値であるSocketクラスのインスタンスの配列をmapで回し、Socketクラスのオブジェクトを元にTCPServerクラスのオブジェクトを生成しています。すごい。

ということで、WEBrick::Utils.create_listenersの返り値は、それぞれIPv4とIPv6に対応する二つのTCPServerクラスのオブジェクトを要素に持つ配列、ということになります。

WEBrick::GenericServer#listenに戻りましょう。

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

インスタンス変数@listenersには次のような配列が格納されています。

[IPv4に対応したTCPServerクラスのインスタンス, IPv6に対応したTCPServerクラスのインスタンス]

それではWEBrick::GenericServer#initializeを読み終えたので、最初に戻ってWEBrick::HTTPServer#initializeを読んでみます。

module WEBrick
  class HTTPServer < ::WEBrick::GenericServer
    def initialize(config={}, default=Config::HTTP)
      super(config, default)
      # ...
      @mount_tab = MountTable.new
      # ...
    end

@mount_tab = MountTable.newという行が登場しました。
同じファイルに記述されているWEBrick::HTTPServer::Mountable#initializeを読んでみます。

module WEBrick
  class HTTPServer < ::WEBrick::GenericServer
    # ...
    class MountTable # :nodoc:
      def initialize
        @tab = Hash.new
        compile
      end

@tabには空ハッシュが代入されています。
compileが気になりますね。
WEBrick::HTTPServer::MountTable#compileを読んでみましょう。

module WEBrick
  class HTTPServer < ::WEBrick::GenericServer
    # ...
    class MountTable # :nodoc:
      # ...
      def compile
        k = @tab.keys
        k.sort!
        k.reverse!
        k.collect!{|path| Regexp.escape(path) }
        @scanner = Regexp.new("\\A(" + k.join("|") +")(?=/|\\z)")
      end

先ほどのWEBrick::HTTPServer::Mountable#initializeにて、@tabは空ハッシュでした。
@tab.keysの返り値は空配列なので、一旦ここでは@scanner/\A()(?=\/|\z)/が入ることになります。
このWEBrick::HTTPServer::MountTable#compileはすぐ後にまた登場します。

ここまでが ① サーバーインスタンスの生成 のコードになります。

まとめ

ここまでで行われた処理は以下の通りです。

  • WEBrick::HTTPServerクラスのインスタンスを生成
  • WEBrick::HTTPServerクラスのインスタンスはインスタンス変数@config@listeners@mount_tabを持つ
    • @config ➡️ 様々なサーバーの設定項目
    • @listeners ➡️ それぞれIPv4/IPv6対応の二つのTCPServerインスタンスからなる配列
    • @mount_tab ➡️ WEBrick::HTTPServer::Mountableクラスのインスタンス
      • @mount_tabはインスタンス変数@tab@scannerを持つ
        • @tab ➡️ {}
        • @scanner ➡️ /\A()(?=\/|\z)/

② サーブレットのマウント

続いて、サンプルコードよりこの行の実装を読んでみます。
(ちなみに、変数srvWEBrick::HTTPServerクラスのインスタンスでしたね)

srv.mount('/view.cgi', WEBrick::HTTPServlet::CGIHandler, 'view.rb')

#mountメソッドはWEBrick::HTTPServerクラスに実装されています。

module WEBrick
  class HTTPServer < ::WEBrick::GenericServer
    # ...
    def mount(dir, servlet, *options)
      # ...
      @mount_tab[dir] = [ servlet, options ]
    end

引数dirには'/view.cgi'
引数servletにはWEBrick::HTTPServlet::CGIHandler
引数optionsには'view.rb'が渡されています。

インスタンス変数@mount_tabにはWEBrick::HTTPServer::MountTableクラスのインスタンスが入っているのでした。
これに対して、#[]=を呼び出しています。

WEBrick::HTTPServer::MountTable#[]=を見てみましょう。

module WEBrick
  class HTTPServer < ::WEBrick::GenericServer
    # ...
    class MountTable # :nodoc:
      # ...
      def []=(dir, val)
        dir = normalize(dir)
        @tab[dir] = val
        compile
        val
      end

      # ...

      def normalize(dir)
        ret = dir ? dir.dup : ""
        ret.sub!(%r|/+\z|, "")
        ret
      end

引数dirには'/view.cgi'
引数valには[WEBrick::HTTPServlet::CGIHandler, 'view.rb']が渡されています。

まずdir = normalize(dir)で、すぐ下にあるWEBrick::HTTPServer::MountTable#normalizeメソッドを呼び出しています。
ここで、引数dirに格納されている'/view.cgi'に対して、末尾に/がある場合は空文字''に置き換えます(なので今回は対象外)

その後@tab[dir] = valで、インスタンス変数@tab(この時点では空ハッシュ)に対し、'/view.cgi'をキーとして値[WEBrick::HTTPServlet::CGIHandler, 'view.rb']を追加しています。

結果、@tabはこのようなハッシュになります。

{ '/view.cgi' => [WEBrick::HTTPServlet::CGIHandler, 'view.rb'] }

次に、再びWEBrick::HTTPServer::MountTable#compileを呼んでいます。

module WEBrick
  class HTTPServer < ::WEBrick::GenericServer
    # ...
    class MountTable # :nodoc:
      # ...
      def compile
        k = @tab.keys
        k.sort!
        k.reverse!
        k.collect!{|path| Regexp.escape(path) }
        @scanner = Regexp.new("\\A(" + k.join("|") +")(?=/|\\z)")
      end

この時、k = @tab.keysで変数k['/view.cgi']が代入されます。
配列の要素が一つしかないので次の二行は飛ばします。
k.collect!{|path| Regexp.escape(path) }が呼ばれると、変数'view.cgi'は次のような配列に変わります。

["view\\.cgi"]

最後の行Regexp.new("\\A(" + k.join("|") +")(?=/|\\z)")を呼ぶと、インスタンス変数@scannerには次のようなRegexpオブジェクトが格納されます。

/\A(\/view\.cgi)(?=\/|\z)/

WEBrick::HTTPServer::MountTable#[]=に戻ります。
最後の行でval(= [WEBrick::HTTPServlet::CGIHandler, 'view.rb'])を返してこのメソッドは終了です。


module WEBrick
  class HTTPServer < ::WEBrick::GenericServer
    # ...
    class MountTable # :nodoc:
      # ...
      def []=(dir, val)
        dir = normalize(dir)
        @tab[dir] = val
        compile
        val
      end

ここまでが ② サーブレットのマウント のコードです。

まとめ

ここまでで行われた処理は以下の通りです。

  • @mount_tab(= WEBrick::HTTPServer::MountTableクラスのインスタンス)の持つインスタンス変数が次のように変化
    • @tab ➡️ { '/view.cgi' => [WEBrick::HTTPServlet::CGIHandler, 'view.rb'] }
    • @scanner ➡️ /\A(\/view\.cgi)(?=\/|\z)/

③ サーバーの起動

続いて読むのは、サンプルコードの最後の一行の部分です。

srv.start

srv、つまりWEBrick::HTTPServerクラスのインスタンスに#startメソッドを呼んでいます。
この一行を実行すると、HTTPサーバーが起動することになります。

早速コードを読んでみましょう。と言いたいところですが、実は#startメソッドはWEBrick::HTTPServerクラスには実装されていません。
WEBrick::HTTPServerクラスのスーパークラスであるWEBrick::GenericServerクラスに実装されています。

WEBrick::GenericServer#startは非常に長いメソッドなので、先に全容を掲載しておきます。
その後、今回コードリーディングを行う箇所に限定して少しずつ読んでいきます。

class 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]}"
        @status = :Running
        call_callback(:StartCallback)

        shutdown_pipe = @shutdown_pipe

        thgroup = ThreadGroup.new
        begin
          while @status == :Running
            begin
              sp = shutdown_pipe[0]
              if svrs = IO.select([sp, *@listeners])
                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
            rescue Errno::EBADF, Errno::ENOTSOCK, IOError => ex
              # if the listening socket was closed in GenericServer#shutdown,
              # IO::select raise it.
            rescue StandardError => ex
              msg = "#{ex.class}: #{ex.message}\n\t#{ex.backtrace[0]}"
              @logger.error msg
            rescue Exception => ex
              @logger.fatal ex
              raise
            end
          end
        ensure
          cleanup_shutdown_pipe(shutdown_pipe)
          cleanup_listener
          @status = :Shutdown
          @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
      }
    end

それでは、具体的な内容を少しずつ読んでいきます。

class WEBrick
  # ...
  class GenericServer
    # ...
    def start(&block)
      # ...
      server_type = @config[:ServerType] || SimpleServer

今回引数にブロックは渡していないので無視します。
@config[:ServerType]nilなので、変数server_typeにはWEBrick::SimpleServerクラスが代入されます。

WEBrick::SimpleServerクラスはWEBrick::GenericServerクラスと同じファイルに記述されています。

module WEBrick
  # ...
  class SimpleServer

    ##
    # A SimpleServer only yields when you start it

    def SimpleServer.start
      yield
    end
  end

シンプル!
コメントに記載の通り、WEBrick::SimpleServerクラスには、渡されたブロックを実行するだけのクラスメソッド.startが実装されているのみです。

WEBrick::GenericServer#startメソッドに戻ると、この後は次のような記述が続きます。

    def start(&block)
      # ...
      server_type = @config[:ServerType] || SimpleServer
      # ...
      server_type.start{
        # ...
      }
    end

WEBrick::GenericServer#startメソッドは、先ほどのWEBrick::SimpleServerクラスに対して.startメソッドを呼び、ブロックの中の処理を実行しているだけのメソッドであるということがわかります。
ブロックの中の処理を見てみましょう。

      server_type.start{
        # ...
        @status = :Running
        # ...
        begin
          while @status == :Running
            begin
              # ...
              if svrs = IO.select([sp, *@listeners])
                # ...

インスタンス変数@status:Runningを代入し、それを条件として、「クライアントからのリクエストを待つ→リクエストを処理する」というループに入ります。

下から二番目の行の条件分岐でspという変数が唐突に登場しています。都合上今回は紹介しませんが、シャットダウンの仕組みに関わるとても重要で面白い概念なので、興味がある方はぜひ読んでみてください。
(@shutdown_pipeWEBrick::GenericServer#setup_shutdown_pipe辺りです)

さて条件文を見ると、IO.selectの返り値を確認しています。

IO.selectの詳しい解説はるりまに譲りますが、今回、このIO.selectの引数には[sp, *@listeners]という配列が渡されています。
この配列に格納されている各要素は、いずれも「クライアントからの接続要求待ち」のソケットオブジェクトです。

@listenersの中身が何だったか覚えていますか?
そう、IPv4とIPv6のそれぞれソケットオブジェクト(TCPServerクラスのインスタンス)からなる配列でした。

この場合splatされているので、実際引数に渡されているのは次のような配列です。

[sp, IPv4のソケットオブジェクト, IPv6のソケットオブジェクト]

この配列の各要素のうち、いずれかのソケットがクライアントからの接続要求を感知した時、IO.selectは「①当該ソケットオブジェクトを要素に持つ配列②空配列(1)③空配列(2)」という三つの要素を持つ二次元配列を返します。
つまり、上記の変数svrsには次のような配列が代入されます。

[[接続要求を感知したソケットオブジェクト], [], []]

なぜこのような形の返り値になるのか、については(面白いのですが)割愛します。
上記のるりまのIO.selectの項やUnixのシステムコールselect(2)のマニュアルの他、書籍「Working with TCP Sockets Chapter12」に詳しく解説されていますので、ご興味のある方にはお勧めです。
なお、今回のケースでは後ろの二つの空配列を気にする必要はありません。

さて、ここで改めて先ほどの条件文を見てみましょう。

              if svrs = IO.select([sp, *@listeners])

IO.selectはブロッキング処理です。
つまり、IO.selectが「接続要求を感知したソケットオブジェクト」を返すまで、ここで一旦処理が止まることになります。

では、この条件文はいつtrueになるのでしょう?そうです、クライアントからのリクエストが到着したときです。

なので、ちょっと中途半端ですが ③ サーバーの起動 は一旦ここまでにして、この先のコードは次の処理 ④ パスに対してリクエストが発生 の中で読んでいくことにしましょう。

まとめ

ここまでで行われた処理は以下の通りです。

  • SimpleServer.startの実行
    • 「クライアントからのリクエストを待つ→リクエストを処理する」というループに入る
    • ソケットオブジェクトがクライアントからのリクエストを待つ

④ パスに対してリクエストが発生

次の条件文にて、IO.selectが引数に渡したソケットオブジェクトのうち、いずれかがクライアントからの接続要求を感知するまでは処理が止まる、と説明しました。

              if svrs = IO.select([sp, *@listeners])

これに対して、るりまのサンプルコードには次のような解説がついてます。

  1. サーバのパス /view.cgi と CGIHandler がマウントにより結びつけられます。
  2. パス /view.cgi にアクセスがあるたびにサーバは 'view.rb' を引数として CGIHandler オブジェクトを生成します。

ここで注目すべきは2の方です。
ここから先の処理を読むにあたり、記載の通り「クライアントからパス/view.cgiにアクセスがあり、その結果ソケットオブジェクトが接続要求を感知した」と仮定しましょう。

それでは、条件分岐の先に進みます。

              if svrs = IO.select([sp, *@listeners])
                svrs[0].each{|svr|
                  # ...
                  if sock = accept_client(svr)
                    # ...
                    th = start_thread(sock, &block)
                    # ...
                  else
                    # ...
                  end
                }
                # この後はエラー処理と終了処理が続く

svrs[[接続要求を感知したソケットオブジェクト], [], []]という二次元配列です。
なので、svrs[0][接続要求を感知したソケットオブジェクト]という配列です。

この配列に対してeachを呼んでいます。
ブロック変数svrは配列要素の接続要求を感知したソケットオブジェクトそのものです。

その次、if sock = accept_client(svr)という行ではWEBrick::GenericServer#accept_clientメソッドを呼んでいます。
WEBrick::GenericServer#accept_clientメソッドの実装を読んでみましょう。

class WEBrick
  # ...
  class GenericServer
    # ...
    def accept_client(svr)
      case sock = svr.to_io.accept_nonblock(exception: false)
      when :wait_readable
        nil
      else
        # ...
        sock
      end
    # ...

引数svrは先ほどの「接続要求を感知したソケットオブジェクト」です。
この「接続要求を感知したソケットオブジェクト」はTCPServerクラスのインスタンスなのでした。

これに対してTCPServer#accept_nonblockメソッドを呼んでいます。

TCPServer#accept_nonblockメソッドは、レシーバーのソケットオブジェクトをノンブロッキングモードに設定した後、クライアントからの接続要求を受け付けます。
無事に接続が完了した場合は、実際にクライアントと接続済みのソケットオブジェクト(先ほど接続要求を感知したソケットオブジェクトとは"別の"TCPSocketのインスタンス)を返します。

変数sockには、この接続済みソケットオブジェクトが代入されます。
この後case文はelseに処理が流れ、最終的には変数sock(つまり接続済みソケットオブジェクト)が返されます。

WEBrick::GenericServer#startメソッドに戻りましょう。

                  if sock = accept_client(svr)
                    # ...
                    th = start_thread(sock, &block)
                    # ...
                  else
                # ...

変数sockには先ほどの接続済みソケットオブジェクトが入っています。

次の行では、これをWEBrick::GenericServer#start_threadメソッドの引数に渡し、その返り値を変数thに代入しています。
(ちなみに、今回ブロックは渡していないので引数のblockは存在しません)

WEBrick::GenericServer#start_threadメソッドを読んでみましょう。

class WEBrick
  # ...
  class GenericServer
    # ...
    def start_thread(sock, &block)
      Thread.start{
        begin
          Thread.current[:WEBrickSocket] = sock
          # ...
          block ? block.call(sock) : run(sock)
          # この後はエラー処理と終了処理が続く
        end
      }
    end

引数sockは接続できたTCPSocketのインスタンス、
引数blockは空です。

ここではThread.startを呼んでいます。

これによってここで新しいスレッドを生成し、その後の処理はこのスレッドの中(つまり、Thread.startのブロックの中)で行われるようになります。

ブロックの中を見てみましょう。
Thread.current[:WEBrickSocket] = sockで、現在のスレッドに固有データのキーWEBrickSocketを与え、その値に接続済みソケットオブジェクトを与えています。

次の行、block ? block.call(sock) : run(sock)において、blockは空なので、run(sock)にフォールバックします。

WEBrick::GenericServer#runメソッドを見てみましょう。

class WEBrick
  # ...
  class GenericServer
    # ...
    def run(sock)
      @logger.fatal "run() must be provided by user."
    end

おっと、#runメソッドの実態はWEBrick::GenericServerクラスには実装されていないようです。

ここで、そもそも最初にWEBrick::GenericServer#startが呼び出されたレシーバーのクラスであるWEBrick::HTTPServerクラスを見てみましょう。
WEBrick::HTTPServerクラスの方に#runメソッドが定義されています。

module WEBrick
  class HTTPServer < ::WEBrick::GenericServer
    # ...
    def run(sock)
      while true
        req = create_request(@config)
        res = create_response(@config)
        server = self
        begin
          # ...
          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.service(req, res)
          # この後はエラー処理・終了処理・その他の処理が続く

引数sockは接続済みソケットオブジェクトです。

長いので詳しくは見ませんが、ざっくりいうと
変数reqにはHTTPリクエストの概念を扱うWEBrick::HTTPRequestクラスのインスタンス、
変数resにはHTTPレスポンスの概念を扱うWEBrick::HTTPResponseクラスのインスタンス、
変数serverにはWEBrick::HTTPServerクラスのインスタンス自身(サンプルコードの変数srvと同じ)が代入されます。

ここで接続済みソケットオブジェクトからリクエストの情報が読み取られ、クライアントに返すレスポンスの概念が構築されていきます。

そして、最後にserver.service(req, res)が実行されます。
WEBrick::HTTPServer#serviceメソッドを読んでみましょう。

module WEBrick
  class HTTPServer < ::WEBrick::GenericServer
    # ...
    def service(req, res)
      # ...
      servlet, options, script_name, path_info = search_servlet(req.path)
      # ...
      si = servlet.get_instance(self, *options)
      # ...
      si.service(req, res)
    end

まずはWEBrick::HTTPServer#service#search_servletメソッドの内容を確認しましょう。

module WEBrick
  class HTTPServer < ::WEBrick::GenericServer
    # ...
    def search_servlet(path)
      script_name, path_info = @mount_tab.scan(path)
      servlet, options = @mount_tab[script_name]
      if servlet
        [ servlet, options, script_name, path_info ]
      end
    end

引数pathはリクエストパスである'view.cgi'です。

ここでインスタンス変数@mount_tabが再登場します。
インスタンス変数@mount_tabの中身はWEBrick::HTTPServer::Mountableクラスのインスタンスでした。

WEBrick::HTTPServer::Mountable#scanメソッドを見てみましょう。

module WEBrick
  class HTTPServer < ::WEBrick::GenericServer
    # ...
    class MountTable # :nodoc:
      # ...
      def scan(path)
        @scanner =~ path
        [ $&, $' ]
      end

引数pathの中身はリクエストパス'/view.cgi'
インスタンス変数@scannerの中身は/\A(\/view\.cgi)(?=\/|\z)/でした。

ここでは、リクエストパス'/view.cgi'/\A(view\.cgi)(?=\/|\z)/の正規表現マッチを行い、次のような配列を返しています。

['/view.cgi'(最後にマッチした文字列), ''(最後にマッチした文字列より後ろの文字列)]

WEBrick::HTTPServer#service#search_servletに戻ると、
変数script_name'/view.cgi'
変数path_infoに空文字列''が格納されていることがわかります。

module WEBrick
  class HTTPServer < ::WEBrick::GenericServer
    # ...
    def search_servlet(path)
      script_name, path_info = @mount_tab.scan(path)
      servlet, options = @mount_tab[script_name]
      if servlet
        [ servlet, options, script_name, path_info ]
      end
    end

続いて、次の行で呼ばれているのは@mount_tab[script_name]です。

WEBrick::HTTPServer::Mountableクラスには#[]メソッドが定義されています。

module WEBrick
  class HTTPServer < ::WEBrick::GenericServer
    # ...
    class MountTable # :nodoc:
      # ...
      def [](dir)
        dir = normalize(dir)
        @tab[dir]
      end

引数dirは先ほどの'/view.cgi'です。

インスタンス変数@tab{ '/view.cgi' => [WEBrick::HTTPServlet::CGIHandler, 'view.rb'] }というハッシュなので、キー'/view.cgi'を渡すと値である配列[WEBrick::HTTPServlet::CGIHandler, 'view.rb']を返します。

WEBrick::HTTPServer#service#search_servletに戻ると、
変数servletWEBrick::HTTPServlet::CGIHandler
変数options'view.rb'が格納されていることがわかります。

module WEBrick
  class HTTPServer < ::WEBrick::GenericServer
    # ...
    def search_servlet(path)
      script_name, path_info = @mount_tab.scan(path)
      servlet, options = @mount_tab[script_name]
      if servlet
        [ servlet, options, script_name, path_info ]
      end
    end

これで、事前にマウントしておいたサーブレットクラスを取り出すことができました。

servletはtruthyなためにif節の中が評価され、最終的にWEBrick::HTTPServer#service#search_servletの返り値はこのような配列になります。

[WEBrick::HTTPServlet::CGIHandler, 'view.rb', '/view.cgi', '']

それではWEBrick::HTTPServer#serviceに戻ります。

module WEBrick
  class HTTPServer < ::WEBrick::GenericServer
    # ...
    def service(req, res)
      # ...
      servlet, options, script_name, path_info = search_servlet(req.path)
      # ...
      si = servlet.get_instance(self, *options)
      # ...
      si.service(req, res)
    end

先ほどのWEBrick::HTTPServer#service#search_servletの返り値がそのまま変数servletoptionsscript_namepath_infoに格納されています。

次の行servlet.get_instanceのレシーバーはWEBrick::HTTPServlet::CGIHandlerクラスですが、.get_instanceメソッドはそのスーパークラスであるWEBrick::HTTPServlet::AbstractServletクラスに実装されています。

メソッド名を見るからにサーブレットクラスのインスタンスの生成を行なっていそうです。
その下には、サーブレットクラスのインスタンスに#serviceメソッドを呼ぶ行もあります。

ということで、 ④ パスに対してリクエストが発生 で読む処理はここまでとし、続きは最後の ⑤ サーブレットが処理を実行 で読むこととします。

まとめ

ここまでで行われた処理は以下の通りです。

  • 接続要求待ちだったソケットオブジェクトがクライアントからの接続要求を感知
  • 接続要求を感知したソケットオブジェクトが接続要求を受け入れ、接続済みソケットオブジェクトを返す
  • サーバープロセスが新しいスレッドを生成、以降の処理は新しいスレッド内で実行
  • 接続済みソケットオブジェクトからリクエストの情報を読み出し、レスポンスの構築を開始
  • リクエストパス('/view.cgi')を元に、インスタンス変数@mount_tabから事前にマウントしておいたサーブレット(WEBrick::HTTPServlet::CGIHandlerクラス)とサーブレットに渡す引数('view.rb')を取得する

WEBrickコードリーディングもいよいよ大詰めです。

⑤ サーブレットが処理を実行

先ほどのWEBrick::HTTPServer#serviceをもう一度見ておきましょう。

module WEBrick
  class HTTPServer < ::WEBrick::GenericServer
    # ...
    def service(req, res)
      # ...
      servlet, options, script_name, path_info = search_servlet(req.path)
      # ...
      si = servlet.get_instance(self, *options)
      # ...
      si.service(req, res)
    end

最初の行で今回実行するサーブレットを特定することができたので、続いてsi = servlet.get_instance(self, *options)の行を確認していきます。

WEBrick::HTTPServlet::AbstractServlet.get_instanceを見てみましょう。

module WEBrick
  module HTTPServlet
    # ...
    class AbstractServlet
      # ...
      def self.get_instance(server, *options)
        self.new(server, *options)
      end

      # ...

      def initialize(server, *options)
        @server = @config = server
        # ...
        @options = options
      end

      # ...

WEBrick::HTTPServlet::AbstractServlet.get_instanceメソッドは、その名の通り自身のインスタンスを生成するのみの役割を担っています。

そのすぐ下にWEBrick::HTTPServlet::AbstractServlet.initializeメソッドが定義されており、実際にはここでインスタンスが初期化されています。

ところで、元々のコードで.get_instanceメソッドのレシーバーになっていたのはWEBrick::HTTPServlet::AbstractServletクラスのサブクラスであるWEBrick::HTTPServlet::CGIHandlerクラスでした。

WEBrick::HTTPServlet::CGIHandlerクラスの#initializeメソッドも見ておきましょう。

module WEBrick
  module HTTPServlet
    # ...
    class CGIHandler < AbstractServlet
      # ...
      def initialize(server, name)
        super(server, name)
        @script_filename = name
        @tempdir = server[:TempDir]
        interpreter = server[:CGIInterpreter]
        if interpreter.is_a?(Array)
          @cgicmd = CGIRunnerArray + interpreter
        else
          @cgicmd = "#{CGIRunner} #{interpreter}"
        end
      end

      # ...

先ほどスーパークラスであるWEBrick::HTTPServlet::AbstractServletクラスで行なった初期化処理に加え、WEBrick::HTTPServlet::CGIHandlerクラスに特有の初期化処理を行なっているようです。

サーブレットクラスのインスタンス化が終わったので、WEBrick::HTTPServer#serviceメソッドに戻ります。

module WEBrick
  class HTTPServer < ::WEBrick::GenericServer
    # ...
    def service(req, res)
      # ...
      si = servlet.get_instance(self, *options)
      # ...
      si.service(req, res)
    end

最終行で、先ほど生成したWEBrick::HTTPServlet::CGIHandlerクラスのインスタンスに対して#serviceメソッドを呼んでいます。

この#serviceメソッドは、.get_instanceメソッドと同じくWEBrick::HTTPServlet::AbstractServletクラスに実装されています。

module WEBrick
  module HTTPServlet
    # ...
    class AbstractServlet
      # ...
      def service(req, res)
        method_name = "do_" + req.request_method.gsub(/-/, "_")
        if respond_to?(method_name)
          __send__(method_name, req, res)
        else
          raise HTTPStatus::MethodNotAllowed,
                "unsupported method `#{req.request_method}'."
        end
      end

      # ...

      def do_GET(req, res)
        raise HTTPStatus::NotFound, "not found."
      end

引数reqにはWEBrick::HTTPRequestクラスのインスタンス、
引数resにはWEBrick::HTTPResponseクラスのインスタンスが格納されています。

最初の行で呼ばれているreq.request_methodには、HTTPリクエストメソッドを示す文字列が入っています。例えばGETPOSTなどです。

ここでは例示のため、req.request_methodを呼んだ結果'GET'という文字列が返ってきたことにしましょう。
一行目を実行すると、変数method_nameには'do_GET'のような文字列が格納されます。

次の条件分岐で、自身(WEBrick::HTTPServlet::CGIHandlerクラスのインスタンス)がこの文字列 'do_GET'と同じ名前のメソッドを持っているかどうかを確認し、trueの場合はこのメソッドを__send__メソッドで呼び出します。

#do_GETメソッドはそのすぐ下に定義されていますが、その中身はこのWEBrick::HTTPServlet::AbstractServletクラスではなく、元々呼んでいたWEBrick::HTTPServlet::CGIHandlerクラスに実装されています。

WEBrick::HTTPServlet::CGIHandler#do_GETメソッドはこちらです。

module WEBrick
  module HTTPServlet
    # ...
    class CGIHandler < AbstractServlet
      # ...
      def do_GET(req, res)
        cgi_in = IO::popen(@cgicmd, "wb")
        cgi_out = Tempfile.new("webrick.cgiout.", @tempdir, mode: IO::BINARY)
        cgi_out.set_encoding("ASCII-8BIT")
        cgi_err = Tempfile.new("webrick.cgierr.", @tempdir, mode: IO::BINARY)
        cgi_err.set_encoding("ASCII-8BIT")
        begin
          cgi_in.sync = true
          meta = req.meta_vars
          meta["SCRIPT_FILENAME"] = @script_filename
          meta["PATH"] = @config[:CGIPathEnv]
          meta.delete("HTTP_PROXY")
          if /mswin|bccwin|mingw/ =~ RUBY_PLATFORM
            meta["SystemRoot"] = ENV["SystemRoot"]
          end
          dump = Marshal.dump(meta)

          cgi_in.write("%8d" % cgi_out.path.bytesize)
          cgi_in.write(cgi_out.path)
          cgi_in.write("%8d" % cgi_err.path.bytesize)
          cgi_in.write(cgi_err.path)
          cgi_in.write("%8d" % dump.bytesize)
          cgi_in.write(dump)

          req.body { |chunk| cgi_in.write(chunk) }
          # この後は終了処理が続く

引数reqにはWEBrick::HTTPRequestクラスのインスタンス、
引数resにはWEBrick::HTTPResponseクラスのインスタンスが格納されています。

ここまで読んでおいて申し訳ないのですが、(すでにお気づきかとは思いますが)読み手であるわたし自身がCGIの仕組みに明るくなく、具体的にここでどのような処理を行なっているのかの詳細をお伝えすることができません。

ただし、一行目でIO.popenを呼んでいることから、forkして子プロセスでインスタンス変数@cgicmdに格納されたシェルコマンドを実行し、子プロセスの標準入力へつながる入力用パイプ(IOクラスのインスタンス)を変数cgi_inに代入していること、

そして最後の行でチャンク化したリクエストボディをこの子プロセスの標準入力へつながる入力用パイプへ書き込んでいることが伺えます。

WEBrick::HTTPServlet::CGIHandlerクラスには、この#do_GETメソッド以外にも、各HTTPメソッドに対応したインスタンスメソッドが記述されています。
それは他のサーブレットクラスも同じです。
WEBrick::HTTPServlet::AbstractServlet#serviceメソッドは、リクエストメソッドに応じて適切なサーバー処理を行うことができるよう、処理を振り分けていたのです。

まとめ

ここまでで行われた処理は以下の通りです。

  • サーブレットクラスであるWEBrick::HTTPServlet::CGIHandlerクラスのインスタンスを生成
  • WEBrick::HTTPServlet::CGIHandlerクラスのインスタンスに対して#serviceメソッドを実行
    • リクエストメソッドに応じて処理を振り分け、それぞれ適切なサーバー処理を実行する

WEBrickをめぐるコードリーディングはここでおしまいです。お疲れ様でした🙌

おわりに

最初にお断りを入れた通り、今回読む対象の機能を絞るためかなりのソースコードを省略してご紹介していますが、もちろん今回取り上げた以外にもWEBrickにはHTTPサーバーの機構を叶える様々な仕掛けが仕込まれています。
Webアプリケーション開発者である自分にとって、普段はアプリケーションサーバーとして身近な存在であるWEBrickですが、Webの仕組みを学ぶコードリーディングの題材としてもとても面白く、読み応えのあるライブラリだと感じました。

しかしながらわたしははまだまだ勉強中の身ですので、この記事の内容への技術的指摘、アドバイス等ありましたら教えていただけると幸いです。

最後になりますが、この記事はTama.rb OSSコードリーディング部 WEBrick編の開催を抜きに書き上げることはできませんでした。
イベントの開催を主導してくださった大倉雅史さん @okuramasafumi 、そしてイベントへの参加を快く引き受けてくださり、当日も興味深いエピソードや示唆に富むコードの読み方を教えてくださった高橋征義さん @takahashim へ心から感謝します。本当にありがとうございました。

それでは皆さん、良いクリスマスをお過ごしください🎄✨

23
23
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
23

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?