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 |