Ruby の net/http のコードを少し読む
require 'net/http' uri = URI.parse("http:example/image.jpg") response = Net::HTTP.get_response(uri) response.code response.body
これで画像がダウンロードできるわけだが、ライブラリを探る。
解読
/home/tomoki/.rbenv/versions/3.1.1/lib/ruby/3.1.0/net/http.rb
module Net class HTTP < Protocol def HTTP.get_response(uri_or_host, path_or_headers = nil, port = nil, &block) if path_or_headers && !path_or_headers.is_a?(Hash) host = uri_or_host path = path_or_headers new(host, port || HTTP.default_port).start {|http| return http.request_get(path, &block) } else uri = uri_or_host headers = path_or_headers start(uri.hostname, uri.port, :use_ssl => uri.scheme == 'https') {|http| return http.request_get(uri, headers, &block) } end end end end
これの
start(uri.hostname, uri.port,:use_ssl => uri.scheme == 'https') {|http| return http.request_get(uri, headers, &block) }
この部分が呼ばれる。HTTP.start
メソッドはどこか。
同じファイルの 608行目からある。
module Net class HTTP < Protocol def HTTP.start(address, *arg, &block) # :yield: +http+ arg.pop if opt = Hash.try_convert(arg[-1]) port, p_addr, p_port, p_user, p_pass = *arg p_addr = :ENV if arg.size < 2 port = https_default_port if !port && opt && opt[:use_ssl] http = new(address, port, p_addr, p_port, p_user, p_pass) http.ipaddr = opt[:ipaddr] if opt && opt[:ipaddr] if opt if opt[:use_ssl] opt = {verify_mode: OpenSSL::SSL::VERIFY_PEER}.update(opt) end http.methods.grep(/\A(\w+)=\z/) do |meth| key = $1.to_sym opt.key?(key) or next http.__send__(meth, opt[key]) end end http.start(&block) end end end
ここの
http = new(address, port, p_addr, p_port, p_user, p_pass)
が重要そうだ。http = Http.new
はどこにあるのか? 682行にある。最後でhttp.start(&block)
が呼ばれている。どこにあるのか?
961行目にある。
module Net class HTTP < Protocol def start # :yield: http raise IOError, 'HTTP session already opened' if @started if block_given? begin do_start return yield(self) ensure do_finish end end do_start self end def do_start connect @started = true end private :do_start def connect if use_ssl? # reference early to load OpenSSL before connecting, # as OpenSSL may take time to load. @ssl_context = OpenSSL::SSL::SSLContext.new end if proxy? then conn_addr = proxy_address conn_port = proxy_port else conn_addr = conn_address conn_port = port end D "opening connection to #{conn_addr}:#{conn_port}..." begin s = Socket.tcp conn_addr, conn_port, @local_host, @local_port, connect_timeout: @open_timeout rescue => e e = Net::OpenTimeout.new(e) if e.is_a?(Errno::ETIMEDOUT) #for compatibility with previous versions raise e, "Failed to open TCP connection to " + "#{conn_addr}:#{conn_port} (#{e.message})" end s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) D "opened" if use_ssl? if proxy? plain_sock = BufferedIO.new(s, read_timeout: @read_timeout, write_timeout: @write_timeout, continue_timeout: @continue_timeout, debug_output: @debug_output) buf = "CONNECT #{conn_address}:#{@port} HTTP/#{HTTPVersion}\r\n" buf << "Host: #{@address}:#{@port}\r\n" if proxy_user credential = ["#{proxy_user}:#{proxy_pass}"].pack('m0') buf << "Proxy-Authorization: Basic #{credential}\r\n" end buf << "\r\n" plain_sock.write(buf) HTTPResponse.read_new(plain_sock).value # assuming nothing left in buffers after successful CONNECT response end ssl_parameters = Hash.new iv_list = instance_variables SSL_IVNAMES.each_with_index do |ivname, i| if iv_list.include?(ivname) value = instance_variable_get(ivname) unless value.nil? ssl_parameters[SSL_ATTRIBUTES[i]] = value end end end @ssl_context.set_params(ssl_parameters) @ssl_context.session_cache_mode = OpenSSL::SSL::SSLContext::SESSION_CACHE_CLIENT | OpenSSL::SSL::SSLContext::SESSION_CACHE_NO_INTERNAL_STORE @ssl_context.session_new_cb = proc {|sock, sess| @ssl_session = sess } D "starting SSL for #{conn_addr}:#{conn_port}..." s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context) s.sync_close = true # Server Name Indication (SNI) RFC 3546 s.hostname = @address if s.respond_to? :hostname= if @ssl_session and Process.clock_gettime(Process::CLOCK_REALTIME) < @ssl_session.time.to_f + @ssl_session.timeout s.session = @ssl_session end ssl_socket_connect(s, @open_timeout) if (@ssl_context.verify_mode != OpenSSL::SSL::VERIFY_NONE) && @ssl_context.verify_hostname s.post_connection_check(@address) end D "SSL established, protocol: #{s.ssl_version}, cipher: #{s.cipher[0]}" end @socket = BufferedIO.new(s, read_timeout: @read_timeout, write_timeout: @write_timeout, continue_timeout: @continue_timeout, debug_output: @debug_output) @last_communicated = nil on_connect rescue => exception if s D "Conn close because of connect error #{exception}" s.close end raise end private :connect end end
Http#connect
が重要そうだ。
s = Socket.tcp conn_addr, conn_port, @local_host, @local_port, connect_timeout: @open_timeout
でs = Socket.tcp
を呼んでいる。
Socket.tcp
メソッドのマニュアルはここにある。ブロック付きだと
require 'socket' Socket.tcp("www.ruby-lang.org", 80) {|sock| sock.print "GET / HTTP/1.0\r\nHost: www.ruby-lang.org\r\n\r\n" sock.close_write puts sock.read }
のように使うらしい。
つまり、s = Socket.tcp
には TCP/IP で host に接続するソケットオブジェクトが入る、と。
続き。
s.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) #1004行 #1054行 @socket = BufferedIO.new(s, read_timeout: @read_timeout, write_timeout: @write_timeout, continue_timeout: @continue_timeout, debug_output: @debug_output) on_connect #1059行
いろいろある。Net::BufferedIO
は /home/tomoki/.rbenv/versions/3.1.1/lib/ruby/3.1.0/net/protocol.rb の 115行からある。まあ、結局s
が io として@socket
に入る。
解読2
もとに戻る。Http#request_get
はどこにあるか。1439行にある。
def request_get(path, initheader = nil, &block) # :yield: +response+ request(Get.new(path, initheader), &block) end #1528行 def request(req, body = nil, &block) # :yield: +response+ unless started? start { req['connection'] ||= 'close' return request(req, body, &block) } end if proxy_user() req.proxy_basic_auth proxy_user(), proxy_pass() unless use_ssl? end req.set_body_internal body res = transport_request(req, &block) if sspi_auth?(res) sspi_auth(req) res = transport_request(req, &block) end res end
Net::Http::Get
はどこにあるか。
/home/tomoki/.rbenv/versions/3.1.1/lib/ruby/3.1.0/net/http/requests.rb にある。
class Net::HTTP::Get < Net::HTTPRequest METHOD = 'GET' REQUEST_HAS_BODY = false RESPONSE_HAS_BODY = true end
Net::HTTPRequest
は /home/tomoki/.rbenv/versions/3.1.1/lib/ruby/3.1.0/net/http/requests.rb にある。
class Net::HTTPRequest < Net::HTTPGenericRequest end
じつは本体は、/home/tomoki/.rbenv/versions/3.1.1/lib/ruby/3.1.0/net/http/generic_request.rb にある。
class Net::HTTPGenericRequest include Net::HTTPHeader def initialize(m, reqbody, resbody, uri_or_path, initheader = nil) @method = m @request_has_body = reqbody @response_has_body = resbody if URI === uri_or_path then raise ArgumentError, "not an HTTP URI" unless URI::HTTP === uri_or_path raise ArgumentError, "no host component for URI" unless uri_or_path.hostname @uri = uri_or_path.dup host = @uri.hostname.dup host << ":".freeze << @uri.port.to_s if @uri.port != @uri.default_port @path = uri_or_path.request_uri raise ArgumentError, "no HTTP request path given" unless @path else @uri = nil host = nil raise ArgumentError, "no HTTP request path given" unless uri_or_path raise ArgumentError, "HTTP request path is empty" if uri_or_path.empty? @path = uri_or_path.dup end @decode_content = false if Net::HTTP::HAVE_ZLIB then if !initheader || !initheader.keys.any? { |k| %w[accept-encoding range].include? k.downcase } then @decode_content = true if @response_has_body initheader = initheader ? initheader.dup : {} initheader["accept-encoding"] = "gzip;q=1.0,deflate;q=0.6,identity;q=0.3" end end initialize_http_header initheader self['Accept'] ||= '*/*' self['User-Agent'] ||= 'Ruby' self['Host'] ||= host if host @body = nil @body_stream = nil @body_data = nil end end
もう一度戻る。再掲載。
#1528行 def request(req, body = nil, &block) # :yield: +response+ unless started? start { req['connection'] ||= 'close' return request(req, body, &block) } end if proxy_user() req.proxy_basic_auth proxy_user(), proxy_pass() unless use_ssl? end req.set_body_internal body res = transport_request(req, &block) if sspi_auth?(res) sspi_auth(req) res = transport_request(req, &block) end res end
問題はこれかな。
res = transport_request(req, &block)
req
はNet::Http::Get.new
が入っている。では、transport_request
メソッドは何でどこか。
1562行目である。
module Net class HTTP < Protocol def transport_request(req) count = 0 begin begin_transport req res = catch(:response) { begin req.exec @socket, @curr_http_version, edit_path(req.path) rescue Errno::EPIPE # Failure when writing full request, but we can probably # still read the received response. end begin res = HTTPResponse.read_new(@socket) res.decode_content = req.decode_content end while res.kind_of?(HTTPInformation) res.uri = req.uri res } res.reading_body(@socket, req.response_body_permitted?) { yield res if block_given? } rescue Net::OpenTimeout raise rescue Net::ReadTimeout, IOError, EOFError, Errno::ECONNRESET, Errno::ECONNABORTED, Errno::EPIPE, Errno::ETIMEDOUT, # avoid a dependency on OpenSSL defined?(OpenSSL::SSL) ? OpenSSL::SSL::SSLError : IOError, Timeout::Error => exception if count < max_retries && IDEMPOTENT_METHODS_.include?(req.method) count += 1 @socket.close if @socket D "Conn close because of error #{exception}, and retry" retry end D "Conn close because of error #{exception}" @socket.close if @socket raise end end_transport req, res res rescue => exception D "Conn close because of error #{exception}" @socket.close if @socket raise exception end end end
res = HTTPResponse.read_new(@socket)
だ。