CrackedRuby logo

CrackedRuby

Socket Programming

Overview

Ruby's socket programming implementation centers around the Socket class and its specialized subclasses. The standard library provides TCPSocket and TCPServer for TCP connections, UDPSocket for UDP communication, and UNIXSocket for Unix domain sockets. These classes abstract the underlying BSD socket API while maintaining access to low-level socket options.

The Socket class serves as the foundation, implementing the core socket functionality. TCPSocket handles stream-oriented, reliable connections between network endpoints. UDPSocket manages datagram-based communication without connection establishment. TCPServer creates listening sockets that accept incoming connections.

require 'socket'

# TCP client connection
client = TCPSocket.new('example.com', 80)
client.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
response = client.read
client.close

Ruby's socket implementation handles address resolution automatically when using hostnames. The library converts string addresses to appropriate socket address structures internally. IPv6 support operates transparently alongside IPv4, with the system selecting the appropriate protocol based on available addresses.

Socket objects inherit from IO, providing standard file operations like read, write, gets, and puts. This inheritance enables sockets to work with Ruby's IO multiplexing mechanisms and buffering systems.

# UDP communication
socket = UDPSocket.new
socket.send("Hello", 0, 'localhost', 5000)
data, addr = socket.recvfrom(1024)
socket.close

The socket library integrates with Ruby's exception hierarchy, raising specific error types for different failure conditions. Network timeouts, connection refusals, and hostname resolution failures generate distinct exception classes.

Basic Usage

TCP client connections require specifying the target host and port. Ruby resolves hostnames to IP addresses and establishes connections transparently. The TCPSocket constructor blocks until the connection completes or fails.

require 'socket'

# Basic TCP client
begin
  socket = TCPSocket.new('httpbin.org', 80)
  socket.puts "GET /get HTTP/1.1"
  socket.puts "Host: httpbin.org"
  socket.puts ""
  
  response = socket.read
  puts response
ensure
  socket&.close
end

TCP servers bind to specific addresses and ports, then listen for incoming connections. The TCPServer class handles the binding and listening automatically. The accept method blocks until a client connects, returning a new socket for communication.

# Basic TCP server
server = TCPServer.new('localhost', 3000)

loop do
  client = server.accept
  request = client.gets
  
  client.puts "HTTP/1.1 200 OK"
  client.puts "Content-Length: 13"
  client.puts ""
  client.puts "Hello, World!"
  
  client.close
end

UDP sockets operate without establishing connections. The send method transmits datagrams to specified addresses, while recvfrom receives data and returns the sender's address information.

# UDP server
server = UDPSocket.new
server.bind('localhost', 5000)

loop do
  data, addr = server.recvfrom(1024)
  puts "Received: #{data} from #{addr[2]}"
  
  server.send("Echo: #{data}", 0, addr[2], addr[1])
end

Socket options control various aspects of socket behavior. Common options include setting timeouts, enabling address reuse, and configuring buffer sizes. The setsockopt method applies these settings using system-level constants.

socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM)
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)

Unix domain sockets provide inter-process communication on the same machine. These sockets use filesystem paths instead of network addresses, offering better performance for local communication.

# Unix domain socket server
server = UNIXServer.new('/tmp/ruby_socket')

client = server.accept
message = client.gets
client.puts "Received: #{message}"
client.close

Advanced Usage

Ruby's Socket class provides direct access to low-level socket operations for applications requiring fine-grained control. Raw socket creation enables custom protocol implementation and network manipulation beyond standard TCP/UDP patterns.

# Low-level socket creation and configuration
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM, Socket::IPPROTO_TCP)

# Configure keep-alive
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1)
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_KEEPIDLE, 600)
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_KEEPINTVL, 60)
socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_KEEPCNT, 3)

# Manual connection establishment
addr = Socket.sockaddr_in(80, 'example.com')
socket.connect(addr)

socket.write("GET / HTTP/1.1\r\nHost: example.com\r\n\r\n")
response = socket.read(4096)
socket.close

Socket multiplexing allows handling multiple connections simultaneously without threading. The IO.select method monitors multiple sockets for read, write, or error conditions, returning arrays of ready sockets.

# Multiplexed server handling multiple clients
server = TCPServer.new('localhost', 3000)
sockets = [server]

loop do
  ready = IO.select(sockets, nil, nil, 1.0)
  next unless ready
  
  ready[0].each do |socket|
    if socket == server
      # Accept new connection
      client = server.accept
      sockets << client
      puts "Client connected: #{client.peeraddr[2]}"
    else
      # Handle client data
      begin
        data = socket.read_nonblock(1024)
        socket.write("Echo: #{data}")
      rescue IO::WaitReadable
        # No data available
      rescue EOFError
        # Client disconnected
        sockets.delete(socket)
        socket.close
      end
    end
  end
end

Non-blocking I/O operations prevent socket operations from blocking execution. Methods like read_nonblock and write_nonblock return immediately, raising exceptions when operations would block.

# Non-blocking client with manual connection handling
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM)
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)

begin
  addr = Socket.sockaddr_in(80, 'example.com')
  socket.connect_nonblock(addr)
rescue IO::WaitWritable
  # Connection in progress, wait for completion
  IO.select(nil, [socket], nil, 5.0)
  
  # Check connection result
  begin
    socket.connect_nonblock(addr)
  rescue Errno::EISCONN
    # Already connected
  rescue Errno::ECONNREFUSED
    raise "Connection refused"
  end
end

# Non-blocking write
data = "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n"
begin
  written = socket.write_nonblock(data)
  data = data[written..-1] if written < data.length
rescue IO::WaitWritable
  IO.select(nil, [socket], nil, 5.0)
  retry
end

Custom protocol implementation requires careful handling of framing and state management. Socket programming often involves building protocols on top of TCP's stream semantics.

class MessageSocket
  def initialize(socket)
    @socket = socket
    @buffer = ""
  end
  
  def send_message(message)
    data = [message.length].pack('N') + message
    @socket.write(data)
  end
  
  def receive_message
    # Ensure we have length header (4 bytes)
    while @buffer.length < 4
      @buffer += @socket.read(4 - @buffer.length)
    end
    
    # Extract message length
    length = @buffer[0, 4].unpack('N')[0]
    @buffer = @buffer[4..-1]
    
    # Read message body
    while @buffer.length < length
      @buffer += @socket.read(length - @buffer.length)
    end
    
    message = @buffer[0, length]
    @buffer = @buffer[length..-1]
    message
  end
  
  def close
    @socket.close
  end
end

Error Handling & Debugging

Socket operations generate various exception types corresponding to different failure modes. Network programming requires comprehensive error handling to manage connection failures, timeouts, and protocol errors gracefully.

Connection-related exceptions include Errno::ECONNREFUSED when target ports are closed, Errno::EHOSTUNREACH for routing failures, and Errno::ETIMEDOUT for connection timeouts. These exceptions inherit from SystemCallError, allowing generic handling of system-level errors.

def robust_connect(host, port, timeout = 10)
  socket = nil
  
  begin
    Timeout::timeout(timeout) do
      socket = TCPSocket.new(host, port)
    end
    
    return socket
    
  rescue Errno::ECONNREFUSED
    raise "Connection refused to #{host}:#{port}"
  rescue Errno::EHOSTUNREACH
    raise "Host unreachable: #{host}"
  rescue Errno::ETIMEDOUT, Timeout::Error
    raise "Connection timeout to #{host}:#{port}"
  rescue SocketError => e
    raise "DNS resolution failed: #{e.message}"
  rescue => e
    socket&.close
    raise "Connection failed: #{e.message}"
  end
end

I/O operations on sockets raise exceptions when connections terminate unexpectedly. EOFError indicates the remote end closed the connection, while Errno::EPIPE occurs when writing to closed sockets. Applications must handle these conditions to avoid crashes.

def safe_socket_read(socket, length)
  buffer = ""
  
  while buffer.length < length
    begin
      chunk = socket.read(length - buffer.length)
      
      if chunk.nil? || chunk.empty?
        raise EOFError, "Connection closed by peer"
      end
      
      buffer += chunk
      
    rescue Errno::ECONNRESET
      raise "Connection reset by peer"
    rescue Errno::EPIPE
      raise "Broken pipe - connection closed"
    rescue IO::WaitReadable
      # Would block in non-blocking mode
      IO.select([socket], nil, nil, 5.0) or raise "Read timeout"
      retry
    end
  end
  
  buffer
end

Debugging socket applications requires monitoring network traffic and connection states. Ruby provides methods to inspect socket addresses, peer information, and socket options. Logging connection events helps diagnose communication problems.

class DebuggingSocket
  def initialize(socket)
    @socket = socket
    @bytes_sent = 0
    @bytes_received = 0
    
    log "Connected to #{peer_address}"
  end
  
  def write(data)
    result = @socket.write(data)
    @bytes_sent += result
    log "Sent #{result} bytes (total: #{@bytes_sent})"
    result
  end
  
  def read(length = nil)
    data = @socket.read(length)
    @bytes_received += data&.length || 0
    log "Received #{data&.length || 0} bytes (total: #{@bytes_received})"
    data
  end
  
  def close
    log "Closing connection (sent: #{@bytes_sent}, received: #{@bytes_received})"
    @socket.close
  end
  
  private
  
  def peer_address
    addr = @socket.peeraddr
    "#{addr[2]}:#{addr[1]}"
  rescue
    "unknown"
  end
  
  def log(message)
    puts "[#{Time.now}] Socket #{object_id}: #{message}"
  end
end

Timeout handling prevents applications from hanging on slow or unresponsive network operations. Ruby's Timeout module provides deadline-based timeouts, while socket-level timeouts offer more granular control.

def configure_socket_timeouts(socket, read_timeout: 30, write_timeout: 30)
  # Set socket-level timeouts
  socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, 
                   [read_timeout, 0].pack("l_2"))
  socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, 
                   [write_timeout, 0].pack("l_2"))
  
  # Enable keep-alive for long-running connections
  socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1)
end

Thread Safety & Concurrency

Socket objects are not inherently thread-safe for simultaneous read and write operations. Multiple threads accessing the same socket require coordination to prevent data corruption and ensure proper protocol semantics.

Reading and writing from different threads requires careful synchronization. While the underlying socket implementation may handle concurrent access at the system level, application protocols often require atomic operations that span multiple socket calls.

class ThreadSafeSocket
  def initialize(socket)
    @socket = socket
    @write_mutex = Mutex.new
    @read_mutex = Mutex.new
  end
  
  def synchronized_write(data)
    @write_mutex.synchronize do
      header = [data.length].pack('N')
      @socket.write(header + data)
    end
  end
  
  def synchronized_read
    @read_mutex.synchronize do
      header = @socket.read(4)
      return nil if header.nil? || header.length < 4
      
      length = header.unpack('N')[0]
      @socket.read(length)
    end
  end
  
  def close
    @socket.close
  end
end

Connection pooling manages multiple socket connections across threads efficiently. Pools reduce connection overhead while ensuring thread isolation and proper resource cleanup.

class ConnectionPool
  def initialize(host, port, pool_size = 10)
    @host = host
    @port = port
    @pool = Queue.new
    @mutex = Mutex.new
    @created = 0
    @pool_size = pool_size
    
    # Pre-populate pool
    pool_size.times { @pool.push(create_connection) }
  end
  
  def with_connection
    connection = @pool.pop(true)
    
    begin
      yield connection
    ensure
      return_connection(connection)
    end
  rescue ThreadError
    # Pool empty, create new connection
    connection = create_connection
    begin
      yield connection
    ensure
      connection.close
    end
  end
  
  private
  
  def create_connection
    @mutex.synchronize do
      @created += 1
      puts "Creating connection #{@created}/#{@pool_size}"
    end
    
    TCPSocket.new(@host, @port)
  end
  
  def return_connection(connection)
    if connection_valid?(connection)
      @pool.push(connection)
    else
      connection.close
      @pool.push(create_connection)
    end
  end
  
  def connection_valid?(connection)
    !connection.closed? && connection.stat.readable?
  rescue
    false
  end
end

Threaded servers handle multiple clients concurrently by spawning threads for each connection. This approach requires managing thread lifecycles and preventing resource exhaustion from excessive thread creation.

class ThreadedServer
  def initialize(port, max_threads = 50)
    @server = TCPServer.new('localhost', port)
    @threads = []
    @mutex = Mutex.new
    @running = true
    @max_threads = max_threads
  end
  
  def start
    puts "Server listening on port #{@server.addr[1]}"
    
    while @running
      begin
        client = @server.accept
        
        if thread_count < @max_threads
          spawn_client_thread(client)
        else
          reject_client(client)
        end
        
      rescue => e
        puts "Accept error: #{e.message}"
      end
    end
    
    cleanup_threads
  end
  
  def stop
    @running = false
    @server.close
  end
  
  private
  
  def spawn_client_thread(client)
    thread = Thread.new do
      begin
        handle_client(client)
      ensure
        client.close
        remove_thread(Thread.current)
      end
    end
    
    @mutex.synchronize { @threads << thread }
  end
  
  def handle_client(client)
    request = client.gets
    client.puts "HTTP/1.1 200 OK"
    client.puts "Content-Length: 13"
    client.puts ""
    client.puts "Hello, World!"
  end
  
  def thread_count
    @mutex.synchronize { @threads.count(&:alive?) }
  end
  
  def remove_thread(thread)
    @mutex.synchronize { @threads.delete(thread) }
  end
  
  def reject_client(client)
    client.puts "HTTP/1.1 503 Service Unavailable"
    client.puts "Content-Length: 21"
    client.puts ""
    client.puts "Server overloaded"
    client.close
  end
  
  def cleanup_threads
    @mutex.synchronize do
      @threads.each(&:join)
      @threads.clear
    end
  end
end

Production Patterns

Production socket applications require robust error handling, monitoring, and graceful shutdown capabilities. Long-running servers must handle process signals, manage resources carefully, and provide operational visibility.

Signal handling enables graceful shutdown procedures that complete active connections before terminating. Production servers register signal handlers to manage shutdown sequences properly.

class ProductionServer
  def initialize(port)
    @server = TCPServer.new('0.0.0.0', port)
    @clients = {}
    @stats = {
      connections_accepted: 0,
      connections_active: 0,
      bytes_transferred: 0
    }
    @running = true
    
    setup_signal_handlers
  end
  
  def start
    puts "Production server starting on port #{@server.addr[1]}"
    
    while @running
      begin
        ready = IO.select([@server] + @clients.keys, nil, nil, 1.0)
        next unless ready
        
        ready[0].each do |socket|
          if socket == @server
            accept_client
          else
            handle_client_data(socket)
          end
        end
        
        cleanup_closed_clients
        
      rescue Interrupt
        break
      rescue => e
        log_error("Server error", e)
      end
    end
    
    shutdown
  end
  
  private
  
  def setup_signal_handlers
    trap('TERM') { graceful_shutdown }
    trap('INT')  { graceful_shutdown }
    trap('USR1') { log_stats }
  end
  
  def accept_client
    client = @server.accept
    @clients[client] = {
      connected_at: Time.now,
      bytes_read: 0,
      bytes_written: 0
    }
    
    @stats[:connections_accepted] += 1
    @stats[:connections_active] += 1
    
    puts "Client connected from #{client.peeraddr[2]} (active: #{@stats[:connections_active]})"
  end
  
  def handle_client_data(client)
    begin
      data = client.read_nonblock(4096)
      @clients[client][:bytes_read] += data.length
      
      response = "Echo: #{data}"
      written = client.write(response)
      @clients[client][:bytes_written] += written
      @stats[:bytes_transferred] += data.length + written
      
    rescue IO::WaitReadable
      # No data available
    rescue EOFError, Errno::ECONNRESET
      disconnect_client(client)
    end
  end
  
  def disconnect_client(client)
    info = @clients.delete(client)
    client.close
    @stats[:connections_active] -= 1
    
    if info
      duration = Time.now - info[:connected_at]
      puts "Client disconnected (duration: #{duration.round(2)}s, " \
           "read: #{info[:bytes_read]}, written: #{info[:bytes_written]})"
    end
  end
  
  def cleanup_closed_clients
    @clients.keys.each do |client|
      disconnect_client(client) if client.closed?
    end
  end
  
  def graceful_shutdown
    puts "Shutting down gracefully..."
    @running = false
  end
  
  def shutdown
    puts "Closing #{@clients.size} active connections"
    @clients.keys.each { |client| disconnect_client(client) }
    @server.close
    log_stats
  end
  
  def log_stats
    puts "Statistics:"
    puts "  Total connections: #{@stats[:connections_accepted]}"
    puts "  Active connections: #{@stats[:connections_active]}"
    puts "  Bytes transferred: #{@stats[:bytes_transferred]}"
    puts "  Memory usage: #{`ps -o rss= -p #{Process.pid}`.strip} KB"
  end
  
  def log_error(context, error)
    puts "[ERROR] #{context}: #{error.message}"
    puts error.backtrace.take(5).join("\n") if error.backtrace
  end
end

Connection management in production environments requires health checking, connection limits, and resource monitoring. Applications must handle network partitions and temporary failures gracefully.

class RobustClient
  RETRY_DELAYS = [0.1, 0.5, 1.0, 2.0, 5.0].freeze
  
  def initialize(host, port, options = {})
    @host = host
    @port = port
    @max_retries = options[:max_retries] || 5
    @connection_timeout = options[:connection_timeout] || 10
    @read_timeout = options[:read_timeout] || 30
    @socket = nil
  end
  
  def send_request(data)
    attempt = 0
    
    begin
      ensure_connected
      write_with_timeout(data)
      read_response
      
    rescue => e
      attempt += 1
      
      if attempt <= @max_retries && retryable_error?(e)
        delay = RETRY_DELAYS[attempt - 1] || RETRY_DELAYS.last
        puts "Request failed (attempt #{attempt}): #{e.message}, retrying in #{delay}s"
        
        close_connection
        sleep(delay)
        retry
      else
        raise "Request failed after #{attempt} attempts: #{e.message}"
      end
    end
  ensure
    close_connection if @socket&.closed?
  end
  
  private
  
  def ensure_connected
    return if @socket && !@socket.closed?
    
    @socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM)
    configure_socket_options
    
    addr = Socket.sockaddr_in(@port, @host)
    
    Timeout::timeout(@connection_timeout) do
      @socket.connect(addr)
    end
  end
  
  def configure_socket_options
    @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1)
    @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
    @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, 
                      [@read_timeout, 0].pack("l_2"))
  end
  
  def write_with_timeout(data)
    Timeout::timeout(@read_timeout) do
      @socket.write(data)
    end
  end
  
  def read_response
    Timeout::timeout(@read_timeout) do
      @socket.read
    end
  end
  
  def retryable_error?(error)
    case error
    when Errno::ECONNREFUSED, Errno::EHOSTUNREACH, 
         Errno::ETIMEDOUT, Timeout::Error, SocketError
      true
    else
      false
    end
  end
  
  def close_connection
    @socket&.close
    @socket = nil
  end
end

Reference

Socket Classes

Class Purpose Key Methods
Socket Low-level socket operations new, connect, bind, listen, accept
TCPSocket TCP client connections new(host, port), read, write, close
TCPServer TCP server sockets new(hostname, port), accept, listen
UDPSocket UDP datagram sockets new, bind, send, recvfrom
UNIXSocket Unix domain client new(path), standard I/O methods
UNIXServer Unix domain server new(path), accept

Socket Options

Constant Level Description
SO_REUSEADDR SOL_SOCKET Allow address reuse
SO_KEEPALIVE SOL_SOCKET Enable keep-alive packets
SO_RCVTIMEO SOL_SOCKET Receive timeout
SO_SNDTIMEO SOL_SOCKET Send timeout
TCP_NODELAY IPPROTO_TCP Disable Nagle algorithm
TCP_KEEPIDLE IPPROTO_TCP Keep-alive idle time

Address Families

Constant Description Usage
AF_INET IPv4 addresses Network sockets
AF_INET6 IPv6 addresses IPv6 network sockets
AF_UNIX Unix domain sockets Local IPC

Socket Types

Constant Description Protocol
SOCK_STREAM Stream sockets TCP
SOCK_DGRAM Datagram sockets UDP
SOCK_RAW Raw sockets Custom protocols

Common Exceptions

Exception Cause Handling Strategy
Errno::ECONNREFUSED Port closed/not listening Retry with backoff
Errno::EHOSTUNREACH Network routing failure Check network connectivity
Errno::ETIMEDOUT Connection/operation timeout Implement timeout handling
Errno::ECONNRESET Connection reset by peer Reconnect if appropriate
Errno::EPIPE Write to closed socket Check connection state
EOFError Unexpected connection close Handle graceful termination
SocketError DNS resolution failure Validate hostnames

I/O Multiplexing Methods

Method Parameters Returns Description
IO.select(read, write, error, timeout) Arrays of IO objects, timeout Array of ready sockets Monitor multiple sockets
socket.read_nonblock(length) length (Integer) String or raises exception Non-blocking read
socket.write_nonblock(data) data (String) Integer (bytes written) Non-blocking write
socket.connect_nonblock(addr) addr (Socket address) 0 or raises exception Non-blocking connect

Socket Information Methods

Method Returns Description
socket.addr Array Local address information
socket.peeraddr Array Remote peer address
socket.getsockopt(level, optname) Socket option value Get socket option
socket.setsockopt(level, optname, value) 0 Set socket option
socket.closed? Boolean Check if socket is closed

Address Conversion Utilities

Method Parameters Returns Description
Socket.sockaddr_in(port, host) port (Integer), host (String) String Create inet socket address
Socket.unpack_sockaddr_in(addr) addr (String) Array [port, host] Parse inet socket address
Socket.getaddrinfo(host, port) host, port, family, socktype Array Resolve address information
Socket.gethostname None String Get system hostname