CrackedRuby CrackedRuby

Socket Programming Concepts

Overview

Sockets provide an abstraction for network communication between processes, either on the same machine or across networks. A socket serves as an endpoint for sending and receiving data, identified by an IP address and port number combination. The socket API originated in BSD Unix in the early 1980s and has become the standard interface for network programming across operating systems.

Network communication through sockets follows a client-server model. The server creates a socket, binds it to an address and port, and listens for incoming connections. Clients create sockets and initiate connections to server addresses. Once connected, both sides exchange data through read and write operations on the socket file descriptors.

Socket programming operates at the transport layer of the network stack, typically using TCP (Transmission Control Protocol) or UDP (User Datagram Protocol). TCP provides reliable, ordered, connection-oriented communication with built-in error checking and flow control. UDP offers connectionless, best-effort delivery with lower overhead but no guarantees of packet delivery or ordering.

# Basic TCP server socket
require 'socket'

server = TCPServer.new(8080)
puts "Server listening on port 8080"

client = server.accept
puts "Client connected from #{client.peeraddr[3]}"

data = client.gets
client.puts "Echo: #{data}"
client.close

The socket abstraction hides network complexity behind familiar file I/O operations. Applications read from and write to sockets using similar methods as file handling, while the operating system manages packet transmission, routing, and protocol implementation. This abstraction enables portable network code across different platforms and network types.

Key Principles

Socket communication operates on several fundamental principles that determine how data flows between networked processes. Understanding these principles is essential for designing reliable and efficient network applications.

Address Families and Domains

Sockets belong to address families that define the format of addresses and the communication domain. The AF_INET family uses IPv4 addresses (32-bit addresses like 192.168.1.1). AF_INET6 uses IPv6 addresses (128-bit addresses like 2001:0db8::1). AF_UNIX (also called AF_LOCAL) uses filesystem paths for inter-process communication on the same machine, offering higher performance than network sockets for local communication.

Each socket type serves different communication needs. Stream sockets (SOCK_STREAM) provide reliable, bidirectional, connection-oriented byte streams, typically using TCP. Datagram sockets (SOCK_DGRAM) send discrete messages without establishing connections, typically using UDP. Raw sockets (SOCK_RAW) access lower-level protocols and require elevated privileges.

Connection-Oriented Communication

TCP sockets establish connections through a three-way handshake before data transmission. The client sends a SYN packet, the server responds with SYN-ACK, and the client completes the handshake with an ACK. This process ensures both sides are ready for communication and agree on initial sequence numbers.

The connection lifecycle for TCP follows a specific sequence. The server creates a socket with socket(), binds it to an address with bind(), marks it as a listening socket with listen(), and waits for connections with accept(). Each call to accept() blocks until a client connects and returns a new socket for that connection. The server continues accepting new connections while handling existing ones, typically using threads or multiplexing.

Clients create a socket and call connect() with the server's address. The connect() call blocks until the connection completes or times out. After successful connection, both client and server use the same read/write operations to exchange data. Connection teardown uses close() or shutdown(), triggering a four-way FIN/ACK sequence.

Connectionless Communication

UDP sockets skip connection establishment and send datagrams directly to destination addresses. Each datagram includes source and destination addresses, allowing communication with multiple peers through a single socket. Applications must handle packet loss, duplication, and reordering themselves.

The recvfrom() and sendto() methods handle datagram communication. sendto() specifies the destination address with each transmission. recvfrom() returns both the data and the sender's address, enabling response routing. UDP sockets can also use connect() to associate with a specific remote address, allowing use of read() and write() instead of sendto() and recvfrom().

Socket States and Blocking Behavior

Sockets operate in blocking or non-blocking mode. Blocking sockets cause operations like accept(), recv(), and connect() to suspend the calling thread until data arrives or the operation completes. This simplifies programming but limits concurrency. Non-blocking sockets return immediately with an error code if the operation cannot complete instantly.

Socket buffers store data temporarily during transmission. The send buffer holds outbound data until transmission, while the receive buffer stores incoming data before application retrieval. Buffer sizes affect performance and memory usage. When buffers fill, send operations block (in blocking mode) or return errors (in non-blocking mode).

Multiplexing and Concurrency

Single-threaded servers handle multiple connections through multiplexing mechanisms like select() or poll(). These functions monitor multiple sockets and report which ones have data available or can accept writes without blocking. The application processes ready sockets in a loop, providing concurrency without threads.

Multiplexing supports three event types: readable (data available or connection pending), writable (send buffer has space), and exceptional (out-of-band data or errors). Applications create sets of sockets to monitor and call select() or poll(), which blocks until events occur on any monitored socket. The function returns a modified set indicating ready sockets.

Error Handling and Reliability

Network operations fail for various reasons: connection refused, host unreachable, timeout, network failure, or buffer overflow. Applications must handle these errors appropriately. Connection errors typically require retry logic with exponential backoff. Timeout errors may indicate network congestion or unresponsive peers.

TCP handles reliability at the transport layer through acknowledgments, retransmission, and flow control. Applications see an abstraction of reliable, ordered byte streams. However, TCP cannot prevent application-level issues like incomplete messages if connections close mid-transmission. Applications implement framing protocols to detect message boundaries and handle partial transmissions.

Ruby Implementation

Ruby provides socket programming through the Socket class and higher-level wrappers in the standard library. The Socket class exposes low-level socket operations matching BSD sockets, while classes like TCPServer, TCPSocket, UDPSocket, and UNIXSocket offer cleaner interfaces for common patterns.

TCP Server Implementation

TCPServer creates listening sockets that accept client connections. The constructor takes a port number and optional bind address. The accept method blocks until a connection arrives and returns a TCPSocket for that client.

require 'socket'

server = TCPServer.new(9000)

loop do
  client = server.accept
  
  Thread.new(client) do |connection|
    begin
      request = connection.gets
      response = process_request(request)
      connection.puts response
    ensure
      connection.close
    end
  end
end

This pattern creates a new thread for each connection, allowing concurrent client handling. The ensure block guarantees socket closure even if errors occur. Thread-per-connection scales to moderate loads but faces resource limits with many connections.

TCP Client Implementation

TCPSocket connects to remote servers. The constructor takes the remote host and port, automatically performing DNS resolution and connection establishment. Connection errors raise Errno exceptions.

require 'socket'

def fetch_http(host, path)
  socket = TCPSocket.new(host, 80)
  
  socket.print "GET #{path} HTTP/1.1\r\n"
  socket.print "Host: #{host}\r\n"
  socket.print "Connection: close\r\n"
  socket.print "\r\n"
  
  response = socket.read
  socket.close
  
  response
end

This example implements a basic HTTP client. The print methods send request headers, and read retrieves the complete response. Connection closure happens automatically when the server closes its end, allowing read to return all data.

UDP Socket Implementation

UDPSocket handles datagram communication. Unlike TCP, no connection setup occurs. The send method transmits datagrams to specified addresses, and recvfrom receives datagrams along with sender information.

require 'socket'

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

loop do
  data, sender = server.recvfrom(1024)
  client_ip = sender[3]
  client_port = sender[1]
  
  response = "Received: #{data}"
  server.send(response, 0, client_ip, client_port)
end

The recvfrom method's second parameter specifies maximum datagram size. The method returns an array containing the data and sender information (port, address family, IP address, hostname). The send method's second parameter specifies flags (typically 0).

Socket Options and Configuration

Socket options control socket behavior at various protocol levels. Common options include timeouts, buffer sizes, address reuse, and keepalive settings. Ruby accesses these through setsockopt and getsockopt methods.

require 'socket'

server = TCPServer.new(8080)

# Reuse address immediately after close
server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)

# Set receive timeout to 5 seconds
timeout = [5, 0].pack('l_2')
server.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, timeout)

# Configure TCP keepalive
server.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, 1)
server.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE, 60)
server.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL, 10)
server.setsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT, 3)

SO_REUSEADDR allows binding to addresses in TIME_WAIT state, useful during development when servers restart frequently. SO_RCVTIMEO and SO_SNDTIMEO prevent indefinite blocking on I/O operations. Keepalive options detect dead connections by sending periodic probes.

Non-Blocking I/O

Ruby supports non-blocking socket operations through fcntl or the Socket::NONBLOCK flag. Non-blocking sockets return immediately from I/O operations, raising IO::WaitReadable or IO::WaitWritable when operations would block.

require 'socket'

server = TCPServer.new(8080)
server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)

# Set non-blocking mode
server.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)

loop do
  begin
    client = server.accept_nonblock
    handle_client(client)
  rescue IO::WaitReadable
    # No connection available, do other work
    IO.select([server])
    retry
  end
end

This pattern attempts non-blocking accept and rescues WaitReadable when no connection is pending. IO.select blocks until the server socket becomes readable, indicating a pending connection. The retry restarts the accept attempt.

Socket Multiplexing with IO.select

IO.select monitors multiple sockets simultaneously, returning when any become ready for I/O. The method accepts arrays of sockets to monitor for reading, writing, or errors, plus an optional timeout.

require 'socket'

server = TCPServer.new(8080)
clients = []

loop do
  readable, writable, errors = IO.select([server] + clients, [], [], 5)
  
  next unless readable
  
  readable.each do |socket|
    if socket == server
      # New connection
      clients << server.accept
    else
      # Existing client has data
      begin
        data = socket.read_nonblock(1024)
        socket.write_nonblock("Echo: #{data}")
      rescue EOFError
        # Client disconnected
        clients.delete(socket)
        socket.close
      end
    end
  end
end

This event loop handles new connections and client data through a single thread. The select call blocks for up to 5 seconds or until sockets become ready. The readable array contains sockets with available data or pending connections.

Unix Domain Sockets

UNIXSocket and UNIXServer provide inter-process communication through filesystem paths. These sockets offer better performance than TCP sockets for local communication and inherit filesystem permissions.

require 'socket'

# Server
File.delete('/tmp/app.sock') if File.exist?('/tmp/app.sock')
server = UNIXServer.new('/tmp/app.sock')

Thread.new do
  loop do
    client = server.accept
    data = client.recv(1024)
    client.send("Processed: #{data}", 0)
    client.close
  end
end

# Client
socket = UNIXSocket.new('/tmp/app.sock')
socket.send('Hello', 0)
response = socket.recv(1024)
socket.close

Unix sockets create files at the specified path. Applications must remove existing socket files before binding. The socket file persists after the server closes, requiring cleanup during shutdown. Permissions control which processes can connect.

Implementation Approaches

Socket programming supports various architectural patterns depending on concurrency requirements, performance goals, and application complexity. The choice of approach affects scalability, resource usage, and code complexity.

Single-Threaded Sequential Processing

The simplest approach handles one connection at a time. The server accepts a connection, processes the request, sends a response, closes the connection, and loops to accept the next connection. This pattern works for low-traffic services or applications where connection setup overhead dominates processing time.

require 'socket'

server = TCPServer.new(8080)

loop do
  client = server.accept
  
  # Handle request completely before accepting next connection
  request = client.read
  response = process_request(request)
  client.write(response)
  client.close
end

Sequential processing offers simplicity and predictability. No concurrency management is needed, eliminating race conditions and synchronization issues. However, clients must wait for previous requests to complete, causing poor responsiveness under load. This approach suits scenarios like simple configuration servers or development environments.

Thread-Per-Connection Model

Creating a thread for each connection enables concurrent client handling. The main thread accepts connections and spawns worker threads to handle requests. Threads share process memory, allowing easy data sharing but requiring synchronization for shared state.

require 'socket'

server = TCPServer.new(8080)

loop do
  client = server.accept
  
  Thread.new(client) do |connection|
    begin
      while data = connection.gets
        response = process_data(data)
        connection.puts(response)
      end
    rescue => e
      puts "Error handling client: #{e.message}"
    ensure
      connection.close
    end
  end
end

Thread-per-connection scales to hundreds of concurrent connections on modern systems. Ruby's Global Interpreter Lock (GIL) prevents true parallel execution of Ruby code, but threads still benefit I/O-bound workloads. The operating system switches between threads during I/O waits, maintaining responsiveness. Thread creation overhead and memory usage limit scalability to thousands of connections.

Process-Per-Connection Model

Forking processes for each connection provides isolation between clients. Unlike threads, processes have separate memory spaces, preventing one connection from affecting others. Process isolation improves security and stability but increases resource usage.

require 'socket'

server = TCPServer.new(8080)

loop do
  client = server.accept
  
  pid = fork do
    # Child process handles connection
    server.close  # Close listening socket in child
    
    handle_client(client)
    client.close
    exit
  end
  
  # Parent process continues accepting
  client.close  # Close client socket in parent
  Process.detach(pid)  # Prevent zombie processes
end

The parent process retains the listening socket after forking, while children close it to prevent descriptor leaks. Similarly, the parent closes client sockets it doesn't use. Process.detach prevents zombie processes by automatically reaping terminated children. This model supports preforking, where processes are created before connections arrive.

Reactor Pattern with IO.select

The reactor pattern multiplexes multiple connections through event-driven programming. A single thread monitors all sockets, processing I/O when sockets become ready. This approach eliminates threading overhead and scales to thousands of connections.

require 'socket'

class Reactor
  def initialize(port)
    @server = TCPServer.new(port)
    @clients = {}
    @running = true
  end
  
  def run
    while @running
      readable, writable, errors = IO.select(
        [@server] + @clients.keys,
        @clients.select { |k, v| v[:buffer].any? }.keys,
        [],
        1
      )
      
      readable&.each { |socket| handle_readable(socket) }
      writable&.each { |socket| handle_writable(socket) }
    end
  end
  
  private
  
  def handle_readable(socket)
    if socket == @server
      accept_connection
    else
      receive_data(socket)
    end
  end
  
  def accept_connection
    client = @server.accept
    client.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)
    @clients[client] = { buffer: [] }
  end
  
  def receive_data(socket)
    data = socket.read_nonblock(4096)
    response = process_data(data)
    @clients[socket][:buffer] << response
  rescue EOFError, Errno::ECONNRESET
    @clients.delete(socket)
    socket.close
  end
  
  def handle_writable(socket)
    data = @clients[socket][:buffer].shift
    socket.write_nonblock(data)
  rescue Errno::EPIPE
    @clients.delete(socket)
    socket.close
  end
end

The reactor maintains state for each connection, including pending data to write. IO.select monitors readable sockets (including the server for new connections) and sockets with pending writes. The pattern processes events synchronously, preventing one slow handler from blocking others by keeping handlers fast.

Thread Pool Architecture

Thread pools limit the number of concurrent threads, balancing concurrency and resource usage. A fixed-size pool of worker threads processes connections from a queue. The main thread accepts connections and enqueues them for workers.

require 'socket'
require 'thread'

class ThreadPoolServer
  def initialize(port, pool_size)
    @server = TCPServer.new(port)
    @queue = Queue.new
    @workers = pool_size.times.map { spawn_worker }
  end
  
  def run
    loop do
      client = @server.accept
      @queue.push(client)
    end
  end
  
  private
  
  def spawn_worker
    Thread.new do
      loop do
        client = @queue.pop
        handle_client(client)
        client.close
      rescue => e
        puts "Worker error: #{e.message}"
      end
    end
  end
  
  def handle_client(client)
    request = client.read
    response = process_request(request)
    client.write(response)
  end
end

Thread pools prevent unbounded thread creation while maintaining concurrency. Queue depth indicates system load; growing queues signal capacity issues. Pool size tuning balances CPU usage and responsiveness. Ruby's Queue class provides thread-safe enqueueing and dequeueing with built-in blocking when empty.

Event Machine and Async I/O Libraries

External libraries like EventMachine provide sophisticated event-driven architectures. These libraries handle socket multiplexing, event dispatching, and connection management, allowing focus on application logic.

EventMachine uses callbacks for connection events (receive data, connection closed, etc.). The library manages the event loop, calling application code when events occur. This inverts control compared to traditional blocking I/O, where application code explicitly waits for data.

The callback-based approach requires different thinking about program flow. Applications cannot block waiting for responses; they must register callbacks that execute when responses arrive. This leads to callback chains that can become complex in applications with multiple asynchronous operations.

Practical Examples

Socket programming implementations demonstrate various communication patterns and techniques applicable to real-world scenarios. These examples progress from basic protocols to more complex interactions.

Echo Server with Error Handling

An echo server demonstrates fundamental socket operations with proper error handling and resource management. The server receives data and sends it back unchanged.

require 'socket'

class EchoServer
  def initialize(port)
    @server = TCPServer.new(port)
    @server.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1)
    puts "Echo server listening on port #{port}"
  end
  
  def run
    loop do
      begin
        client = @server.accept
        Thread.new(client) { |conn| handle_client(conn) }
      rescue Interrupt
        puts "\nShutting down..."
        break
      rescue => e
        puts "Error accepting connection: #{e.message}"
      end
    end
  ensure
    @server.close if @server
  end
  
  private
  
  def handle_client(client)
    client_addr = client.peeraddr[3]
    puts "Client connected: #{client_addr}"
    
    while line = client.gets
      client.puts line
      puts "#{client_addr}: #{line.chomp}"
    end
  rescue Errno::ECONNRESET, Errno::EPIPE, IOError
    puts "Client #{client_addr} disconnected"
  ensure
    client.close
  end
end

server = EchoServer.new(3000)
server.run

This implementation handles interrupts gracefully for shutdown, catches connection errors without crashing, and ensures socket cleanup. The peeraddr method retrieves client connection information for logging. Thread-per-connection allows multiple simultaneous clients.

HTTP Request Client

Creating HTTP requests through raw sockets demonstrates protocol implementation and request/response handling. This client supports basic GET requests with header parsing.

require 'socket'
require 'uri'

class SimpleHTTPClient
  def get(url)
    uri = URI.parse(url)
    
    socket = TCPSocket.new(uri.host, uri.port || 80)
    socket.setsockopt(Socket::SOL_TCP, Socket::TCP_NODELAY, 1)
    
    # Build HTTP request
    request = build_request(uri)
    socket.print request
    
    # Read response
    response = read_response(socket)
    socket.close
    
    response
  end
  
  private
  
  def build_request(uri)
    path = uri.path.empty? ? '/' : uri.path
    path += "?#{uri.query}" if uri.query
    
    [
      "GET #{path} HTTP/1.1",
      "Host: #{uri.host}",
      "User-Agent: SimpleHTTPClient/1.0",
      "Accept: */*",
      "Connection: close",
      "",
      ""
    ].join("\r\n")
  end
  
  def read_response(socket)
    # Read status line
    status_line = socket.gets
    status_code = status_line.split[1].to_i
    
    # Read headers
    headers = {}
    while line = socket.gets
      break if line.strip.empty?
      key, value = line.split(':', 2)
      headers[key.strip.downcase] = value.strip
    end
    
    # Read body
    body = if headers['content-length']
      socket.read(headers['content-length'].to_i)
    else
      socket.read
    end
    
    { status: status_code, headers: headers, body: body }
  end
end

client = SimpleHTTPClient.new
response = client.get('http://example.com/')
puts "Status: #{response[:status]}"
puts response[:body]

TCP_NODELAY disables Nagle's algorithm for immediate packet transmission. The request follows HTTP/1.1 format with required headers. Response parsing handles both content-length and connection-close body delimiting. This demonstrates protocol-level socket programming beyond library abstractions.

Chat Server with Broadcasting

A chat server illustrates connection management and message broadcasting. Multiple clients connect, and messages from any client broadcast to all others.

require 'socket'

class ChatServer
  def initialize(port)
    @server = TCPServer.new(port)
    @clients = []
    @mutex = Mutex.new
    puts "Chat server started on port #{port}"
  end
  
  def run
    loop do
      Thread.new(@server.accept) do |client|
        handle_client(client)
      end
    end
  end
  
  private
  
  def handle_client(client)
    nickname = client.gets.chomp
    
    @mutex.synchronize do
      @clients << { socket: client, nickname: nickname }
      broadcast("#{nickname} joined the chat", client)
    end
    
    while message = client.gets
      @mutex.synchronize do
        broadcast("#{nickname}: #{message}", client)
      end
    end
  rescue
    # Client disconnected
  ensure
    @mutex.synchronize do
      @clients.delete_if { |c| c[:socket] == client }
      broadcast("#{nickname} left the chat", client) if nickname
    end
    client.close
  end
  
  def broadcast(message, sender)
    @clients.each do |client|
      next if client[:socket] == sender
      begin
        client[:socket].puts message
      rescue
        # Remove dead connections
        @clients.delete(client)
      end
    end
  end
end

server = ChatServer.new(5000)
server.run

Mutual exclusion protects the client list from concurrent modification. Broadcasting sends messages to all connected clients except the sender. Error handling during broadcast removes dead connections without affecting others. The server expects clients to send nicknames as the first message.

UDP Time Server

A UDP-based time server demonstrates datagram communication. Clients send requests and receive timestamps without connection establishment.

require 'socket'

class TimeServer
  def initialize(port)
    @socket = UDPSocket.new
    @socket.bind('0.0.0.0', port)
    puts "Time server listening on UDP port #{port}"
  end
  
  def run
    loop do
      data, sender = @socket.recvfrom(1024)
      client_ip = sender[3]
      client_port = sender[1]
      
      puts "Request from #{client_ip}:#{client_port}"
      
      response = case data.strip
      when 'TIME'
        Time.now.to_s
      when 'TIMESTAMP'
        Time.now.to_i.to_s
      when 'ISO8601'
        Time.now.iso8601
      else
        "Unknown command. Use TIME, TIMESTAMP, or ISO8601"
      end
      
      @socket.send(response, 0, client_ip, client_port)
    end
  end
end

# Server
server = TimeServer.new(3000)
server.run

# Client
client = UDPSocket.new
client.send('TIME', 0, 'localhost', 3000)
response, _ = client.recvfrom(1024)
puts "Server time: #{response}"
client.close

UDP communication requires no connection setup. Each datagram stands alone; the server processes requests independently. The recvfrom return value includes sender information for responses. UDP suits scenarios where occasional packet loss is acceptable and low latency matters more than reliability.

Port Scanner

A port scanner demonstrates concurrent connection attempts with timeout handling. The scanner tests multiple ports simultaneously to detect open services.

require 'socket'
require 'timeout'

class PortScanner
  def initialize(host, timeout: 1)
    @host = host
    @timeout = timeout
  end
  
  def scan(ports)
    threads = ports.map do |port|
      Thread.new { check_port(port) }
    end
    
    results = threads.map(&:value).compact
    results.sort_by { |r| r[:port] }
  end
  
  private
  
  def check_port(port)
    Timeout.timeout(@timeout) do
      socket = TCPSocket.new(@host, port)
      socket.close
      { port: port, state: :open }
    end
  rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
    { port: port, state: :closed }
  rescue Timeout::Error
    { port: port, state: :filtered }
  rescue => e
    { port: port, state: :error, message: e.message }
  end
end

scanner = PortScanner.new('localhost')
results = scanner.scan(20..100)

results.each do |result|
  puts "Port #{result[:port]}: #{result[:state]}"
end

Concurrent scanning improves speed dramatically compared to sequential scanning. Timeout prevents hanging on filtered ports. Different exceptions indicate port states: ECONNREFUSED means the port is closed, timeout suggests filtering by a firewall. This pattern applies to any scenario requiring multiple connection tests.

Error Handling & Edge Cases

Network programming encounters numerous error conditions requiring careful handling. Robust socket applications anticipate failures, implement appropriate recovery strategies, and maintain consistency under adverse conditions.

Connection Failures

Connection establishment fails for various reasons: host unreachable, connection refused, timeout, or network unreachable. Each error suggests different underlying problems and recovery strategies.

require 'socket'

def robust_connect(host, port, max_attempts: 3, timeout: 5)
  attempt = 0
  
  begin
    attempt += 1
    
    Timeout.timeout(timeout) do
      socket = TCPSocket.new(host, port)
      return socket
    end
  rescue Errno::ECONNREFUSED
    # Service not running, retry won't help immediately
    raise "Connection refused by #{host}:#{port}"
  rescue Errno::EHOSTUNREACH, Errno::ENETUNREACH
    # Network problem, retry might succeed
    if attempt < max_attempts
      sleep(2 ** attempt)  # Exponential backoff
      retry
    else
      raise "Host unreachable after #{max_attempts} attempts"
    end
  rescue Timeout::Error
    # Connection hanging, possibly network congestion
    if attempt < max_attempts
      retry
    else
      raise "Connection timeout after #{max_attempts} attempts"
    end
  rescue SocketError => e
    # DNS resolution failure
    raise "Cannot resolve hostname: #{e.message}"
  end
end

ECONNREFUSED indicates the target port has no listening service. Retrying immediately rarely helps; applications should wait or alert administrators. EHOSTUNREACH suggests routing problems that might resolve after delays. Exponential backoff prevents overwhelming recovering networks. SocketError covers DNS failures requiring different handling than network errors.

Partial Reads and Writes

TCP provides a byte stream without message boundaries. Applications cannot assume read operations return complete messages or that write operations send all data immediately.

def read_exact(socket, length)
  data = String.new
  
  while data.bytesize < length
    chunk = socket.read(length - data.bytesize)
    
    if chunk.nil? || chunk.empty?
      raise EOFError, "Connection closed after #{data.bytesize} of #{length} bytes"
    end
    
    data << chunk
  end
  
  data
end

def write_all(socket, data)
  written = 0
  
  while written < data.bytesize
    begin
      n = socket.write_nonblock(data.byteslice(written..-1))
      written += n
    rescue IO::WaitWritable
      # Send buffer full, wait for space
      IO.select(nil, [socket])
      retry
    end
  end
  
  written
end

# Message framing with length prefix
def send_message(socket, message)
  length = [message.bytesize].pack('N')  # 4-byte network byte order
  write_all(socket, length)
  write_all(socket, message)
end

def receive_message(socket)
  length_bytes = read_exact(socket, 4)
  length = length_bytes.unpack1('N')
  read_exact(socket, length)
end

Length-prefixed framing ensures complete message transmission. The read_exact method loops until receiving the specified bytes, detecting premature closure. write_all handles partial writes from full send buffers, using IO.select to wait for buffer space. Applications must implement similar logic for any binary protocol.

Timeout Management

Timeouts prevent indefinite blocking on network operations. Ruby supports timeouts through socket options, IO.select, or the Timeout module.

require 'socket'
require 'timeout'

class TimeoutSocket
  def initialize(host, port, timeout: 10)
    @socket = nil
    @timeout = timeout
    
    # Connection timeout via Timeout module
    Timeout.timeout(@timeout) do
      @socket = TCPSocket.new(host, port)
    end
    
    # Set socket-level timeouts
    timeval = [@timeout, 0].pack('l_2')
    @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_RCVTIMEO, timeval)
    @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_SNDTIMEO, timeval)
  end
  
  def read_with_timeout(length)
    Timeout.timeout(@timeout) do
      @socket.read(length)
    end
  rescue Timeout::Error
    raise "Read timeout after #{@timeout} seconds"
  end
  
  def write_with_timeout(data)
    Timeout.timeout(@timeout) do
      @socket.write(data)
    end
  rescue Timeout::Error
    raise "Write timeout after #{@timeout} seconds"
  end
  
  def close
    @socket.close if @socket
  end
end

Socket-level timeouts (SO_RCVTIMEO, SO_SNDTIMEO) apply to individual I/O operations. The Timeout module provides higher-level operation timeouts. Combining both strategies ensures protection against hanging operations at multiple levels. Timeout durations should account for expected network latency and data volumes.

Buffer Overflow Prevention

Fixed-size buffers prevent unbounded memory growth when receiving large amounts of data. Applications must limit read sizes and implement resource constraints.

class BoundedReceiver
  MAX_MESSAGE_SIZE = 1024 * 1024  # 1 MB limit
  
  def initialize(socket)
    @socket = socket
    @buffer = String.new
  end
  
  def receive_limited
    while @buffer.bytesize < MAX_MESSAGE_SIZE
      chunk = @socket.read_nonblock(4096)
      @buffer << chunk
      
      # Check for message terminator
      if message = extract_message
        return message
      end
    rescue IO::WaitReadable
      IO.select([@socket])
      retry
    end
    
    raise "Message exceeds maximum size of #{MAX_MESSAGE_SIZE} bytes"
  end
  
  private
  
  def extract_message
    # Example: newline-terminated messages
    if index = @buffer.index("\n")
      message = @buffer.slice!(0, index + 1)
      message.chomp
    end
  end
end

The bounded receiver accumulates data up to a maximum size while searching for message boundaries. Applications should validate input sizes before processing to prevent denial of service through memory exhaustion. Rate limiting and connection quotas provide additional protection against resource abuse.

Signal Handling and Cleanup

Servers must handle signals gracefully, closing sockets and cleaning up resources before termination. Improper shutdown can leave sockets in TIME_WAIT state or orphan child processes.

require 'socket'

class GracefulServer
  def initialize(port)
    @server = TCPServer.new(port)
    @running = true
    @threads = []
    
    setup_signal_handlers
  end
  
  def run
    while @running
      begin
        client = @server.accept_nonblock
        @threads << Thread.new(client) { |c| handle_client(c) }
      rescue IO::WaitReadable
        IO.select([@server], nil, nil, 1)
      end
      
      # Clean up finished threads
      @threads.reject!(&:alive?)
    end
    
    shutdown
  end
  
  private
  
  def setup_signal_handlers
    ['INT', 'TERM'].each do |signal|
      Signal.trap(signal) do
        puts "\nReceived #{signal}, shutting down..."
        @running = false
      end
    end
  end
  
  def shutdown
    puts "Closing server socket..."
    @server.close
    
    puts "Waiting for #{@threads.count} client threads..."
    @threads.each { |t| t.join(5) }  # Wait up to 5 seconds per thread
    
    puts "Shutdown complete"
  end
  
  def handle_client(client)
    # Handle client connection
  ensure
    client.close
  end
end

Signal handlers set flags rather than directly closing resources, allowing the main loop to shut down cleanly. The server waits for active connections to complete with a timeout. Non-blocking accept with IO.select allows checking the running flag periodically. This pattern ensures proper resource cleanup and prevents connection interruption.

Security Implications

Socket programming exposes applications to various security threats. Network-facing services must implement defensive measures against attacks, validate all input, and protect sensitive data during transmission.

Input Validation and Injection

Network input arrives from untrusted sources and requires rigorous validation. Applications must sanitize data before using it in commands, queries, or system operations to prevent injection attacks.

require 'socket'

class SecureCommandServer
  ALLOWED_COMMANDS = ['STATUS', 'INFO', 'HELP'].freeze
  MAX_INPUT_LENGTH = 256
  
  def handle_client(client)
    while line = client.gets
      command = validate_input(line)
      
      response = case command
      when 'STATUS'
        get_system_status
      when 'INFO'
        get_system_info
      when 'HELP'
        get_help_text
      else
        "Unknown command"
      end
      
      client.puts response
    end
  rescue SecurityError => e
    client.puts "Error: #{e.message}"
    client.close
  end
  
  private
  
  def validate_input(input)
    # Length check
    if input.bytesize > MAX_INPUT_LENGTH
      raise SecurityError, "Input exceeds maximum length"
    end
    
    # Character whitelist
    unless input =~ /\A[A-Z]+\z/
      raise SecurityError, "Input contains invalid characters"
    end
    
    # Command whitelist
    command = input.strip.upcase
    unless ALLOWED_COMMANDS.include?(command)
      raise SecurityError, "Command not permitted"
    end
    
    command
  end
end

Whitelist validation prevents injection by accepting only known-safe commands and characters. Length limits prevent buffer overflow and denial of service. Never trust client input or use it directly in system commands, file paths, or database queries. Sanitization alone is insufficient; use parameterized interfaces or prepared statements when available.

TLS/SSL Encryption

Sensitive data transmitted over networks requires encryption. Ruby's OpenSSL library wraps sockets with TLS, providing confidentiality and integrity.

require 'socket'
require 'openssl'

class SecureServer
  def initialize(port, cert_file, key_file)
    @tcp_server = TCPServer.new(port)
    
    # Create SSL context
    @ssl_context = OpenSSL::SSL::SSLContext.new
    @ssl_context.cert = OpenSSL::X509::Certificate.new(File.read(cert_file))
    @ssl_context.key = OpenSSL::PKey::RSA.new(File.read(key_file))
    @ssl_context.ssl_version = :TLSv1_2_server
    @ssl_context.ciphers = 'HIGH:!aNULL:!MD5'
    
    @ssl_server = OpenSSL::SSL::SSLServer.new(@tcp_server, @ssl_context)
  end
  
  def run
    loop do
      ssl_client = @ssl_server.accept
      Thread.new(ssl_client) { |client| handle_secure_client(client) }
    end
  end
  
  private
  
  def handle_secure_client(client)
    puts "Secure connection from #{client.peeraddr[3]}"
    puts "Cipher: #{client.cipher.first}"
    
    # Normal socket operations work on SSL sockets
    while line = client.gets
      client.puts "Received: #{line}"
    end
  ensure
    client.close
  end
end

# Client with certificate verification
def secure_connect(host, port, ca_file)
  tcp_socket = TCPSocket.new(host, port)
  
  ssl_context = OpenSSL::SSL::SSLContext.new
  ssl_context.ca_file = ca_file
  ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER
  
  ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, ssl_context)
  ssl_socket.connect
  ssl_socket.post_connection_check(host)
  
  ssl_socket
end

TLS configuration requires certificates and keys. The ssl_version setting enforces secure protocol versions. Cipher configuration prevents weak algorithms. Client verification ensures connection to legitimate servers, preventing man-in-the-middle attacks. post_connection_check validates the certificate hostname matches the requested host.

Denial of Service Prevention

Network services face resource exhaustion attacks through excessive connections, slow clients, or large payloads. Rate limiting, timeouts, and resource quotas mitigate these threats.

require 'socket'

class RateLimitedServer
  MAX_CONNECTIONS = 100
  MAX_REQUEST_RATE = 10  # per second per client
  
  def initialize(port)
    @server = TCPServer.new(port)
    @connections = {}
    @mutex = Mutex.new
  end
  
  def run
    loop do
      client = @server.accept
      client_ip = client.peeraddr[3]
      
      @mutex.synchronize do
        if @connections.count >= MAX_CONNECTIONS
          client.puts "Server full, try again later"
          client.close
          next
        end
        
        @connections[client] = {
          ip: client_ip,
          requests: [],
          connected_at: Time.now
        }
      end
      
      Thread.new(client) { |c| handle_rate_limited_client(c) }
    end
  end
  
  private
  
  def handle_rate_limited_client(client)
    while line = client.gets
      unless check_rate_limit(client)
        client.puts "Rate limit exceeded"
        sleep 1  # Slow down abusive clients
        next
      end
      
      process_request(client, line)
    end
  ensure
    @mutex.synchronize { @connections.delete(client) }
    client.close
  end
  
  def check_rate_limit(client)
    @mutex.synchronize do
      info = @connections[client]
      now = Time.now
      
      # Remove old requests (older than 1 second)
      info[:requests].reject! { |time| now - time > 1 }
      
      if info[:requests].count >= MAX_REQUEST_RATE
        return false
      end
      
      info[:requests] << now
      true
    end
  end
end

Connection limits prevent resource exhaustion. Per-client rate tracking detects and throttles abusive clients. Sliding window rate limiting provides more accurate rate control than fixed time windows. Additional protections include connection timeouts, maximum message sizes, and IP-based blocking after violations.

Authentication and Authorization

Network services require authentication to verify client identity and authorization to control resource access. Token-based authentication separates authentication from authorization.

require 'socket'
require 'securerandom'
require 'digest'

class AuthenticatedServer
  def initialize(port)
    @server = TCPServer.new(port)
    @users = load_users
    @sessions = {}
    @mutex = Mutex.new
  end
  
  def handle_client(client)
    session_id = authenticate(client)
    return unless session_id
    
    client.puts "Authenticated successfully"
    
    while line = client.gets
      command, *args = line.strip.split
      
      unless authorized?(session_id, command)
        client.puts "Unauthorized"
        next
      end
      
      response = execute_command(command, args)
      client.puts response
    end
  ensure
    @mutex.synchronize { @sessions.delete(session_id) }
    client.close
  end
  
  private
  
  def authenticate(client)
    client.puts "Username:"
    username = client.gets.strip
    
    client.puts "Password:"
    password = client.gets.strip
    
    user = @users[username]
    return nil unless user
    
    password_hash = Digest::SHA256.hexdigest(password + user[:salt])
    return nil unless password_hash == user[:password_hash]
    
    session_id = SecureRandom.hex(32)
    @mutex.synchronize do
      @sessions[session_id] = {
        username: username,
        permissions: user[:permissions],
        created_at: Time.now
      }
    end
    
    session_id
  end
  
  def authorized?(session_id, command)
    @mutex.synchronize do
      session = @sessions[session_id]
      return false unless session
      
      # Check if session expired (30 minutes)
      if Time.now - session[:created_at] > 1800
        @sessions.delete(session_id)
        return false
      end
      
      session[:permissions].include?(command)
    end
  end
  
  def load_users
    {
      'admin' => {
        password_hash: Digest::SHA256.hexdigest('password' + 'salt123'),
        salt: 'salt123',
        permissions: ['STATUS', 'SHUTDOWN', 'CONFIG']
      },
      'user' => {
        password_hash: Digest::SHA256.hexdigest('userpass' + 'salt456'),
        salt: 'salt456',
        permissions: ['STATUS']
      }
    }
  end
end

Password hashing with salt prevents rainbow table attacks. Session tokens identify authenticated clients without transmitting passwords repeatedly. Session expiration limits the window for token theft. Permission-based authorization controls command access. Production systems should use established authentication frameworks and store credentials securely.

Tools & Ecosystem

Ruby's networking ecosystem includes standard library components and third-party gems providing higher-level abstractions and additional functionality for common socket programming tasks.

EventMachine

EventMachine implements the reactor pattern with a clean callback-based API. It handles connection management, multiplexing, and timer events, allowing focus on application logic.

require 'eventmachine'

module EchoServer
  def post_init
    @addr = Socket.unpack_sockaddr_in(get_peername)
    puts "Client connected: #{@addr[1]}"
  end
  
  def receive_data(data)
    send_data "Echo: #{data}"
  end
  
  def unbind
    puts "Client disconnected: #{@addr[1]}"
  end
end

EventMachine.run do
  EventMachine.start_server '0.0.0.0', 8080, EchoServer
  puts "Server running on port 8080"
end

EventMachine calls post_init when connections establish, receive_data when data arrives, and unbind when connections close. The library handles all socket operations and event loop management. This pattern suits applications with many concurrent connections and asynchronous operations.

Async and Async-IO

The async gem provides fiber-based concurrency with async/await semantics. Async-IO implements non-blocking I/O operations using fibers instead of callbacks.

require 'async'
require 'async/io/socket'

Async do |task|
  endpoint = Async::IO::Endpoint.tcp('0.0.0.0', 8080)
  
  endpoint.accept do |client|
    task.async do
      while line = client.read_until("\n")
        client.write("Echo: #{line}")
      end
    end
  end
end

Async uses cooperative scheduling where fibers yield during I/O operations. This provides concurrency without threads or callbacks. The task.async method spawns concurrent operations that run within the same thread, yielding control during blocking calls.

Celluloid

Celluloid combines actor model concurrency with Ruby's object-oriented programming. Each actor runs in its own thread, processing messages asynchronously.

require 'celluloid/io'

class ChatServer
  include Celluloid::IO
  
  def initialize(port)
    @server = TCPServer.new(port)
    @clients = []
    async.run
  end
  
  def run
    loop { async.handle_connection(@server.accept) }
  end
  
  def handle_connection(socket)
    @clients << socket
    
    while data = socket.readpartial(4096)
      broadcast(data, socket)
    end
  rescue EOFError
    @clients.delete(socket)
    socket.close
  end
  
  def broadcast(message, sender)
    @clients.each do |client|
      client.write(message) unless client == sender
    end
  end
end

server = ChatServer.new(8080)
sleep

Celluloid actors isolate state and communicate through method calls that become asynchronous messages. The async method prefix makes calls non-blocking. This model prevents race conditions by serializing operations per actor while enabling concurrency between actors.

HTTParty and RestClient

Higher-level HTTP clients abstract socket details for REST API consumption. These gems handle connection pooling, redirects, and response parsing.

require 'httparty'

class APIClient
  include HTTParty
  base_uri 'api.example.com'
  
  def initialize(api_key)
    @options = { headers: { 'Authorization' => "Bearer #{api_key}" } }
  end
  
  def get_resource(id)
    self.class.get("/resources/#{id}", @options)
  end
  
  def create_resource(data)
    self.class.post('/resources', @options.merge(body: data.to_json))
  end
end

HTTParty handles HTTP semantics, connection management, and response parsing. Applications focus on API interaction rather than low-level socket programming. These libraries remain useful references for understanding HTTP over sockets.

Socket.IO-Ruby

Socket.IO provides WebSocket communication with fallback mechanisms. The Ruby implementation enables real-time bidirectional communication between web clients and Ruby servers.

WebSocket upgrades HTTP connections to full-duplex socket connections, enabling server-push notifications and reduced latency. Socket.IO adds transport abstraction, automatic reconnection, and room/namespace organization.

Puma and Unicorn

Production Ruby web servers like Puma and Unicorn handle socket management for web applications. Understanding their architectures informs decisions about concurrency models and deployment.

Puma uses threads for concurrency, sharing memory between requests. It creates a thread pool and distributes connections across threads. This model balances memory efficiency and concurrency, particularly with Ruby implementations without a GIL.

Unicorn uses process forking for isolation. A master process manages worker processes, each handling one connection at a time. Slow clients tie up workers, but process isolation prevents one request from affecting others. Unix socket communication between master and workers coordinates connection distribution.

Reference

Socket Classes and Methods

Class Purpose Key Methods
Socket Low-level socket operations socket, bind, listen, accept, connect, send, recv, close
TCPServer TCP server creation new, accept, accept_nonblock, listen, close
TCPSocket TCP client connections new, recv, send, read, write, close, peeraddr
UDPSocket UDP datagram communication new, bind, send, recvfrom, connect
UNIXServer Unix domain server new, accept, path
UNIXSocket Unix domain client new, recv_io, send_io
BasicSocket Base class for sockets getsockopt, setsockopt, shutdown, getpeername

Common Socket Options

Level Option Purpose Values
SOL_SOCKET SO_REUSEADDR Allow address reuse 0 or 1
SOL_SOCKET SO_KEEPALIVE Enable TCP keepalive 0 or 1
SOL_SOCKET SO_RCVTIMEO Receive timeout timeval struct
SOL_SOCKET SO_SNDTIMEO Send timeout timeval struct
SOL_SOCKET SO_RCVBUF Receive buffer size bytes
SOL_SOCKET SO_SNDBUF Send buffer size bytes
SOL_TCP TCP_NODELAY Disable Nagle algorithm 0 or 1
SOL_TCP TCP_KEEPIDLE Keepalive idle time seconds
SOL_TCP TCP_KEEPINTVL Keepalive interval seconds
SOL_TCP TCP_KEEPCNT Keepalive probe count integer

Address Families

Family Description Address Format Use Case
AF_INET IPv4 addressing 32-bit address Internet communication
AF_INET6 IPv6 addressing 128-bit address Internet communication with larger address space
AF_UNIX Unix domain sockets Filesystem path Local inter-process communication
AF_UNSPEC Unspecified family Varies Address family agnostic operations

Socket Types

Type Protocol Connection Reliability Order Boundaries
SOCK_STREAM TCP Connection-oriented Reliable Ordered No message boundaries
SOCK_DGRAM UDP Connectionless Unreliable Unordered Message boundaries preserved
SOCK_RAW Various Varies Varies Varies Direct protocol access
SOCK_SEQPACKET SCTP Connection-oriented Reliable Ordered Message boundaries preserved

Error Codes

Error Meaning Common Cause Recovery
ECONNREFUSED Connection refused No listener on port Verify service running, check port
ETIMEDOUT Operation timed out Network delay, host down Retry with backoff, check network
EHOSTUNREACH Host unreachable Routing problem Check network connectivity, routing
ENETUNREACH Network unreachable Network interface down Verify network configuration
EADDRINUSE Address in use Port already bound Change port or use SO_REUSEADDR
EPIPE Broken pipe Write to closed socket Handle disconnect, check connection state
ECONNRESET Connection reset Remote forced close Reconnect or handle as disconnect
EWOULDBLOCK Would block Non-blocking operation pending Use select or retry later

Connection States

State Description Transitions
CLOSED No connection -> LISTEN (server) or -> SYN_SENT (client)
LISTEN Waiting for connections -> SYN_RCVD on incoming SYN
SYN_SENT Connection request sent -> ESTABLISHED on SYN-ACK
SYN_RCVD Connection request received -> ESTABLISHED on ACK
ESTABLISHED Connection active -> FIN_WAIT_1 on close, -> CLOSE_WAIT on FIN
FIN_WAIT_1 Close initiated -> FIN_WAIT_2 on ACK
FIN_WAIT_2 Close acknowledged -> TIME_WAIT on FIN
CLOSE_WAIT Remote closed, local pending -> LAST_ACK on close
LAST_ACK Final acknowledgment pending -> CLOSED on ACK
TIME_WAIT Connection fully closed -> CLOSED after 2MSL timeout

IO.select Parameters

Parameter Type Purpose Notes
read_array Array of IO Sockets to monitor for reading Returns when data available
write_array Array of IO Sockets to monitor for writing Returns when send buffer has space
error_array Array of IO Sockets to monitor for errors Returns on exceptional conditions
timeout Numeric or nil Maximum wait time nil blocks indefinitely, 0 polls

Socket Lifecycle Methods

Phase Server Methods Client Methods
Creation Socket.new, TCPServer.new Socket.new, TCPSocket.new
Binding bind (automatic with TCPSocket.new)
Listening listen N/A
Connecting accept connect
Data Transfer recv, send, read, write recv, send, read, write
Shutdown shutdown, close shutdown, close

Performance Tuning Options

Technique Implementation Benefit Tradeoff
TCP_NODELAY setsockopt(SOL_TCP, TCP_NODELAY, 1) Lower latency Higher bandwidth usage
Buffer sizing SO_RCVBUF, SO_SNDBUF Higher throughput More memory usage
Non-blocking I/O fcntl(F_SETFL, O_NONBLOCK) Better concurrency Complex error handling
Connection pooling Reuse connections Reduced overhead State management complexity
Multiplexing IO.select, epoll Many connections Event-driven architecture