👁️
【コードリーディング】Webrickでプロキシの仕組みを覗いてみる
プロキシの内部的な仕組みを知るためにWebrickのソースを読んで大まかな処理の流れを確認してみたのでまとめておきます。
大まかな流れ
自分なりに抑えておきたいポイントをまとめると以下のような流れでした:
(1) ループに入ります:クライアントからの通信をIO.selectで待ちます
(2) 宛先と通信するためスレッドを立ち上げます(メインのスレッドはループ処理の先頭に戻り、IO.selectに戻ります。以下は立ち上げられたスレッドの処理)
(3) Net::HTTPで宛先にリクエストします
(4) 返信を受け取ったらデータを載せて元のクライアントに返信します
(5) スレッドを終了します
動作確認に用いたコード
以下のコードの動作を追いました。(その他のオプションの時の動作は追っていません)
test.rb
require 'webrick'
require 'webrick/httpproxy'
s = WEBrick::HTTPProxyServer.new(Port: 8080)
Signal.trap('INT') do
s.shutdown
end
s.start
コードでの確認
IO.selectで待ちます
(1) ループに入ります:クライアントからの通信を処理が開始されるとループ処理に入り、その中でIO.selectによりクライアントからのリクエストを待ちます。 @listeners
は受信用のソケットが格納されます。
server.rb
while @status == :Running
begin
sp = shutdown_pipe[0]
if svrs = IO.select([sp, *@listeners])
@listeners
の中身は以下のメソッドにより実体が準備されます。中身はTCPServerのオブジェクトです。
utils.rb
def create_listeners(address, port)
unless port
raise ArgumentError, "must specify port"
end
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
(2) 宛先と通信するためスレッドを立ち上げます(メインのスレッドはループ処理の先頭に戻り、IO.selectに戻ります。以下は立ち上げられたスレッドの処理)
クライアントのリクエストを受信したら処理が進み、プロキシとしての動作は以下 start_thread
により別スレッドに渡されます。
server.rb
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)
server.rb
def start_thread(sock, &block)
Thread.start{
Net::HTTPで宛先にリクエストします
(3)スレッドで処理が進むと、以下 perform_proxy_request
メソッドまで進み、プロキシとして宛先のサーバと通信します。
httpproxy.rb
http = create_net_http(uri, upstream)
req_fib = Fiber.new do
http.start do
httpproxy.rb
def create_net_http(uri, upstream)
Net::HTTP.new(uri.host, uri.port, upstream.host, upstream.port)
end
(4) 返信を受け取ったらデータを載せて元のクライアントに返信します
ここが少しややこしかったですが、返信データを扱っているResponseのオブジェクトの @body
インスタンスにクライアントにデータを返信するための処理を行うProcオブジェクトを格納し、実行します。
httpproxy.rb
res.body = ->(socket) do
while buf = body_tmp.shift
socket.write(buf)
buf.clear
req_fib.resume # continue response.read_body
end
end
httpresponse.rb
@body.call(socket)
(5) スレッドを終了します
以上です。
参考
Discussion