👁️

【コードリーディング】Webrickでプロキシの仕組みを覗いてみる

2021/08/05に公開

プロキシの内部的な仕組みを知るために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

コードでの確認

(1) ループに入ります:クライアントからの通信をIO.selectで待ちます

処理が開始されるとループ処理に入り、その中でIO.selectによりクライアントからのリクエストを待ちます。 @listeners は受信用のソケットが格納されます。

server.rb
          while @status == :Running
            begin
              sp = shutdown_pipe[0]
              if svrs = IO.select([sp, *@listeners])

https://github.com/ruby/webrick/blob/3515081a51b91b730267ba2b224039ecfbf8bd7b/lib/webrick/server.rb#L170-L173

@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

https://github.com/ruby/webrick/blob/3515081a51b91b730267ba2b224039ecfbf8bd7b/lib/webrick/utils.rb#L56-L68

(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)

https://github.com/ruby/webrick/blob/3515081a51b91b730267ba2b224039ecfbf8bd7b/lib/webrick/server.rb#L173-L187

server.rb
    def start_thread(sock, &block)
      Thread.start{

https://github.com/ruby/webrick/blob/3515081a51b91b730267ba2b224039ecfbf8bd7b/lib/webrick/server.rb#L288-L289

(3)Net::HTTPで宛先にリクエストします

スレッドで処理が進むと、以下 perform_proxy_request メソッドまで進み、プロキシとして宛先のサーバと通信します。

httpproxy.rb
      http = create_net_http(uri, upstream)
      req_fib = Fiber.new do
        http.start do

https://github.com/ruby/webrick/blob/3515081a51b91b730267ba2b224039ecfbf8bd7b/lib/webrick/httpproxy.rb#L310-L312

httpproxy.rb
    def create_net_http(uri, upstream)
      Net::HTTP.new(uri.host, uri.port, upstream.host, upstream.port)
    end

https://github.com/ruby/webrick/blob/3515081a51b91b730267ba2b224039ecfbf8bd7b/lib/webrick/httpproxy.rb#L298-L300

(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

https://github.com/ruby/webrick/blob/3515081a51b91b730267ba2b224039ecfbf8bd7b/lib/webrick/httpproxy.rb#L344-L350

httpresponse.rb
          @body.call(socket)

https://github.com/ruby/webrick/blob/3515081a51b91b730267ba2b224039ecfbf8bd7b/lib/webrick/httpresponse.rb#L525

(5) スレッドを終了します

以上です。

参考

https://amzn.to/3jhOH4M

https://www.geekpage.jp/programming/ruby-network/select-0.php

Discussion