Socket Programming in Ruby
Socket programming in Ruby lets you build network applications that communicate over TCP, UDP, or Unix sockets. Ruby’s socket library provides both low-level and high-level interfaces, from the core Socket class to convenient wrappers like TCPServer and TCPSocket.
When to Use Sockets
Use Ruby’s socket classes when you need:
- Custom network protocols
- Low-level network control
- UDP datagram communication
- Unix domain sockets for local IPC
For HTTP clients, prefer Net::HTTP. For HTTP servers, consider WEBrick or frameworks like Rails. Sockets give you direct control over the wire protocol.
Core Classes Overview
| Class | Purpose |
|---|---|
Socket | Low-level, supports any protocol family |
TCPServer | TCP server sockets |
TCPSocket | TCP client sockets |
UDPSocket | UDP datagram sockets |
UNIXServer | Unix domain server sockets |
UNIXSocket | Unix domain client sockets |
TCP Server
Basic TCP Server
require 'socket'
server = TCPServer.new(8080)
loop do
client = server.accept
request = client.gets
client.puts "HTTP/1.1 200 OK\r\n\r\nHello, World!"
client.close
end
TCPServer.new(port) creates a server socket bound to the specified port on all interfaces. For a specific interface, pass the hostname:
server = TCPServer.new('127.0.0.1', 8080)
Accept Returns a TCPSocket
The accept method returns a TCPSocket representing the connected client. This socket is bidirectional—you can read and write to it:
client = server.accept
client.puts "Welcome!"
response = client.gets
client.close
Handling Multiple Clients
To handle multiple clients concurrently, use threads:
require 'socket'
server = TCPServer.new(8080)
loop do
client = server.accept
Thread.start(client) do |conn|
loop do
line = conn.gets
break if line.nil?
conn.puts "Echo: #{line}"
end
conn.close
end
end
TCP Client
Connecting to a Server
require 'socket'
client = TCPSocket.new('localhost', 8080)
client.puts "Hello, server!"
response = client.gets
client.close
TCPSocket.new(hostname, port) connects to the specified server. The connection is blocking—execution pauses until the connection is established or fails.
Non-Blocking Connect
For non-blocking behavior, use TCPSocket.open with a block and handle IO::WaitWritable:
require 'socket'
begin
socket = TCPSocket.new('slow-server', 80)
rescue IO::WaitWritable
IO.select(nil, [socket], nil, 5) # Wait up to 5 seconds
begin
socket.connect_nonblock(socket.remote_address)
rescue IO::WaitReadable
# Connection failed
end
end
UDP Sockets
UDP is connectionless—each datagram is independent. Create a UDPSocket and use send and recv (or recvfrom):
require 'socket'
socket = UDPSocket.new
socket.bind('127.0.0.1', 8080)
message, sender = socket.recvfrom(1024)
puts "Received: #{message} from #{sender.ip_address}"
socket.send("ACK", 0, sender.ip_address, sender.ip_port)
For sending without binding:
socket = UDPSocket.new
socket.send("Hello", 0, '127.0.0.1', 8081)
Low-Level Socket Class
The Socket class provides full control over socket options, addresses, and protocols:
require 'socket'
# Create an IPv4 TCP socket
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM)
# Bind to a specific address and port
address = Socket.pack_sockaddr_in(8080, '0.0.0.0')
socket.bind(address)
# Start listening
socket.listen(5)
# Accept a connection
client, client_addr = socket.accept
Socket Address Packing
Socket.pack_sockaddr_in(port, hostname) packs a port and hostname into a sockaddr structure required by low-level socket calls:
addr = Socket.pack_sockaddr_in(80, 'example.com')
socket.connect(addr)
Common Gotchas
1. Forgetting to Close Sockets
Sockets are file descriptors. Unclosed sockets leak file descriptors and may keep connections alive unexpectedly. Always close sockets, preferably with blocks:
TCPServer.new(8080) do |server|
client = server.accept
begin
# Work with client
ensure
client.close # Always close
end
end
2. Blocking I/O
All socket operations are blocking by default. In a threaded server, this is fine. For event-driven or async servers, use non-blocking methods:
accept_nonblockconnect_nonblockread_nonblock/write_nonblock
3. Socket Options
Default socket options may not suit your needs. Set options before binding or connecting:
socket = TCPSocket.new
socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, true)
Common options:
SO_REUSEADDR— allows binding to a port in TIME_WAIT stateSO_REUSEPORT— allows multiple sockets on the same port (Linux 3.9+)TCP_NODELAY— disables Nagle’s algorithm for low-latency
4. Address Family Confusion
Different address families use different address formats. AF_INET is IPv4, AF_INET6 is IPv6. Mixing them causes errors:
# IPv4 only
socket = Socket.new(AF_INET, SOCK_STREAM)
# IPv6 only
socket = Socket.new(AF_INET6, SOCK_STREAM)
5. Reading Until EOF
gets reads until a newline. If the server doesn’t send a newline, gets blocks. For binary protocols, use read with a length or readpartial:
# Read exactly 100 bytes
data = client.read(100)
# Read up to 1024 bytes (non-blocking)
data = client.read_nonblock(1024)
Ruby Version Requirements
All socket classes work with Ruby 2.7+. Ruby 3.0+ includes frozen string literals and keyword argument changes. The socket API has been stable across Ruby versions.
Summary
- Use
TCPServer/TCPSocketfor simple TCP client-server applications - Use
UDPSocketfor connectionless datagram communication - Use
Socketfor low-level control over protocols and socket options - Always close sockets to prevent resource leaks
- Use threads or non-blocking I/O for concurrent servers
- Set socket options like
SO_REUSEADDRbeforebindorlisten