読者です 読者をやめる 読者になる 読者になる

Mongrelとrubyのみを使ってブラウザ上に文字列を表示する

Mongrel::HTTPServer.newの仕組み

Mongrelを利用して文字列を返してみる。
普段はRailsと組み合わせるが今回は単体で。
例を参考に書いてみると以下のように利用できる。

require 'rubygems'
require 'mongrel'

class SampleHandler < Mongrel::HttpHandler
  def process(request, response)
    response.start(200) do |head, out|
      head["Content-Type"] = "text/plain"
      out.write("Hello, Mongrel")
    end
  end
end

h = Mongrel::HttpServer.new("0.0.0.0", "3000")
h.register("/test", SampleHandler.new)
h.run.join

これを実行して、http://localhost:3000/testにアクセスすると文字列が表示されるはずです。
今日はこれがどうやって動いているか調べたいと思います。

これがいろいろやっているようなのは火を見るより明らかですね。
早速gemでインストールしたmongrelのlib/mongrel.rbを。。。

    def initialize(host, port, num_processors=950, throttle=0, timeout=60)

      tries = 0
      @socket = TCPServer.new(host, port)

      @classifier = URIClassifier.new
      @host = host
      @port = port
      @workers = ThreadGroup.new
      @throttle = throttle / 100.0
      @num_processors = num_processors
      @timeout = timeout
    end

つまり、Mongrel::HttpServer.new("0.0.0.0", "3000")とするとhost=>"0.0.0.0", port=>3000で
HttpServerオブジェクトが生成されるようです。(タイムアウトは60秒なんですね)

ここでTCPServerオブジェクトをnewしていますね。
これはlib/mongrel/tcphack.rb に定義してあります。

class TCPServer
   def initialize_with_backlog(*args)
     initialize_without_backlog(*args)
     listen(1024)
   end

   alias_method :initialize_without_backlog, :initialize
   alias_method :initialize, :initialize_with_backlog
end

lib/mongrel.rb で socketをrequireしているのでこれはそのクラスを拡張していることになります。
要するにhostとportを設定した上でクライアントからのリクエスト要求を1024に設定しています。

ちなみにクラス階層としては

IO > BasicSocket > IPSocket > TCPSocket > TCPServer です。

litenはman listenで調べてもらえばわかりますが、与えられた引数分のソケットを用意します。
(私自身このあたりあまりわかっていませんが)
イメージとしてはhostとportで生成されたソケットに1024個要求を受け入れることができる接続ができる感じですかね。

この辺でTCPServerは一回おいておきます。

次は URIClassifer.newをしているところ。。。
ここはnewしたとこで特に何も設定していないようなので後で必要になったときに読むことにします。

最後に、ThreadGroupというクラスをnewしています。
これは Object > ThreadGroup という継承関係をもっています。
名前の通りthreadを管理するオブジェクトということで良さそうです。

ここまでで、Mongrel::HTTPServer.newの挙動をみれたことになりそうです。(長かった…)

一旦まとめ
Mongrel::HTTPServer.newは
  引数をもとに hostとportからsocketをつなぎ、(最大コネクション数1024)
  スレッドグループを作成し、接続タイムアウトを60秒に設定し、
 クラス変数としてhostとportをもち、さらにURIClassifierクラスを生成する。
registerメソッド

次は、registerメソッドです。
どんなメソッドかというと以下のような挙動です。

    def register(uri, handler, in_front=false)
      begin
        @classifier.register(uri, [handler])
      rescue URIClassifier::RegistrationError
        handlers = @classifier.resolve(uri)[2]
        method_name = in_front ? 'unshift' : 'push'
        handlers.send(method_name, handler)
      end
      handler.listener = self
    end

エラー処理は今回はみないことにして、実際やってるのはURIClassifierクラスのregisterクラス呼び出して、handlerのliternerでHTTPServerインスタンスそのものをセットしているみたいです。

では、早速みていく。。。

    def register(uri, handler)
      raise RegistrationError, "#{uri.inspect} is already registered" if @handler_map[uri]
      raise RegistrationError, "URI is empty" if !uri or uri.empty?
      raise RegistrationError, "URI must begin with a \"#{Const::SLASH}\"" unless uri[0..0] == Const::SLASH
      @handler_map[uri.dup] = handler
      rebuild
    end

これが、URIClassifier.registerメソッドです。
@handler_mapはURLを登録するhashです。(newしたときに生成する)
つまり一番上のエラー処理は既に登録されていた場合はエラーを出すということ。一度定義すると上書きできないようです。2番目の処理は空だった場合、3番目は最初が'/'で始まっていないとエラー出すみたいです。
そして正常なURIがきたときは引数で指定されたhandlerを登録します。

つまり,登録したURLにどんな処理をさせるのかを登録させる部分ですね。(多分)
なので、ここでリクエストがきたときのhandlerはどのようになっているのかみていきます。

一旦まとめ
registerメソッドはURLを登録し、その対応するURLに処理をセットする。
Monglre:HTTPHandler

処理に関してはこのHTTPServerが重要そうです。
例をみてもここを継承したクラスに処理を書いてレスポンスをセットするみたいです。
では早速。。。

  class HttpHandler
    attr_reader :request_notify
    attr_accessor :listener

    def request_begins(params)
    end

    def request_progress(params, clen, total)
    end

    def process(request, response)
    end

  end

これだけ。。。なるほどーやっぱここは開発者がひな形だけもらってあとは自分で実装できるようになっているのか。
納得。。。
上記の例だとSampleHandlerは継承してprocessだけ設定している感じ。

ここまで設定するとMongel::HttpServer.runメソッドを走らせることで実際にレスポンスを返すことが可能になる。

Mongel::HttpServer.run

ちょっと長いですけど、順番に。

    def run
      BasicSocket.do_not_reverse_lookup=true

      configure_socket_options

      if defined?($tcp_defer_accept_opts) and $tcp_defer_accept_opts
        @socket.setsockopt(*$tcp_defer_accept_opts) rescue nil
      end

      @acceptor = Thread.new do
        begin
          while true
            begin
              client = @socket.accept

              if defined?($tcp_cork_opts) and $tcp_cork_opts
                client.setsockopt(*$tcp_cork_opts) rescue nil
              end

              worker_list = @workers.list

              if worker_list.length >= @num_processors
                STDERR.puts "Server overloaded with #{worker_list.length} processors (#@num_processors max). Dropping connection."
                client.close rescue nil
                reap_dead_workers("max processors")
              else
                thread = Thread.new(client) {|c| process_client(c) }
                thread[:started_on] = Time.now
                @workers.add(thread)

                sleep @throttle if @throttle > 0
              end
            rescue StopServer
              break
            rescue Errno::EMFILE
              reap_dead_workers("too many open files")
              sleep 0.5
            rescue Errno::ECONNABORTED                                                                                                 
              client.close rescue nil
            rescue Object => e
              STDERR.puts "#{Time.now}: Unhandled listen loop exception #{e.inspect}."
              STDERR.puts e.backtrace.join("\n")
            end
          end
          graceful_shutdown
        ensure
          @socket.close
          # STDERR.puts"#{Time.now}:Closedsocket."                                                                                                       
        end
      end

      return @acceptor
    end

BasicSocket.do_not_reverse_lookup = true はアドレスからホスト名を逆引きしないという設定らしいです。
(詳しくはRubyリファレンスを)
configure_socket_optionsはRUBY_PLATFORMによってSOCKETのオプションを設定します。
@socket.setsocoptsでセットしていますね。

そして、とうとう @acceptor = Thread.newで処理開始します。
trueの間リクエストを受け続けるよう設定しています。(当然か。。。)

そして、次の client = @socket.accept で最初に生成したTCPServerコネクションの接続要求を受けるように設定します。workers_list = @workers.listではスレッドリストを返すようにしています。

ここでworkers_listはnum_proccessorsを超える、つまり現在の処理スレッド数が設定している値を超えたらコネクションを終了します。
それ以外の場合はThreadGroupに対して新しいThreadを作成しprocess_clientメソッドを実行します。
ただRubyはあくまでシングルスレッドなので新しいスレッドを作成して平行で動いているように見せることは可能ですが実際に実行するときは時分割で実行します。

ちなみに@throttleはスレッド間で待ち時間を設定したいときに利用するものっぽいですね。
覚えておくと便利かも。