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とは?
- 引用: library 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
- サーバのパス /view.cgi と CGIHandler がマウントにより結びつけられます。
- パス /view.cgi にアクセスがあるたびにサーバは 'view.rb' を引数として CGIHandler オブジェクトを生成します。
- サーバはリクエストオブジェクトを引数として CGIHandler#service メソッドを呼びます。
- 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
# ...
引数default
にWEBrick::Config::General
が入っていますが、ここはサブクラスのWEBrick::HTTPServer#initialize
でWEBrick::Config::HTTP
によって上書きされています。
それを踏まえると、インスタンス変数@config
にはWEBrick::Config::HTTP
をdup
し、{ :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サーバーの概念を扱う場合には、後者の方が都合が良いのです。
- 参照: class TCPServer
そのため、ここでは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)/
-
-
-
② サーブレットのマウント
続いて、サンプルコードよりこの行の実装を読んでみます。
(ちなみに、変数srv
はWEBrick::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.escape
最後の行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_pipe
、WEBrick::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」に詳しく解説されていますので、ご興味のある方にはお勧めです。
なお、今回のケースでは後ろの二つの空配列を気にする必要はありません。
- 参照: select(2)
- 参照: Working with TCP Sockets by Jesse Storimer
- ちなみにこの本は達人出版会から出版されている「なるほどUnixプロセス」の続編として位置付けられています
さて、ここで改めて先ほどの条件文を見てみましょう。
if svrs = IO.select([sp, *@listeners])
IO.select
はブロッキング処理です。
つまり、IO.select
が「接続要求を感知したソケットオブジェクト」を返すまで、ここで一旦処理が止まることになります。
では、この条件文はいつtrue
になるのでしょう?そうです、クライアントからのリクエストが到着したときです。
なので、ちょっと中途半端ですが ③ サーバーの起動 は一旦ここまでにして、この先のコードは次の処理 ④ パスに対してリクエストが発生 の中で読んでいくことにしましょう。
まとめ
ここまでで行われた処理は以下の通りです。
-
SimpleServer.start
の実行- 「クライアントからのリクエストを待つ→リクエストを処理する」というループに入る
- ソケットオブジェクトがクライアントからのリクエストを待つ
④ パスに対してリクエストが発生
次の条件文にて、IO.select
が引数に渡したソケットオブジェクトのうち、いずれかがクライアントからの接続要求を感知するまでは処理が止まる、と説明しました。
if svrs = IO.select([sp, *@listeners])
これに対して、るりまのサンプルコードには次のような解説がついてます。
- サーバのパス /view.cgi と CGIHandler がマウントにより結びつけられます。
- パス /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
を呼んでいます。
- 参照: singleton method Thread.fork
-
Thread.start
はThread.fork
のエイリアスです
-
これによってここで新しいスレッドを生成し、その後の処理はこのスレッドの中(つまり、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
に戻ると、
変数servlet
にWEBrick::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
の返り値がそのまま変数servlet
、options
、script_name
、path_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リクエストメソッドを示す文字列が入っています。例えばGET
やPOST
などです。
ここでは例示のため、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 へ心から感謝します。本当にありがとうございました。
それでは皆さん、良いクリスマスをお過ごしください🎄✨