Ruby TCP socket with read, write, and connect timeout

See Ruby's Net::Protocol implementation for an example on how to implement connect, read, and write timeouts: - https://github.com/ruby/ruby/blob/634e0a97eb82ab259c7f7a35d0486baebe77df0f/lib/net/protocol.rb#L40-L56 - https://github.com/ruby/ruby/blob/master/lib/net/protocol.rb#L210-L229 Or, write your own custom TCP socket implementation that supports read, write, and connect timeouts (warning, not tested): ```ruby require 'socket' require 'timeout' class TCPSocketWithTimeout class Error < StandardError; end class Timeout < StandardError; end class ReadTimeout < Timeout; end class WriteTimeout < Timeout; end attr_reader :host, :port, :tls, :read_timeout, :write_timeout, :connect_timeout, :socket def initialize(host:, port:, tls: false, connect_timeout: 5, read_timeout: 5, write_timeout: 5) @tls = tls @host = host @port = port @write_timeout = write_timeout @read_timeout = read_timeout @connect_timeout = connect_timeout end def ssl_context ctx = OpenSSL::SSL::SSLContext.new ctx.ssl_version = :TLSv1_2 ctx.ca_file = ca_file if ca_file ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER ctx end # NOTE: Upgrading Ruby 2.7 might change the Ruby's socket API def init_socket if tls sock = OpenSSL::SSL::SSLSocket.new( TCPSocket.open(host, port), # opens connection to server ssl_context ) # Close both socket and encrypted layer sock.sync_close = true else sock = Socket.new(:INET, :STREAM, 0) end sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) sock end def connect(timeout: CONNECT_TIMEOUT) @socket = init_socket deadline = Time.now.utc + timeout non_blocking(socket, deadline) do # NOTE: different method arity for non-SSL if tls socket.connect_nonblock else socket_address = Socket.pack_sockaddr_in(port, host) socket.connect_nonblock(socket_address) end end rescue Errno::EISCONN # Connection established rescue Timeout, Errno::ETIMEDOUT => e raise ConnectTimeout, "Connection timeout after #{timeout} seconds trying to connect to '#{host}:#{port}': #{e.class}: #{e.message}" rescue SystemCallError, IOError => e raise Error, "Connection failure while connecting to '#{host}:#{port}': #{e.class}: #{e.message}" end def read(timeout: READ_TIMEOUT, length: 1024) deadline = Time.now.utc + timeout non_blocking(socket, deadline) do socket.read_nonblock(length) end rescue Timeout raise ReadTimeout, "Timeout after #{timeout}s while reading data from #{host}:#{port}" rescue SystemCallError, IOError => e raise Error, "Connection error while reading data from #{host}:#{port} #{e.class}: #{e.message}" end def write(data, timeout: WRITE_TIMEOUT) deadline = Time.now.utc + timeout length = data.bytesize total_count = 0 non_blocking(socket, deadline) do loop do count = socket.write_nonblock(data) total_count += count return total_count if total_count >= length data = data.byteslice(count..-1) end end rescue Timeout raise WriteTimeout, "Timeout after #{timeout}s while writing data to #{host}:#{port}" rescue SystemCallError, IOError => e raise Error, "Connection error while writing data to #{host}:#{port} #{e.class}: #{e.message}" end def disconnect socket&.close end def non_blocking(socket, deadline) raise Error, "Socket #{host}:#{port} is closed" if closed? yield rescue IO::WaitReadable => e time_remaining = calculate_remaining_time(deadline) raise Timeout, e unless IO.select([socket], nil, nil, time_remaining) retry rescue IO::WaitWritable => e time_remaining = calculate_remaining_time(deadline) raise Timeout, e unless IO.select(nil, [socket], nil, time_remaining) retry end def calculate_remaining_time(deadline) time_remaining = deadline - Time.now.utc raise Timeout if time_remaining.negative? time_remaining end end sock = TCPSocketWithTimeout.new(host: 'localhost', port: 8888) sock.write "HELLO" puts "Writing done" ``` Reference: https://ruby-doc.org/core-2.6.2/IO.html#method-c-select https://workingwithruby.com/wwtcps/nonblocking/ A basic TCP server that can be used to test the client: ```ruby require 'socket' server = TCPServer.open(8888) while client = server.accept puts "Accepted" puts "Received #{client.read}" puts "Wrote #{client.write('Hello back')}" client.close end ``` ## Notes Use non-blocking methods and IO.select to implement timeouts using non-blocking methods. Use TCPSocket with TLS connections. Use Socket with SSL connections. TCPSocket in Ruby version 3 and greater includes a connect_timeout parameter in the constructor: https://ruby-doc.org/stdlib-3.0.0/libdoc/socket/rdoc/TCPSocket.html Ruby's socket API is a work in progress... ## References Socket connection timeout in Ruby: https://spin.atomicobject.com/2013/09/30/socket-connection-timeout-ruby/ Working with TCP sockets: https://workingwithruby.com/downloads/Working%20With%20TCP%20Sockets.pdf Ruby's socket API and related documentation is not that great, so you might need to read the source: https://github.com/ruby/ruby/blob/v2_7_6/ext/socket/tcpsocket.c https://github.com/ruby/ruby/blob/v2_7_6/ext/socket/socket.c https://github.com/ruby/ruby/blob/v2_7_6/test/openssl/test_ssl.rb