Skip to content

Ruby Socket Programming

Ruby provides access to network services at multiple levels. At a low level, the socket standard library allows you to use the underlying operating system’s socket support. This enables implementation of clients and servers for both connection-oriented (like TCP) and connectionless (like UDP) protocols.

Ruby also has higher-level libraries (e.g., net/http, net/ftp) for specific application-level network protocols. This tutorial focuses on basic socket programming using TCP.

Sockets are endpoints of a bidirectional communication channel. They can facilitate communication within a single process, between processes on the same machine, or between processes on different machines across a network.

The socket library provides classes for various transport types like Unix domain sockets, TCP, UDP, and a generic interface for others.

Socket Terminology:

TermDescription
DomainThe protocol family, e.g., Socket::PF_INET (for IPv4), Socket::PF_INET6 (for IPv6), Socket::PF_UNIX (for local Unix domain sockets).
TypeThe communication type, e.g., Socket::SOCK_STREAM (for connection-oriented protocols like TCP), Socket::SOCK_DGRAM (for connectionless protocols like UDP).
ProtocolTypically 0 or Socket::IPPROTO_TCP / Socket::IPPROTO_UDP. It can specify a variant within a domain and type.
HostnameA network interface identifier: a hostname string (e.g., ‘localhost’, ‘www.example.com’), an IP address string (e.g., ‘127.0.0.1’, ‘::1’), or special strings like ''.
PortA numerical identifier for a service on a host. Servers listen on ports; clients connect to them. It can be an integer (e.g., 80, 443) or a service name string (e.g., ‘http’, ‘https’) which gets resolved.

This client connects to a specified host and port, sends a message, reads the server’s response, and then closes the connection. TCPSocket.new(hostname, port) (or TCPSocket.open) establishes a TCP connection.

#!/usr/bin/env ruby
# frozen_string_literal: true
require 'socket'
hostname = 'localhost'
port = 2000
begin
socket = TCPSocket.new(hostname, port)
puts "Connected to #{hostname}:#{port}"
# Send a message to the server
message = "Hello from client!"
socket.puts message
puts "Sent: #{message}"
# Read the server's response
while line = socket.gets # Read lines from the socket
puts "Received: #{line.chomp}" # chomp removes the newline
end
rescue Errno::ECONNREFUSED => e
puts "Connection refused. Is the server running at #{hostname}:#{port}?"
rescue StandardError => e
puts "An error occurred: #{e.message}"
ensure
socket.close if socket # Close the socket when done
puts "Connection closed."
end

This server listens on a port for incoming connections. When a client connects, it accepts the connection, sends a greeting, and closes the client socket. TCPServer.new(port) creates a server socket.

#!/usr/bin/env ruby
# frozen_string_literal: true
require 'socket'
port = 2000
server = TCPServer.new(port) # Socket to listen on port 2000
puts "Server listening on port #{port}..."
loop do # Servers typically run forever
begin
client_socket = server.accept # Wait for a client to connect
client_addr = client_socket.peeraddr # Get client address info
puts "Accepted connection from #{client_addr[2]}:#{client_addr[1]}" # IP:Port
# Read client's message
client_message = client_socket.gets.chomp
puts "Client said: #{client_message}"
# Send a response to the client
server_response = "Hello from server! You said: #{client_message}"
client_socket.puts server_response
client_socket.puts "Server closing the connection. Bye!"
rescue StandardError => e
puts "Error handling client: #{e.message}"
ensure
client_socket.close if client_socket # Disconnect from the client
puts "Closed connection to client."
end
end

To test, run the server script in one terminal, then run the client script in another.

Real-world servers need to handle multiple clients concurrently. A basic way to achieve this in Ruby is by spawning a new thread for each client connection.

#!/usr/bin/env ruby
# frozen_string_literal: true
require 'socket'
port = 2000
server = TCPServer.new(port)
puts "Multi-threaded server listening on port #{port}..."
loop do
Thread.start(server.accept) do |client_socket| # Pass accepted socket to the new thread
begin
client_addr = client_socket.peeraddr
thread_id = Thread.current.object_id # For logging
puts "[Thread #{thread_id}] Accepted connection from #{client_addr[2]}:#{client_addr[1]}"
client_message = client_socket.gets.chomp
puts "[Thread #{thread_id}] Client said: #{client_message}"
server_response = "Hello from threaded server! Processed by thread #{thread_id}."
client_socket.puts server_response
client_socket.puts "Server closing your connection. Bye!"
rescue StandardError => e
puts "[Thread #{thread_id}] Error handling client: #{e.message}"
ensure
client_socket.close if client_socket
puts "[Thread #{thread_id}] Closed connection to client."
end
end
end

This approach creates a new thread for each server.accept. While simple, for very high concurrency, more advanced patterns (like thread pools, event-driven I/O with libraries like async) might be considered to manage resources more efficiently. However, for many applications, Ruby’s threading is effective.

We can use sockets to implement higher-level protocols like HTTP. This example fetches a web page.

#!/usr/bin/env ruby
# frozen_string_literal: true
require 'socket'
host = 'www.example.com' # Target web server
port = 80 # Default HTTP port
path = "/" # Path to the resource (e.g., root page)
# Construct the HTTP GET request
request = "GET #{path} HTTP/1.1\r\n"
request << "Host: #{host}\r\n"
request << "Connection: close\r\n"
request << "User-Agent: SimpleRubyBrowser/1.0\r\n"
request << "\r\n" # Blank line signifies end of headers
begin
socket = TCPSocket.new(host, port)
socket.print(request) # Send the request
response = socket.read # Read the complete response
# Split response into headers and body
headers, body = response.split("\r\n\r\n", 2)
puts "--- HEADERS ---"
puts headers
puts "\n--- BODY ---"
puts body if body
rescue StandardError => e
puts "Error fetching page: #{e.message}"
ensure
socket.close if socket
end

For HTTP, Ruby’s standard library Net::HTTP provides a more convenient and robust interface than raw sockets.

#!/usr/bin/env ruby
# frozen_string_literal: true
require 'net/http'
require 'uri'
uri_string = 'http://www.example.com/'
begin
uri = URI(uri_string)
response = Net::HTTP.get_response(uri) # Sends a GET request
puts "--- STATUS ---"
puts "#{response.code} #{response.message}"
puts "\n--- HEADERS ---"
response.each_header { |key, value| puts "#{key}: #{value}" }
if response.is_a?(Net::HTTPSuccess) # Check for 2xx status codes
puts "\n--- BODY ---"
puts response.body
else
puts "\nRequest failed or redirected."
puts "Body (if any):\n#{response.body}"
end
rescue StandardError => e
puts "Error with Net::HTTP: #{e.message}"
end

Ruby provides similar standard libraries (Net::FTP, Net::SMTP, Net::IMAP, Net::POP3) for other common internet protocols.

This was a brief introduction to socket programming in Ruby. For more depth, explore:

  • The official Ruby documentation for the Socket, TCPSocket, TCPServer, UDPSocket classes.
  • Non-blocking I/O and event-driven programming models (e.g., using the async gem or IO.select).
  • Secure Sockets Layer (SSL/TLS) using OpenSSL::SSL::SSLSocket for encrypted communication.