DEV Community

Davide Santangelo
Davide Santangelo

Posted on

Exploring Ruby's Networking Capabilities: From Basics to Advanced Implementations

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

  1. Introduction to Networking in Ruby
  2. Understanding Ruby's Net Module
  3. Low-Level Networking: Socket Programming
  4. High-Level Networking: Using Net::HTTP
  5. Advanced Networking Libraries and Gems
  6. Best Practices and Security
  7. Real-World Applications
  8. 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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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:

  1. Start with Net::HTTP for basic HTTP operations—it's included in the standard library and handles most common use cases
  2. Use sockets when you need low-level control or custom protocols
  3. Leverage gems like Faraday, HTTParty, or Typhoeus for advanced features and better developer experience
  4. Always implement proper error handling, timeouts, and security measures
  5. 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 or eventmachine
  • Investigate GraphQL clients like graphql-client
  • Consider message queues and pub/sub patterns with gems like bunny (RabbitMQ) or redis

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)