зеркало из https://github.com/github/ruby.git
Introduction of Happy Eyeballs Version 2 (RFC8305) in Socket.tcp (#9374)
* Introduction of Happy Eyeballs Version 2 (RFC8305) in Socket.tcp This is an implementation of Happy Eyeballs version 2 (RFC 8305) in Socket.tcp. [Background] Currently, `Socket.tcp` synchronously resolves names and makes connection attempts with `Addrinfo::foreach.` This implementation has the following two problems. 1. In name resolution, the program stops until the DNS server responds to all DNS queries. 2. In a connection attempt, while an IP address is trying to connect to the destination host and is taking time, the program stops, and other resolved IP addresses cannot try to connect. [Proposal] "Happy Eyeballs" ([RFC 8305](https://datatracker.ietf.org/doc/html/rfc8305)) is an algorithm to solve this kind of problem. It avoids delays to the user whenever possible and also uses IPv6 preferentially. I implemented it into `Socket.tcp` by using `Addrinfo.getaddrinfo` in each thread spawned per address family to resolve the hostname asynchronously, and using `Socket::connect_nonblock` to try to connect with multiple addrinfo in parallel. [Outcome] This change eliminates a fatal defect in the following cases. Case 1. One of the A or AAAA DNS queries does not return --- require 'socket' class Addrinfo class << self # Current Socket.tcp depends on foreach def foreach(nodename, service, family=nil, socktype=nil, protocol=nil, flags=nil, timeout: nil, &block) getaddrinfo(nodename, service, Socket::AF_INET6, socktype, protocol, flags, timeout: timeout) .concat(getaddrinfo(nodename, service, Socket::AF_INET, socktype, protocol, flags, timeout: timeout)) .each(&block) end def getaddrinfo(_, _, family, *_) case family when Socket::AF_INET6 then sleep when Socket::AF_INET then [Addrinfo.tcp("127.0.0.1", 4567)] end end end end Socket.tcp("localhost", 4567) --- Because the current `Socket.tcp` cannot resolve IPv6 names, the program stops in this case. It cannot start to connect with IPv4 address. Though `Socket.tcp` with HEv2 can promptly start a connection attempt with IPv4 address in this case. Case 2. Server does not promptly return ack for syn of either IPv4 / IPv6 address family --- require 'socket' fork do socket = Socket.new(Socket::AF_INET6, :STREAM) socket.setsockopt(:SOCKET, :REUSEADDR, true) socket.bind(Socket.pack_sockaddr_in(4567, '::1')) sleep socket.listen(1) connection, _ = socket.accept connection.close socket.close end fork do socket = Socket.new(Socket::AF_INET, :STREAM) socket.setsockopt(:SOCKET, :REUSEADDR, true) socket.bind(Socket.pack_sockaddr_in(4567, '127.0.0.1')) socket.listen(1) connection, _ = socket.accept connection.close socket.close end Socket.tcp("localhost", 4567) --- The current `Socket.tcp` tries to connect serially, so when its first name resolves an IPv6 address and initiates a connection to an IPv6 server, this server does not return an ACK, and the program stops. Though `Socket.tcp` with HEv2 starts to connect sequentially and in parallel so a connection can be established promptly at the socket that attempted to connect to the IPv4 server. In exchange, the performance of `Socket.tcp` with HEv2 will be degraded. --- 100.times { Socket.tcp("www.ruby-lang.org", 80) } --- This is due to the addition of the creation of IO objects, Thread objects, etc., and calls to `IO::select` in the implementation. * Avoid NameError of Socket::EAI_ADDRFAMILY in MinGW * Support Windows with SO_CONNECT_TIME * Improve performance I have additionally implemented the following patterns: - If the host is single-stack, name resolution is performed in the main thread. This reduces the cost of creating threads. - If an IP address is specified, name resolution is performed in the main thread. This also reduces the cost of creating threads. - If only one IP address is resolved, connect is executed in blocking mode. This reduces the cost of calling IO::select. Also, I have added a fast_fallback option for users who wish not to use HE. Here are the results of each performance test. ```ruby require 'socket' require 'benchmark' HOSTNAME = "www.ruby-lang.org" PORT = 80 ai = Addrinfo.tcp(HOSTNAME, PORT) Benchmark.bmbm do |x| x.report("Domain name") do 30.times { Socket.tcp(HOSTNAME, PORT).close } end x.report("IP Address") do 30.times { Socket.tcp(ai.ip_address, PORT).close } end x.report("fast_fallback: false") do 30.times { Socket.tcp(HOSTNAME, PORT, fast_fallback: false).close } end end ``` ``` user system total real Domain name 0.015567 0.032511 0.048078 ( 0.325284) IP Address 0.004458 0.014219 0.018677 ( 0.284361) fast_fallback: false 0.005869 0.021511 0.027380 ( 0.321891) ```` And this is the measurement result when executed in a single stack environment. ``` user system total real Domain name 0.007062 0.019276 0.026338 ( 1.905775) IP Address 0.004527 0.012176 0.016703 ( 3.051192) fast_fallback: false 0.005546 0.019426 0.024972 ( 1.775798) ``` The following is the result of the run on Ruby 3.3.0. (on Dual stack environment) ``` user system total real Ruby 3.3.0 0.007271 0.027410 0.034681 ( 0.472510) ``` (on Single stack environment) ``` user system total real Ruby 3.3.0 0.005353 0.018898 0.024251 ( 1.774535) ``` * Do not cache `Socket.ip_address_list` As mentioned in the comment at https://github.com/ruby/ruby/pull/9374#discussion_r1482269186, caching Socket.ip_address_list does not follow changes in network configuration. But if we stop caching, it becomes necessary to check every time `Socket.tcp` is called whether it's a single stack or not, which could further degrade performance in the case of a dual stack. From this, I've changed the approach so that when a domain name is passed, it doesn't check whether it's a single stack or not and resolves names in parallel each time. The performance measurement results are as follows. require 'socket' require 'benchmark' HOSTNAME = "www.ruby-lang.org" PORT = 80 ai = Addrinfo.tcp(HOSTNAME, PORT) Benchmark.bmbm do |x| x.report("Domain name") do 30.times { Socket.tcp(HOSTNAME, PORT).close } end x.report("IP Address") do 30.times { Socket.tcp(ai.ip_address, PORT).close } end x.report("fast_fallback: false") do 30.times { Socket.tcp(HOSTNAME, PORT, fast_fallback: false).close } end end user system total real Domain name 0.004085 0.011873 0.015958 ( 0.330097) IP Address 0.000993 0.004400 0.005393 ( 0.257286) fast_fallback: false 0.001348 0.008266 0.009614 ( 0.298626) * Wait forever if fallback addresses are unresolved, unless resolv_timeout Changed from waiting only 3 seconds for name resolution when there is no fallback address available, to waiting as long as there is no resolv_timeout. This is in accordance with the current `Socket.tcp` specification. * Use exact pattern to match IPv6 address format for specify address family
This commit is contained in:
Родитель
616b414621
Коммит
9ec342e07d
|
@ -599,6 +599,30 @@ class Socket < BasicSocket
|
|||
__accept_nonblock(exception)
|
||||
end
|
||||
|
||||
RESOLUTION_DELAY = 0.05
|
||||
private_constant :RESOLUTION_DELAY
|
||||
|
||||
CONNECTION_ATTEMPT_DELAY = 0.25
|
||||
private_constant :CONNECTION_ATTEMPT_DELAY
|
||||
|
||||
ADDRESS_FAMILIES = {
|
||||
ipv6: Socket::AF_INET6,
|
||||
ipv4: Socket::AF_INET
|
||||
}.freeze
|
||||
private_constant :ADDRESS_FAMILIES
|
||||
|
||||
HOSTNAME_RESOLUTION_QUEUE_UPDATED = 0
|
||||
private_constant :HOSTNAME_RESOLUTION_QUEUE_UPDATED
|
||||
|
||||
IPV6_ADRESS_FORMAT = /(?i)(?:(?:[0-9A-F]{1,4}:){7}(?:[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){6}(?:[0-9A-F]{1,4}::[0-9A-F]{1,4}|:(?:[0-9A-F]{1,4}:){1,5}[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){5}(?::[0-9A-F]{1,4}::[0-9A-F]{1,4}|:(?:[0-9A-F]{1,4}:){1,4}[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){4}(?::[0-9A-F]{1,4}::[0-9A-F]{1,4}|:(?:[0-9A-F]{1,4}:){1,3}[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){3}(?::[0-9A-F]{1,4}::[0-9A-F]{1,4}|:(?:[0-9A-F]{1,4}:){1,2}[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){2}(?::[0-9A-F]{1,4}::[0-9A-F]{1,4}|:(?:[0-9A-F]{1,4}:)[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){1}(?::[0-9A-F]{1,4}::[0-9A-F]{1,4}|::(?:[0-9A-F]{1,4}:){1,5}[0-9A-F]{1,4}|:)|::(?:[0-9A-F]{1,4}::[0-9A-F]{1,4}|:(?:[0-9A-F]{1,4}:){1,6}[0-9A-F]{1,4}|:))(?:%.+)?/
|
||||
private_constant :IPV6_ADRESS_FORMAT
|
||||
|
||||
@tcp_fast_fallback = true
|
||||
|
||||
class << self
|
||||
attr_accessor :tcp_fast_fallback
|
||||
end
|
||||
|
||||
# :call-seq:
|
||||
# Socket.tcp(host, port, local_host=nil, local_port=nil, [opts]) {|socket| ... }
|
||||
# Socket.tcp(host, port, local_host=nil, local_port=nil, [opts])
|
||||
|
@ -624,8 +648,491 @@ class Socket < BasicSocket
|
|||
# sock.close_write
|
||||
# puts sock.read
|
||||
# }
|
||||
#
|
||||
def self.tcp(host, port, local_host = nil, local_port = nil, connect_timeout: nil, resolv_timeout: nil) # :yield: socket
|
||||
def self.tcp(host, port, local_host = nil, local_port = nil, connect_timeout: nil, resolv_timeout: nil, fast_fallback: tcp_fast_fallback, &block) # :yield: socket
|
||||
unless fast_fallback
|
||||
return tcp_without_fast_fallback(host, port, local_host, local_port, connect_timeout:, resolv_timeout:, &block)
|
||||
end
|
||||
|
||||
# Happy Eyeballs' states
|
||||
# - :start
|
||||
# - :v6c
|
||||
# - :v4w
|
||||
# - :v4c
|
||||
# - :v46c
|
||||
# - :v46w
|
||||
# - :success
|
||||
# - :failure
|
||||
# - :timeout
|
||||
|
||||
specified_family_name = nil
|
||||
hostname_resolution_threads = []
|
||||
hostname_resolution_queue = nil
|
||||
hostname_resolution_waiting = nil
|
||||
hostname_resolution_expires_at = nil
|
||||
selectable_addrinfos = SelectableAddrinfos.new
|
||||
connecting_sockets = ConnectingSockets.new
|
||||
local_addrinfos = []
|
||||
connection_attempt_delay_expires_at = nil
|
||||
connection_attempt_started_at = nil
|
||||
state = :start
|
||||
connected_socket = nil
|
||||
last_error = nil
|
||||
is_windows_environment ||= (RUBY_PLATFORM =~ /mswin|mingw|cygwin/)
|
||||
|
||||
ret = loop do
|
||||
case state
|
||||
when :start
|
||||
specified_family_name, next_state = host && specified_family_name_and_next_state(host)
|
||||
|
||||
if local_host && local_port
|
||||
specified_family_name, next_state = specified_family_name_and_next_state(local_host) unless specified_family_name
|
||||
local_addrinfos = Addrinfo.getaddrinfo(local_host, local_port, ADDRESS_FAMILIES[specified_family_name], :STREAM, timeout: resolv_timeout)
|
||||
end
|
||||
|
||||
if specified_family_name
|
||||
addrinfos = Addrinfo.getaddrinfo(host, port, ADDRESS_FAMILIES[specified_family_name], :STREAM, timeout: resolv_timeout)
|
||||
selectable_addrinfos.add(specified_family_name, addrinfos)
|
||||
hostname_resolution_queue = NoHostnameResolutionQueue.new
|
||||
state = next_state
|
||||
next
|
||||
end
|
||||
|
||||
resolving_family_names = ADDRESS_FAMILIES.keys
|
||||
hostname_resolution_queue = HostnameResolutionQueue.new(resolving_family_names.size)
|
||||
hostname_resolution_waiting = hostname_resolution_queue.waiting_pipe
|
||||
hostname_resolution_started_at = current_clocktime if resolv_timeout
|
||||
hostname_resolution_args = [host, port, hostname_resolution_queue]
|
||||
|
||||
hostname_resolution_threads.concat(
|
||||
resolving_family_names.map { |family|
|
||||
thread_args = [family, *hostname_resolution_args]
|
||||
thread = Thread.new(*thread_args) { |*thread_args| hostname_resolution(*thread_args) }
|
||||
Thread.pass
|
||||
thread
|
||||
}
|
||||
)
|
||||
|
||||
hostname_resolution_retry_count = resolving_family_names.size - 1
|
||||
hostname_resolution_expires_at = hostname_resolution_started_at + resolv_timeout if resolv_timeout
|
||||
|
||||
while hostname_resolution_retry_count >= 0
|
||||
remaining = resolv_timeout ? second_to_timeout(hostname_resolution_started_at + resolv_timeout) : nil
|
||||
hostname_resolved, _, = IO.select(hostname_resolution_waiting, nil, nil, remaining)
|
||||
|
||||
unless hostname_resolved
|
||||
state = :timeout
|
||||
break
|
||||
end
|
||||
|
||||
family_name, res = hostname_resolution_queue.get
|
||||
|
||||
if res.is_a? Exception
|
||||
unless (Socket.const_defined?(:EAI_ADDRFAMILY)) &&
|
||||
(res.is_a?(Socket::ResolutionError)) &&
|
||||
(res.error_code == Socket::EAI_ADDRFAMILY)
|
||||
last_error = res
|
||||
end
|
||||
|
||||
if hostname_resolution_retry_count.zero?
|
||||
state = :failure
|
||||
break
|
||||
end
|
||||
hostname_resolution_retry_count -= 1
|
||||
else
|
||||
state = case family_name
|
||||
when :ipv6 then :v6c
|
||||
when :ipv4 then hostname_resolution_queue.closed? ? :v4c : :v4w
|
||||
end
|
||||
selectable_addrinfos.add(family_name, res)
|
||||
last_error = nil
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
next
|
||||
when :v4w
|
||||
ipv6_resolved, _, = IO.select(hostname_resolution_waiting, nil, nil, RESOLUTION_DELAY)
|
||||
|
||||
if ipv6_resolved
|
||||
family_name, res = hostname_resolution_queue.get
|
||||
selectable_addrinfos.add(family_name, res) unless res.is_a? Exception
|
||||
state = :v46c
|
||||
else
|
||||
state = :v4c
|
||||
end
|
||||
|
||||
next
|
||||
when :v4c, :v6c, :v46c
|
||||
connection_attempt_started_at = current_clocktime unless connection_attempt_started_at
|
||||
addrinfo = selectable_addrinfos.get
|
||||
|
||||
if local_addrinfos.any?
|
||||
local_addrinfo = local_addrinfos.find { |lai| lai.afamily == addrinfo.afamily }
|
||||
|
||||
if local_addrinfo.nil?
|
||||
if selectable_addrinfos.empty? && connecting_sockets.empty? && hostname_resolution_queue.closed?
|
||||
last_error = SocketError.new 'no appropriate local address'
|
||||
state = :failure
|
||||
elsif selectable_addrinfos.any?
|
||||
# Try other Addrinfo in next loop
|
||||
else
|
||||
if resolv_timeout && hostname_resolution_queue.opened? &&
|
||||
(current_clocktime >= hostname_resolution_expires_at)
|
||||
state = :timeout
|
||||
else
|
||||
# Wait for connection to be established or hostname resolution in next loop
|
||||
connection_attempt_delay_expires_at = nil
|
||||
state = :v46w
|
||||
end
|
||||
end
|
||||
next
|
||||
end
|
||||
end
|
||||
|
||||
connection_attempt_delay_expires_at = current_clocktime + CONNECTION_ATTEMPT_DELAY
|
||||
|
||||
begin
|
||||
result = if specified_family_name && selectable_addrinfos.empty? &&
|
||||
connecting_sockets.empty? && hostname_resolution_queue.closed?
|
||||
local_addrinfo ?
|
||||
addrinfo.connect_from(local_addrinfo, timeout: connect_timeout) :
|
||||
addrinfo.connect(timeout: connect_timeout)
|
||||
else
|
||||
socket = Socket.new(addrinfo.pfamily, addrinfo.socktype, addrinfo.protocol)
|
||||
socket.bind(local_addrinfo) if local_addrinfo
|
||||
socket.connect_nonblock(addrinfo, exception: false)
|
||||
end
|
||||
|
||||
case result
|
||||
when 0
|
||||
connected_socket = socket
|
||||
state = :success
|
||||
when Socket
|
||||
connected_socket = result
|
||||
state = :success
|
||||
when :wait_writable
|
||||
connecting_sockets.add(socket, addrinfo)
|
||||
state = :v46w
|
||||
end
|
||||
rescue SystemCallError => e
|
||||
last_error = e
|
||||
socket.close if socket && !socket.closed?
|
||||
|
||||
if selectable_addrinfos.empty? && connecting_sockets.empty? && hostname_resolution_queue.closed?
|
||||
state = :failure
|
||||
elsif selectable_addrinfos.any?
|
||||
# Try other Addrinfo in next loop
|
||||
else
|
||||
if resolv_timeout && hostname_resolution_queue.opened? &&
|
||||
(current_clocktime >= hostname_resolution_expires_at)
|
||||
state = :timeout
|
||||
else
|
||||
# Wait for connection to be established or hostname resolution in next loop
|
||||
connection_attempt_delay_expires_at = nil
|
||||
state = :v46w
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
next
|
||||
when :v46w
|
||||
if connect_timeout && second_to_timeout(connection_attempt_started_at + connect_timeout).zero?
|
||||
state = :timeout
|
||||
next
|
||||
end
|
||||
|
||||
remaining = second_to_timeout(connection_attempt_delay_expires_at)
|
||||
hostname_resolution_waiting = hostname_resolution_waiting && hostname_resolution_queue.closed? ? nil : hostname_resolution_waiting
|
||||
hostname_resolved, connectable_sockets, = IO.select(hostname_resolution_waiting, connecting_sockets.all, nil, remaining)
|
||||
|
||||
if connectable_sockets&.any?
|
||||
while (connectable_socket = connectable_sockets.pop)
|
||||
is_connected =
|
||||
if is_windows_environment
|
||||
sockopt = connectable_socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_CONNECT_TIME)
|
||||
sockopt.unpack('i').first >= 0
|
||||
else
|
||||
sockopt = connectable_socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_ERROR)
|
||||
sockopt.int.zero?
|
||||
end
|
||||
|
||||
if is_connected
|
||||
connected_socket = connectable_socket
|
||||
connecting_sockets.delete connectable_socket
|
||||
connectable_sockets.each do |other_connectable_socket|
|
||||
other_connectable_socket.close unless other_connectable_socket.closed?
|
||||
end
|
||||
state = :success
|
||||
break
|
||||
else
|
||||
failed_ai = connecting_sockets.delete connectable_socket
|
||||
inspected_ip_address = failed_ai.ipv6? ? "[#{failed_ai.ip_address}]" : failed_ai.ip_address
|
||||
last_error = SystemCallError.new("connect(2) for #{inspected_ip_address}:#{failed_ai.ip_port}", sockopt.int)
|
||||
connectable_socket.close unless connectable_socket.closed?
|
||||
|
||||
next if connectable_sockets.any?
|
||||
|
||||
if selectable_addrinfos.empty? && connecting_sockets.empty? && hostname_resolution_queue.closed?
|
||||
state = :failure
|
||||
elsif selectable_addrinfos.any?
|
||||
# Wait for connection attempt delay timeout in next loop
|
||||
else
|
||||
if resolv_timeout && hostname_resolution_queue.opened? &&
|
||||
(current_clocktime >= hostname_resolution_expires_at)
|
||||
state = :timeout
|
||||
else
|
||||
# Wait for connection to be established or hostname resolution in next loop
|
||||
connection_attempt_delay_expires_at = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
elsif hostname_resolved&.any?
|
||||
family_name, res = hostname_resolution_queue.get
|
||||
selectable_addrinfos.add(family_name, res) unless res.is_a? Exception
|
||||
else
|
||||
if selectable_addrinfos.empty? && connecting_sockets.empty? && hostname_resolution_queue.closed?
|
||||
state = :failure
|
||||
elsif selectable_addrinfos.any?
|
||||
# Try other Addrinfo in next loop
|
||||
state = :v46c
|
||||
else
|
||||
if resolv_timeout && hostname_resolution_queue.opened? &&
|
||||
(current_clocktime >= hostname_resolution_expires_at)
|
||||
state = :timeout
|
||||
else
|
||||
# Wait for connection to be established or hostname resolution in next loop
|
||||
connection_attempt_delay_expires_at = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
next
|
||||
when :success
|
||||
break connected_socket
|
||||
when :failure
|
||||
raise last_error
|
||||
when :timeout
|
||||
raise Errno::ETIMEDOUT, 'user specified timeout'
|
||||
end
|
||||
end
|
||||
|
||||
if block_given?
|
||||
begin
|
||||
yield ret
|
||||
ensure
|
||||
ret.close
|
||||
end
|
||||
else
|
||||
ret
|
||||
end
|
||||
ensure
|
||||
if fast_fallback
|
||||
hostname_resolution_threads.each do |thread|
|
||||
thread&.exit
|
||||
end
|
||||
|
||||
hostname_resolution_queue&.close_all
|
||||
|
||||
connecting_sockets.each do |connecting_socket|
|
||||
connecting_socket.close unless connecting_socket.closed?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.specified_family_name_and_next_state(hostname)
|
||||
if hostname.match?(IPV6_ADRESS_FORMAT) then [:ipv6, :v6c]
|
||||
elsif hostname.match?(/^([0-9]{1,3}\.){3}[0-9]{1,3}$/) then [:ipv4, :v4c]
|
||||
end
|
||||
end
|
||||
private_class_method :specified_family_name_and_next_state
|
||||
|
||||
def self.hostname_resolution(family, host, port, hostname_resolution_queue)
|
||||
begin
|
||||
resolved_addrinfos = Addrinfo.getaddrinfo(host, port, ADDRESS_FAMILIES[family], :STREAM)
|
||||
hostname_resolution_queue.add_resolved(family, resolved_addrinfos)
|
||||
rescue => e
|
||||
hostname_resolution_queue.add_error(family, e)
|
||||
end
|
||||
end
|
||||
private_class_method :hostname_resolution
|
||||
|
||||
def self.second_to_timeout(ends_at)
|
||||
return 0 unless ends_at
|
||||
|
||||
remaining = (ends_at - current_clocktime)
|
||||
remaining.negative? ? 0 : remaining
|
||||
end
|
||||
private_class_method :second_to_timeout
|
||||
|
||||
def self.current_clocktime
|
||||
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
end
|
||||
private_class_method :current_clocktime
|
||||
|
||||
class SelectableAddrinfos
|
||||
PRIORITY_ON_V6 = [:ipv6, :ipv4]
|
||||
PRIORITY_ON_V4 = [:ipv4, :ipv6]
|
||||
|
||||
def initialize
|
||||
@addrinfo_dict = {}
|
||||
@last_family = nil
|
||||
end
|
||||
|
||||
def add(family_name, addrinfos)
|
||||
@addrinfo_dict[family_name] = addrinfos
|
||||
end
|
||||
|
||||
def get
|
||||
return nil if empty?
|
||||
|
||||
if @addrinfo_dict.size == 1
|
||||
@addrinfo_dict.each { |_, addrinfos| return addrinfos.shift }
|
||||
end
|
||||
|
||||
precedences = case @last_family
|
||||
when :ipv4, nil then PRIORITY_ON_V6
|
||||
when :ipv6 then PRIORITY_ON_V4
|
||||
end
|
||||
|
||||
precedences.each do |family_name|
|
||||
addrinfo = @addrinfo_dict[family_name]&.shift
|
||||
next unless addrinfo
|
||||
|
||||
@last_family = family_name
|
||||
return addrinfo
|
||||
end
|
||||
end
|
||||
|
||||
def empty?
|
||||
@addrinfo_dict.all? { |_, addrinfos| addrinfos.empty? }
|
||||
end
|
||||
|
||||
def any?
|
||||
!empty?
|
||||
end
|
||||
end
|
||||
private_constant :SelectableAddrinfos
|
||||
|
||||
class NoHostnameResolutionQueue
|
||||
def waiting_pipe
|
||||
nil
|
||||
end
|
||||
|
||||
def add_resolved(_, _)
|
||||
raise StandardError, "This #{self.class} cannot respond to:"
|
||||
end
|
||||
|
||||
def add_error(_, _)
|
||||
raise StandardError, "This #{self.class} cannot respond to:"
|
||||
end
|
||||
|
||||
def get
|
||||
nil
|
||||
end
|
||||
|
||||
def opened?
|
||||
false
|
||||
end
|
||||
|
||||
def closed?
|
||||
true
|
||||
end
|
||||
|
||||
def close_all
|
||||
# Do nothing
|
||||
end
|
||||
end
|
||||
private_constant :NoHostnameResolutionQueue
|
||||
|
||||
class HostnameResolutionQueue
|
||||
def initialize(size)
|
||||
@size = size
|
||||
@taken_count = 0
|
||||
@rpipe, @wpipe = IO.pipe
|
||||
@queue = Queue.new
|
||||
@mutex = Mutex.new
|
||||
end
|
||||
|
||||
def waiting_pipe
|
||||
[@rpipe]
|
||||
end
|
||||
|
||||
def add_resolved(family, resolved_addrinfos)
|
||||
@mutex.synchronize do
|
||||
@queue.push [family, resolved_addrinfos]
|
||||
@wpipe.putc HOSTNAME_RESOLUTION_QUEUE_UPDATED
|
||||
end
|
||||
end
|
||||
|
||||
def add_error(family, error)
|
||||
@mutex.synchronize do
|
||||
@queue.push [family, error]
|
||||
@wpipe.putc HOSTNAME_RESOLUTION_QUEUE_UPDATED
|
||||
end
|
||||
end
|
||||
|
||||
def get
|
||||
return nil if @queue.empty?
|
||||
|
||||
res = nil
|
||||
|
||||
@mutex.synchronize do
|
||||
@rpipe.getbyte
|
||||
res = @queue.pop
|
||||
end
|
||||
|
||||
@taken_count += 1
|
||||
close_all if @taken_count == @size
|
||||
res
|
||||
end
|
||||
|
||||
def closed?
|
||||
@rpipe.closed?
|
||||
end
|
||||
|
||||
def opened?
|
||||
!closed?
|
||||
end
|
||||
|
||||
def close_all
|
||||
@queue.close unless @queue.closed?
|
||||
@rpipe.close unless @rpipe.closed?
|
||||
@wpipe.close unless @wpipe.closed?
|
||||
end
|
||||
end
|
||||
private_constant :HostnameResolutionQueue
|
||||
|
||||
class ConnectingSockets
|
||||
def initialize
|
||||
@socket_dict = {}
|
||||
end
|
||||
|
||||
def all
|
||||
@socket_dict.keys
|
||||
end
|
||||
|
||||
def add(socket, addrinfo)
|
||||
@socket_dict[socket] = addrinfo
|
||||
end
|
||||
|
||||
def delete(socket)
|
||||
@socket_dict.delete socket
|
||||
end
|
||||
|
||||
def empty?
|
||||
@socket_dict.empty?
|
||||
end
|
||||
|
||||
def each
|
||||
@socket_dict.keys.each do |socket|
|
||||
yield socket
|
||||
end
|
||||
end
|
||||
end
|
||||
private_constant :ConnectingSockets
|
||||
|
||||
def self.tcp_without_fast_fallback(host, port, local_host, local_port, connect_timeout:, resolv_timeout:, &block)
|
||||
last_error = nil
|
||||
ret = nil
|
||||
|
||||
|
@ -669,6 +1176,7 @@ class Socket < BasicSocket
|
|||
ret
|
||||
end
|
||||
end
|
||||
private_class_method :tcp_without_fast_fallback
|
||||
|
||||
# :stopdoc:
|
||||
def self.ip_sockets_port0(ai_list, reuseaddr)
|
||||
|
|
|
@ -669,6 +669,7 @@ SO_SETFIB nil Set the associated routing table for the socket (FreeBSD
|
|||
SO_RTABLE nil Set the routing table for this socket (OpenBSD)
|
||||
SO_INCOMING_CPU nil Receive the cpu attached to the socket (Linux 3.19)
|
||||
SO_INCOMING_NAPI_ID nil Receive the napi ID attached to a RX queue (Linux 4.12)
|
||||
SO_CONNECT_TIME nil Returns the number of seconds a socket has been connected. This option is only valid for connection-oriented protocols (Windows)
|
||||
|
||||
SOPRI_INTERACTIVE nil Interactive socket priority
|
||||
SOPRI_NORMAL nil Normal socket priority
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
#ifdef _WIN32
|
||||
# include <winsock2.h>
|
||||
# include <ws2tcpip.h>
|
||||
# include <mswsock.h>
|
||||
# include <iphlpapi.h>
|
||||
# if defined(_MSC_VER)
|
||||
# undef HAVE_TYPE_STRUCT_SOCKADDR_DL
|
||||
|
|
|
@ -35,6 +35,7 @@ extern "C++" { /* template without extern "C++" */
|
|||
#endif
|
||||
#include <winsock2.h>
|
||||
#include <ws2tcpip.h>
|
||||
#include <mswsock.h>
|
||||
#if !defined(_MSC_VER) || _MSC_VER >= 1400
|
||||
#include <iphlpapi.h>
|
||||
#endif
|
||||
|
|
|
@ -778,4 +778,243 @@ class TestSocket < Test::Unit::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
def test_tcp_socket_v6_hostname_resolved_earlier
|
||||
opts = %w[-rsocket -W1]
|
||||
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
|
||||
|
||||
begin;
|
||||
begin
|
||||
server = TCPServer.new("::1", 0)
|
||||
rescue Errno::EADDRNOTAVAIL # IPv6 is not supported
|
||||
exit
|
||||
end
|
||||
|
||||
server_thread = Thread.new { server.accept }
|
||||
port = server.addr[1]
|
||||
|
||||
Addrinfo.define_singleton_method(:getaddrinfo) do |_, _, family, *_|
|
||||
case family
|
||||
when Socket::AF_INET6 then [Addrinfo.tcp("::1", port)]
|
||||
when Socket::AF_INET then sleep(10); [Addrinfo.tcp("127.0.0.1", port)]
|
||||
end
|
||||
end
|
||||
|
||||
socket = Socket.tcp("localhost", port)
|
||||
assert_true(socket.remote_address.ipv6?)
|
||||
server_thread.value.close
|
||||
server.close
|
||||
socket.close if socket && !socket.closed?
|
||||
end;
|
||||
end
|
||||
|
||||
def test_tcp_socket_v4_hostname_resolved_earlier
|
||||
opts = %w[-rsocket -W1]
|
||||
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
|
||||
|
||||
begin;
|
||||
server = TCPServer.new("127.0.0.1", 0)
|
||||
port = server.addr[1]
|
||||
|
||||
Addrinfo.define_singleton_method(:getaddrinfo) do |_, _, family, *_|
|
||||
case family
|
||||
when Socket::AF_INET6 then sleep(10); [Addrinfo.tcp("::1", port)]
|
||||
when Socket::AF_INET then [Addrinfo.tcp("127.0.0.1", port)]
|
||||
end
|
||||
end
|
||||
|
||||
server_thread = Thread.new { server.accept }
|
||||
socket = Socket.tcp("localhost", port)
|
||||
assert_true(socket.remote_address.ipv4?)
|
||||
server_thread.value.close
|
||||
server.close
|
||||
socket.close if socket && !socket.closed?
|
||||
end;
|
||||
end
|
||||
|
||||
def test_tcp_socket_v6_hostname_resolved_in_resolution_delay
|
||||
opts = %w[-rsocket -W1]
|
||||
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
|
||||
|
||||
begin;
|
||||
begin
|
||||
server = TCPServer.new("::1", 0)
|
||||
rescue Errno::EADDRNOTAVAIL # IPv6 is not supported
|
||||
exit
|
||||
end
|
||||
|
||||
port = server.addr[1]
|
||||
delay_time = 0.025 # Socket::RESOLUTION_DELAY (private) is 0.05
|
||||
|
||||
Addrinfo.define_singleton_method(:getaddrinfo) do |_, _, family, *_|
|
||||
case family
|
||||
when Socket::AF_INET6 then sleep(delay_time); [Addrinfo.tcp("::1", port)]
|
||||
when Socket::AF_INET then [Addrinfo.tcp("127.0.0.1", port)]
|
||||
end
|
||||
end
|
||||
|
||||
server_thread = Thread.new { server.accept }
|
||||
socket = Socket.tcp("localhost", port)
|
||||
assert_true(socket.remote_address.ipv6?)
|
||||
server_thread.value.close
|
||||
server.close
|
||||
socket.close if socket && !socket.closed?
|
||||
end;
|
||||
end
|
||||
|
||||
def test_tcp_socket_v6_hostname_resolved_earlier_and_v6_server_is_not_listening
|
||||
opts = %w[-rsocket -W1]
|
||||
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
|
||||
|
||||
begin;
|
||||
ipv4_address = "127.0.0.1"
|
||||
ipv4_server = Socket.new(Socket::AF_INET, :STREAM)
|
||||
ipv4_server.bind(Socket.pack_sockaddr_in(0, ipv4_address))
|
||||
port = ipv4_server.connect_address.ip_port
|
||||
|
||||
Addrinfo.define_singleton_method(:getaddrinfo) do |_, _, family, *_|
|
||||
case family
|
||||
when Socket::AF_INET6 then [Addrinfo.tcp("::1", port)]
|
||||
when Socket::AF_INET then sleep(0.001); [Addrinfo.tcp(ipv4_address, port)]
|
||||
end
|
||||
end
|
||||
|
||||
ipv4_server_thread = Thread.new { ipv4_server.listen(1); ipv4_server.accept }
|
||||
socket = Socket.tcp("localhost", port)
|
||||
assert_equal(ipv4_address, socket.remote_address.ip_address)
|
||||
|
||||
accepted, _ = ipv4_server_thread.value
|
||||
accepted.close
|
||||
ipv4_server.close
|
||||
socket.close if socket && !socket.closed?
|
||||
end;
|
||||
end
|
||||
|
||||
def test_tcp_socket_resolv_timeout
|
||||
opts = %w[-rsocket -W1]
|
||||
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
|
||||
|
||||
begin;
|
||||
Addrinfo.define_singleton_method(:getaddrinfo) { |*_| sleep }
|
||||
port = TCPServer.new("localhost", 0).addr[1]
|
||||
|
||||
assert_raise(Errno::ETIMEDOUT) do
|
||||
Socket.tcp("localhost", port, resolv_timeout: 0.01)
|
||||
end
|
||||
end;
|
||||
end
|
||||
|
||||
def test_tcp_socket_resolv_timeout_with_connection_failure
|
||||
opts = %w[-rsocket -W1]
|
||||
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
|
||||
|
||||
begin;
|
||||
server = TCPServer.new("127.0.0.1", 12345)
|
||||
_, port, = server.addr
|
||||
|
||||
Addrinfo.define_singleton_method(:getaddrinfo) do |_, _, family, *_|
|
||||
if family == Socket::AF_INET6
|
||||
sleep
|
||||
else
|
||||
[Addrinfo.tcp("127.0.0.1", port)]
|
||||
end
|
||||
end
|
||||
|
||||
server.close
|
||||
|
||||
assert_raise(Errno::ETIMEDOUT) do
|
||||
Socket.tcp("localhost", port, resolv_timeout: 0.01)
|
||||
end
|
||||
end;
|
||||
end
|
||||
|
||||
def test_tcp_socket_one_hostname_resolution_succeeded_at_least
|
||||
opts = %w[-rsocket -W1]
|
||||
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
|
||||
|
||||
begin;
|
||||
begin
|
||||
server = TCPServer.new("::1", 0)
|
||||
rescue Errno::EADDRNOTAVAIL # IPv6 is not supported
|
||||
exit
|
||||
end
|
||||
|
||||
port = server.addr[1]
|
||||
|
||||
Addrinfo.define_singleton_method(:getaddrinfo) do |_, _, family, *_|
|
||||
case family
|
||||
when Socket::AF_INET6 then [Addrinfo.tcp("::1", port)]
|
||||
when Socket::AF_INET then sleep(0.001); raise SocketError
|
||||
end
|
||||
end
|
||||
|
||||
server_thread = Thread.new { server.accept }
|
||||
socket = nil
|
||||
|
||||
assert_nothing_raised do
|
||||
socket = Socket.tcp("localhost", port)
|
||||
end
|
||||
|
||||
server_thread.value.close
|
||||
server.close
|
||||
socket.close if socket && !socket.closed?
|
||||
end;
|
||||
end
|
||||
|
||||
def test_tcp_socket_all_hostname_resolution_failed
|
||||
opts = %w[-rsocket -W1]
|
||||
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
|
||||
|
||||
begin;
|
||||
Addrinfo.define_singleton_method(:getaddrinfo) do |_, _, family, *_|
|
||||
case family
|
||||
when Socket::AF_INET6 then raise SocketError
|
||||
when Socket::AF_INET then sleep(0.001); raise SocketError, "Last hostname resolution error"
|
||||
end
|
||||
end
|
||||
port = TCPServer.new("localhost", 0).addr[1]
|
||||
|
||||
assert_raise_with_message(SocketError, "Last hostname resolution error") do
|
||||
Socket.tcp("localhost", port)
|
||||
end
|
||||
end;
|
||||
end
|
||||
|
||||
def test_tcp_socket_v6_address_passed
|
||||
opts = %w[-rsocket -W1]
|
||||
assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}"
|
||||
|
||||
begin;
|
||||
begin
|
||||
server = TCPServer.new("::1", 0)
|
||||
rescue Errno::EADDRNOTAVAIL # IPv6 is not supported
|
||||
exit
|
||||
end
|
||||
|
||||
_, port, = server.addr
|
||||
|
||||
Addrinfo.define_singleton_method(:getaddrinfo) do |*_|
|
||||
[Addrinfo.tcp("::1", port)]
|
||||
end
|
||||
|
||||
server_thread = Thread.new { server.accept }
|
||||
socket = Socket.tcp("::1", port)
|
||||
|
||||
assert_true(socket.remote_address.ipv6?)
|
||||
server_thread.value.close
|
||||
server.close
|
||||
socket.close if socket && !socket.closed?
|
||||
end;
|
||||
end
|
||||
|
||||
def test_tcp_socket_fast_fallback_is_false
|
||||
server = TCPServer.new("127.0.0.1", 0)
|
||||
_, port, = server.addr
|
||||
server_thread = Thread.new { server.accept }
|
||||
socket = Socket.tcp("127.0.0.1", port, fast_fallback: false)
|
||||
|
||||
assert_true(socket.remote_address.ipv4?)
|
||||
server_thread.value.close
|
||||
server.close
|
||||
socket.close if socket && !socket.closed?
|
||||
end
|
||||
end if defined?(Socket)
|
||||
|
|
Загрузка…
Ссылка в новой задаче