Ruby, known for its elegant syntax and developer-friendly ecosystem, offers robust tools for networking tasks. Whether you're building web clients, servers, or handling low-level socket communications, Ruby provides built-in modules and libraries that make networking straightforward and powerful. This comprehensive guide explores Ruby's networking capabilities, from basic socket operations to HTTP requests, with practical examples to get you started.
Table of Contents
- Introduction to Networking in Ruby
- Understanding Ruby's Net Module
- Low-Level Networking: Socket Programming
- High-Level Networking: Using Net::HTTP
- Advanced Networking Libraries and Gems
- Best Practices and Security
- Real-World Applications
- Conclusion
Introduction to Networking in Ruby
Networking in Ruby revolves around sending and receiving data over networks using protocols like TCP, UDP, and HTTP. Ruby's standard library includes the Net
module, which provides high-level abstractions for common networking tasks, particularly HTTP interactions. For more specialized needs, Ruby also supports lower-level socket programming.
Why Choose Ruby for Networking?
Ruby offers several advantages for networking applications:
- Rich ecosystem: Extensive collection of gems for specialized networking tasks
- Built-in tools: No external dependencies required for basic HTTP operations
- Elegant syntax: Clean, readable code that's easy to maintain
- Cross-platform: Works consistently across different operating systems
- Active community: Well-documented libraries and community support
Understanding Ruby's Net Module
The Net
module serves as a namespace for networking-related classes in Ruby's standard library. It's designed primarily for client-server interactions, with HTTP being the primary focus.
Core Components
Net::HTTP
The flagship class for handling HTTP requests and responses. It provides methods for all standard HTTP operations (GET, POST, PUT, DELETE, etc.) and is RFC 2616 compliant.
require 'net/http' require 'uri' # Basic usage uri = URI('https://httpbin.org/get') response = Net::HTTP.get_response(uri) puts response.code # => "200" puts response.body # => JSON response
Net::HTTPHeader
Manages HTTP headers, allowing you to set and retrieve header information:
require 'net/http' http = Net::HTTP.new('httpbin.org', 443) http.use_ssl = true request = Net::HTTP::Get.new('/headers') request['User-Agent'] = 'Ruby HTTP Client' request['Accept'] = 'application/json' response = http.request(request) puts response.body
Error Handling
The Net module provides comprehensive error handling for network operations:
require 'net/http' require 'uri' begin uri = URI('https://httpbin.org/delay/10') http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true http.read_timeout = 5 # 5 seconds timeout response = http.get(uri.path) puts response.body rescue Net::ReadTimeout => e puts "Request timed out: #{e.message}" rescue Net::HTTPError => e puts "HTTP error: #{e.message}" rescue StandardError => e puts "Unexpected error: #{e.message}" end
Low-Level Networking: Socket Programming
For scenarios requiring fine-grained control, Ruby's Socket
class provides direct access to network sockets, supporting both TCP and UDP protocols.
TCP Server Example
Here's a robust TCP server that handles multiple clients:
require 'socket' class EchoServer def initialize(host = 'localhost', port = 8080) @host = host @port = port @server = nil end def start @server = TCPServer.new(@host, @port) puts "Server listening on #{@host}:#{@port}" loop do Thread.start(@server.accept) do |client| handle_client(client) end end rescue Interrupt puts "\nShutting down server..." @server&.close end private def handle_client(client) client_info = client.peeraddr puts "Client connected: #{client_info[2]}:#{client_info[1]}" loop do data = client.gets break if data.nil? || data.strip.downcase == 'quit' client.puts "Echo: #{data}" end client.close puts "Client disconnected: #{client_info[2]}:#{client_info[1]}" rescue StandardError => e puts "Error handling client: #{e.message}" client.close end end # Usage server = EchoServer.new server.start
TCP Client Example
A corresponding client to interact with the server:
require 'socket' class EchoClient def initialize(host = 'localhost', port = 8080) @host = host @port = port end def connect @socket = TCPSocket.new(@host, @port) puts "Connected to #{@host}:#{@port}" loop do print "Enter message (or 'quit' to exit): " message = gets.chomp @socket.puts message break if message.downcase == 'quit' response = @socket.gets puts "Server response: #{response}" end @socket.close puts "Connection closed" rescue StandardError => e puts "Error: #{e.message}" @socket&.close end end # Usage client = EchoClient.new client.connect
UDP Example
For connectionless communication, here's a UDP example:
require 'socket' # UDP Server class UDPServer def initialize(host = 'localhost', port = 8080) @socket = UDPSocket.new @socket.bind(host, port) puts "UDP Server listening on #{host}:#{port}" end def start loop do data, addr = @socket.recvfrom(1024) puts "Received from #{addr[2]}:#{addr[1]}: #{data}" @socket.send("Echo: #{data}", 0, addr[2], addr[1]) end rescue Interrupt puts "\nShutting down server..." @socket.close end end # UDP Client class UDPClient def initialize(host = 'localhost', port = 8080) @socket = UDPSocket.new @host = host @port = port end def send_message(message) @socket.send(message, 0, @host, @port) response, addr = @socket.recvfrom(1024) puts "Server response: #{response}" end def close @socket.close end end
High-Level Networking: Using Net::HTTP
Net::HTTP provides a comprehensive interface for HTTP operations. Here are practical examples for common use cases:
GET Requests with Parameters
require 'net/http' require 'uri' def fetch_with_params(base_url, params = {}) uri = URI(base_url) uri.query = URI.encode_www_form(params) unless params.empty? response = Net::HTTP.get_response(uri) case response when Net::HTTPSuccess response.body when Net::HTTPRedirection location = response['location'] puts "Redirected to: #{location}" fetch_with_params(location) else raise "HTTP Error: #{response.code} #{response.message}" end end # Usage data = fetch_with_params('https://httpbin.org/get', { name: 'Ruby', version: '3.0' }) puts data
POST Requests with JSON
require 'net/http' require 'json' require 'uri' def post_json(url, data) uri = URI(url) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = uri.scheme == 'https' request = Net::HTTP::Post.new(uri.path) request['Content-Type'] = 'application/json' request['Accept'] = 'application/json' request.body = data.to_json response = http.request(request) if response.is_a?(Net::HTTPSuccess) JSON.parse(response.body) else raise "HTTP Error: #{response.code} #{response.message}" end end # Usage payload = { name: "Ruby Developer", email: "ruby@example.com", skills: ["Ruby", "Rails", "Networking"] } result = post_json('https://httpbin.org/post', payload) puts result
File Upload Example
require 'net/http' require 'uri' def upload_file(url, file_path, field_name = 'file') uri = URI(url) File.open(file_path, 'rb') do |file| boundary = "----WebKitFormBoundary#{Time.now.to_i}" post_body = [] post_body << "--#{boundary}\r\n" post_body << "Content-Disposition: form-data; name=\"#{field_name}\"; filename=\"#{File.basename(file_path)}\"\r\n" post_body << "Content-Type: application/octet-stream\r\n\r\n" post_body << file.read post_body << "\r\n--#{boundary}--\r\n" http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = uri.scheme == 'https' request = Net::HTTP::Post.new(uri.path) request['Content-Type'] = "multipart/form-data; boundary=#{boundary}" request.body = post_body.join response = http.request(request) response.body end end
HTTP Client with Connection Pooling
require 'net/http' require 'uri' class HTTPClient def initialize(base_url, options = {}) @uri = URI(base_url) @options = { open_timeout: 10, read_timeout: 30, max_retries: 3 }.merge(options) @http = Net::HTTP.new(@uri.host, @uri.port) @http.use_ssl = @uri.scheme == 'https' @http.open_timeout = @options[:open_timeout] @http.read_timeout = @options[:read_timeout] @http.start end def get(path, params = {}) uri = @uri.dup uri.path = path uri.query = URI.encode_www_form(params) unless params.empty? request = Net::HTTP::Get.new(uri.request_uri) execute_request(request) end def post(path, data, content_type = 'application/json') request = Net::HTTP::Post.new(path) request['Content-Type'] = content_type request.body = data.is_a?(String) ? data : data.to_json execute_request(request) end def close @http.finish if @http.started? end private def execute_request(request) retries = 0 begin response = @http.request(request) case response when Net::HTTPSuccess response.body when Net::HTTPRedirection raise "Redirection not supported in this client" else raise "HTTP Error: #{response.code} #{response.message}" end rescue Net::TimeoutError, Errno::ECONNRESET => e retries += 1 if retries <= @options[:max_retries] sleep(2 ** retries) # Exponential backoff retry else raise "Max retries exceeded: #{e.message}" end end end end # Usage client = HTTPClient.new('https://httpbin.org') result = client.get('/get', { key: 'value' }) puts result client.close
Advanced Networking Libraries and Gems
While Ruby's standard library covers basic networking needs, the ecosystem offers powerful gems for specialized tasks:
Popular HTTP Clients
Faraday
A flexible HTTP client library that supports middleware:
require 'faraday' require 'json' conn = Faraday.new(url: 'https://httpbin.org') do |faraday| faraday.request :json faraday.response :json faraday.response :logger faraday.adapter Faraday.default_adapter end response = conn.get('/get', { param: 'value' }) puts response.body
HTTParty
A simplified HTTP client with a clean DSL:
require 'httparty' class APIClient include HTTParty base_uri 'https://api.example.com' def self.get_user(id) get("/users/#{id}") end def self.create_user(user_data) post('/users', body: user_data.to_json, headers: { 'Content-Type' => 'application/json' }) end end
SSH and Secure File Transfer
Net::SSH
For remote command execution:
require 'net/ssh' Net::SSH.start('hostname', 'username', password: 'password') do |ssh| # Execute a command result = ssh.exec!('ls -la') puts result # Open a channel for more complex operations ssh.open_channel do |channel| channel.exec('tail -f /var/log/syslog') do |ch, success| raise "Command failed" unless success channel.on_data do |ch, data| puts data end end end end
Net::SCP
For secure file transfers:
require 'net/scp' Net::SCP.start('hostname', 'username', password: 'password') do |scp| # Upload a file scp.upload!('/local/file.txt', '/remote/file.txt') # Download a file scp.download!('/remote/file.txt', '/local/downloaded_file.txt') end
Concurrent HTTP Requests
Typhoeus
For high-performance concurrent requests:
require 'typhoeus' urls = [ 'https://httpbin.org/get', 'https://httpbin.org/uuid', 'https://httpbin.org/ip' ] hydra = Typhoeus::Hydra.new(max_concurrency: 3) requests = urls.map do |url| request = Typhoeus::Request.new(url) hydra.queue(request) request end hydra.run requests.each do |request| puts "Response for #{request.url}: #{request.response.code}" end
Best Practices and Security
Error Handling and Timeouts
Always implement proper error handling and timeouts:
require 'net/http' require 'timeout' def robust_http_request(url, options = {}) uri = URI(url) timeout_duration = options[:timeout] || 10 Timeout.timeout(timeout_duration) do http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = uri.scheme == 'https' http.read_timeout = timeout_duration http.open_timeout = timeout_duration response = http.get(uri.path) case response when Net::HTTPSuccess response.body when Net::HTTPRedirection # Handle redirects (with loop prevention) redirect_url = response['location'] raise "Too many redirects" if options[:redirect_count].to_i > 5 robust_http_request(redirect_url, options.merge(redirect_count: options[:redirect_count].to_i + 1)) else raise "HTTP Error: #{response.code} #{response.message}" end end rescue Timeout::Error raise "Request timed out after #{timeout_duration} seconds" rescue StandardError => e raise "Network error: #{e.message}" end
Input Validation and Sanitization
require 'uri' def validate_url(url) uri = URI.parse(url) # Check scheme unless %w[http https].include?(uri.scheme) raise "Invalid scheme: #{uri.scheme}" end # Check for private IP ranges (basic check) if uri.host =~ /^(10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.)/ raise "Private IP addresses not allowed" end # Check for localhost if uri.host =~ /^(localhost|127\.0\.0\.1|::1)$/ raise "Localhost not allowed" end uri rescue URI::InvalidURIError => e raise "Invalid URL: #{e.message}" end
SSL/TLS Configuration
require 'net/http' require 'openssl' def secure_http_request(url) uri = URI(url) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = true # Configure SSL options http.ssl_version = :TLSv1_2 http.verify_mode = OpenSSL::SSL::VERIFY_PEER http.ca_file = '/path/to/ca-bundle.crt' # System CA bundle # Optional: Certificate pinning http.verify_callback = proc do |preverify_ok, ssl_context| if preverify_ok cert = ssl_context.current_cert # Verify certificate fingerprint expected_fingerprint = 'AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD' actual_fingerprint = Digest::SHA1.hexdigest(cert.to_der).upcase.scan(/../).join(':') actual_fingerprint == expected_fingerprint else false end end response = http.get(uri.path) response.body end
Rate Limiting and Throttling
class RateLimitedClient def initialize(requests_per_second = 10) @requests_per_second = requests_per_second @last_request_time = Time.now @request_count = 0 end def make_request(url) enforce_rate_limit # Make the actual request uri = URI(url) response = Net::HTTP.get_response(uri) @request_count += 1 response.body end private def enforce_rate_limit now = Time.now time_since_last_request = now - @last_request_time if time_since_last_request < (1.0 / @requests_per_second) sleep_time = (1.0 / @requests_per_second) - time_since_last_request sleep(sleep_time) end @last_request_time = Time.now end end
Real-World Applications
Web Scraper with Retry Logic
require 'net/http' require 'nokogiri' require 'uri' class WebScraper def initialize(options = {}) @max_retries = options[:max_retries] || 3 @retry_delay = options[:retry_delay] || 1 @user_agent = options[:user_agent] || 'Ruby WebScraper 1.0' end def scrape(url) retries = 0 begin uri = URI(url) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = uri.scheme == 'https' request = Net::HTTP::Get.new(uri.path) request['User-Agent'] = @user_agent response = http.request(request) case response when Net::HTTPSuccess parse_content(response.body) when Net::HTTPRedirection new_url = response['location'] scrape(new_url) else raise "HTTP Error: #{response.code}" end rescue StandardError => e retries += 1 if retries <= @max_retries puts "Retry #{retries}/#{@max_retries} for #{url}: #{e.message}" sleep(@retry_delay * retries) retry else raise "Failed to scrape #{url} after #{@max_retries} retries: #{e.message}" end end end private def parse_content(html) doc = Nokogiri::HTML(html) { title: doc.css('title').text.strip, links: doc.css('a').map { |link| link['href'] }.compact, headings: doc.css('h1, h2, h3').map(&:text).map(&:strip) } end end # Usage scraper = WebScraper.new(max_retries: 5) result = scraper.scrape('https://example.com') puts result
API Client with Caching
require 'net/http' require 'json' require 'digest' class CachedAPIClient def initialize(base_url, cache_ttl = 300) @base_url = base_url @cache_ttl = cache_ttl @cache = {} end def get(endpoint, params = {}) cache_key = generate_cache_key(endpoint, params) if cached_response = get_from_cache(cache_key) puts "Cache hit for #{endpoint}" return cached_response end response = make_request(endpoint, params) store_in_cache(cache_key, response) response end private def generate_cache_key(endpoint, params) content = "#{endpoint}#{params.to_json}" Digest::MD5.hexdigest(content) end def get_from_cache(key) cached_item = @cache[key] return nil unless cached_item if Time.now - cached_item[:timestamp] < @cache_ttl cached_item[:data] else @cache.delete(key) nil end end def store_in_cache(key, data) @cache[key] = { data: data, timestamp: Time.now } end def make_request(endpoint, params) uri = URI("#{@base_url}#{endpoint}") uri.query = URI.encode_www_form(params) unless params.empty? response = Net::HTTP.get_response(uri) if response.is_a?(Net::HTTPSuccess) JSON.parse(response.body) else raise "API Error: #{response.code} #{response.message}" end end end # Usage client = CachedAPIClient.new('https://api.example.com', 600) data = client.get('/users', { limit: 10 }) puts data
Simple Load Balancer
require 'net/http' require 'uri' class LoadBalancer def initialize(servers) @servers = servers @current_index = 0 @failed_servers = Set.new end def make_request(path, method = :get, body = nil) attempts = 0 max_attempts = @servers.length while attempts < max_attempts server = next_server begin response = send_request(server, path, method, body) mark_server_healthy(server) return response rescue StandardError => e puts "Request to #{server} failed: #{e.message}" mark_server_failed(server) attempts += 1 end end raise "All servers failed" end private def next_server available_servers = @servers - @failed_servers.to_a raise "No healthy servers available" if available_servers.empty? server = available_servers[@current_index % available_servers.length] @current_index += 1 server end def send_request(server, path, method, body) uri = URI("#{server}#{path}") http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = uri.scheme == 'https' http.read_timeout = 5 request = case method when :get Net::HTTP::Get.new(uri.path) when :post req = Net::HTTP::Post.new(uri.path) req.body = body if body req else raise "Unsupported method: #{method}" end response = http.request(request) if response.is_a?(Net::HTTPSuccess) response.body else raise "HTTP Error: #{response.code}" end end def mark_server_failed(server) @failed_servers.add(server) end def mark_server_healthy(server) @failed_servers.delete(server) end end # Usage lb = LoadBalancer.new([ 'https://server1.example.com', 'https://server2.example.com', 'https://server3.example.com' ]) response = lb.make_request('/api/health') puts response
Conclusion
Ruby's networking capabilities provide a robust foundation for building connected applications. From the high-level abstractions in the Net module to the fine-grained control offered by socket programming, Ruby offers flexibility and power for various networking scenarios.
Key Takeaways:
- Start with Net::HTTP for basic HTTP operations—it's included in the standard library and handles most common use cases
- Use sockets when you need low-level control or custom protocols
- Leverage gems like Faraday, HTTParty, or Typhoeus for advanced features and better developer experience
- Always implement proper error handling, timeouts, and security measures
- Consider performance implications and use appropriate concurrency patterns for high-throughput applications
Next Steps:
- Explore WebSocket support with gems like
websocket-client-simple
- Learn about async networking with
async-http
oreventmachine
- Investigate GraphQL clients like
graphql-client
- Consider message queues and pub/sub patterns with gems like
bunny
(RabbitMQ) orredis
Ruby's networking ecosystem continues to evolve, with new gems and improvements being added regularly. The examples in this guide provide a solid foundation for building robust, scalable networked applications in Ruby.
Top comments (0)