Socket Programming in Ruby

· 4 min read · Updated March 18, 2026 · intermediate
ruby networking sockets tcp udp

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

ClassPurpose
SocketLow-level, supports any protocol family
TCPServerTCP server sockets
TCPSocketTCP client sockets
UDPSocketUDP datagram sockets
UNIXServerUnix domain server sockets
UNIXSocketUnix 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_nonblock
  • connect_nonblock
  • read_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 state
  • SO_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 / TCPSocket for simple TCP client-server applications
  • Use UDPSocket for connectionless datagram communication
  • Use Socket for 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_REUSEADDR before bind or listen