/lib/cool.io/socket.rb
Ruby | 233 lines | 160 code | 45 blank | 28 comment | 8 complexity | c126fe1c89c569332e6c8ee8e1b2d7e2 MD5 | raw file
Possible License(s): BSD-3-Clause, BSD-2-Clause
- #--
- # Copyright (C)2007 Tony Arcieri
- # You can redistribute this under the terms of the Ruby license
- # See file LICENSE for details
- #++
- require 'socket'
- require 'resolv'
- module Coolio
- class Socket < IO
- def self.connect(socket, *args)
- new(socket, *args).instance_eval do
- @_connector = Connector.new(self, socket)
- self
- end
- end
- # Just initializes some instance variables to avoid
- # warnings and calls super().
- def initialize *args
- @_failed = nil
- @_connector = nil
- super
- end
- watcher_delegate :@_connector
- remove_method :attach
- def attach(evloop)
- raise RuntimeError, "connection failed" if @_failed
- if @_connector
- @_connector.attach(evloop)
- return self
- end
- super
- end
- # Called upon completion of a socket connection
- def on_connect; end
- event_callback :on_connect
- # Called if a socket connection failed to complete
- def on_connect_failed; end
- event_callback :on_connect_failed
- # Called if a hostname failed to resolve when connecting
- # Defaults to calling on_connect_failed
- alias_method :on_resolve_failed, :on_connect_failed
- #########
- protected
- #########
- class Connector < IOWatcher
- def initialize(coolio_socket, ruby_socket)
- @coolio_socket, @ruby_socket = coolio_socket, ruby_socket
- super(ruby_socket, :w)
- end
- def on_writable
- evl = evloop
- detach
- if connect_successful?
- @coolio_socket.instance_eval { @_connector = nil }
- @coolio_socket.attach(evl)
- @ruby_socket.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, [1].pack("l"))
- @ruby_socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_KEEPALIVE, true)
- @coolio_socket.__send__(:on_connect)
- else
- @coolio_socket.instance_eval { @_failed = true }
- @coolio_socket.__send__(:on_connect_failed)
- end
- end
- #######
- private
- #######
- def connect_successful?
- @ruby_socket.getsockopt(::Socket::SOL_SOCKET, ::Socket::SO_ERROR).unpack('i').first == 0
- rescue IOError
- false
- end
- end
- end
- class TCPSocket < Socket
- attr_reader :remote_host, :remote_addr, :remote_port, :address_family
- watcher_delegate :@_resolver
- # Similar to .new, but used in cases where the resulting object is in a
- # "half-open" state. This is primarily used for when asynchronous
- # DNS resolution is taking place. We don't actually have a handle to
- # the socket we want to use to create the watcher yet, since we don't
- # know the IP address to connect to.
- def self.precreate(*args, &block)
- obj = allocate
- obj.__send__(:preinitialize, *args, &block)
- obj
- end
- # Perform a non-blocking connect to the given host and port
- # see examples/echo_client.rb
- # addr is a string, can be an IP address or a hostname.
- def self.connect(addr, port, *args)
- family = nil
- if (Resolv::IPv4.create(addr) rescue nil)
- family = ::Socket::AF_INET
- elsif(Resolv::IPv6.create(addr) rescue nil)
- family = ::Socket::AF_INET6
- end
- if family
- return super(TCPConnectSocket.new(family, addr, port), *args) # this creates a 'real' write buffer so we're ok there with regards to already having a write buffer from the get go
- end
- if host = Coolio::DNSResolver.hosts(addr)
- return connect(host, port, *args) # calls this same function
- end
- precreate(addr, port, *args)
- end
- # Called by precreate during asyncronous DNS resolution
- def preinitialize(addr, port, *args)
- @_write_buffer = ::IO::Buffer.new # allow for writing BEFORE DNS has resolved
- @remote_host, @remote_addr, @remote_port = addr, addr, port
- @_resolver = TCPConnectResolver.new(self, addr, port, *args)
- end
- private :preinitialize
- PEERADDR_FAILED = ["?", 0, "name resolusion failed", "?"]
- def initialize(socket)
- unless socket.is_a?(::TCPSocket) or socket.is_a?(TCPConnectSocket)
- raise TypeError, "socket must be a TCPSocket"
- end
- super
- @address_family, @remote_port, @remote_host, @remote_addr = (socket.peeraddr rescue PEERADDR_FAILED)
- end
- def peeraddr
- [@address_family, @remote_port, @remote_host, @remote_addr]
- end
- #########
- protected
- #########
- class TCPConnectSocket < ::Socket
- def initialize(family, addr, port, host = addr)
- @host, @addr, @port = host, addr, port
- @address_family = nil
- super(family, ::Socket::SOCK_STREAM, 0)
- begin
- connect_nonblock(::Socket.sockaddr_in(port, addr))
- rescue Errno::EINPROGRESS
- end
- end
- def peeraddr
- [
- @address_family == ::Socket::AF_INET ? 'AF_INET' : 'AF_INET6',
- @port,
- @host,
- @addr
- ]
- end
- end
- class TCPConnectResolver < Coolio::DNSResolver
- def initialize(socket, host, port, *args)
- @sock, @host, @port, @args = socket, host, port, args
- super(host)
- end
- def on_success(addr)
- host, port, args = @host, @port, @args
- @sock.instance_eval do
- # DNSResolver only supports IPv4 so we can safely assume IPv4 address
- begin
- socket = TCPConnectSocket.new(::Socket::AF_INET, addr, port, host)
- rescue Errno::ENETUNREACH
- on_connect_failed
- return
- end
- initialize(socket, *args)
- @_connector = Socket::Connector.new(self, socket)
- @_resolver = nil
- end
- @sock.attach(evloop)
- end
- def on_failure
- @sock.__send__(:on_resolve_failed)
- @sock.instance_eval do
- @_resolver = nil
- @_failed = true
- end
- return
- end
- end
- end
- class UNIXSocket < Socket
- attr_reader :path, :address_family
- # Connect to the given UNIX domain socket
- def self.connect(path, *args)
- new(::UNIXSocket.new(path), *args)
- end
- def initialize(socket)
- raise ArgumentError, "socket must be a UNIXSocket" unless socket.is_a? ::UNIXSocket
- super
- @address_family, @path = socket.peeraddr
- end
- end
- end