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にアクセスすると文字列が表示されるはずです。
今日はこれがどうやって動いているか調べたいと思います。
- Mongrel::HttpServer.new
これがいろいろやっているようなのは火を見るより明らかですね。
早速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はスレッド間で待ち時間を設定したいときに利用するものっぽいですね。
覚えておくと便利かも。