Ruby Logo

Sockets & TCP/UDP

Learn socket programming with TCPServer, UDPSocket, and build simple servers for network communication.

Home Ruby Sockets & TCP/UDP

Sockets & TCP/UDP in Ruby

Introduction to Sockets

Sockets provide a low-level interface for network communication. Ruby's Socket library offers both TCP (reliable, connection-oriented) and UDP (fast, connectionless) protocols for building network applications.

Socket Basics

require 'socket'

# Create a basic socket
socket = Socket.new(Socket::AF_INET, Socket::SOCK_STREAM)
puts socket.class  # => Socket

TCP Sockets with TCPServer

TCPServer creates a server that listens for incoming TCP connections. It's perfect for building reliable, connection-oriented services.

Basic TCP Server

require 'socket'

# Create a TCP server on port 3000
server = TCPServer.new('localhost', 3000)
puts "Server listening on port 3000..."

loop do
  # Accept incoming connections
  client = server.accept

  # Read client request
  request = client.read_nonblock(1024) rescue nil
  puts "Received: #{request}"

  # Send response
  client.write("HTTP/1.1 200 OK\r\n\r\nHello from Ruby server!")

  # Close connection
  client.close
end

Multi-threaded TCP Server

require 'socket'

server = TCPServer.new('localhost', 3000)
puts "Multi-threaded server listening on port 3000..."

loop do
  Thread.start(server.accept) do |client|
    begin
      # Handle each client in separate thread
      request = client.read_nonblock(1024) rescue ""

      # Process request
      response = process_request(request)

      # Send response
      client.write(response)
    rescue => e
      puts "Error handling client: #{e.message}"
    ensure
      client.close
    end
  end
end

def process_request(request)
  case request
  when /GET \/hello/
    "HTTP/1.1 200 OK\r\n\r\nHello World!"
  when /GET \/time/
    "HTTP/1.1 200 OK\r\n\r\nCurrent time: #{Time.now}"
  else
    "HTTP/1.1 404 Not Found\r\n\r\nPage not found"
  end
end

TCP Client

require 'socket'

# Connect to TCP server
client = TCPSocket.new('localhost', 3000)

# Send request
client.write("GET /hello HTTP/1.1\r\n\r\n")

# Read response
response = client.read
puts "Server response: #{response}"

client.close

UDP Sockets with UDPSocket

UDP provides fast, connectionless communication. It's ideal for applications where speed is more important than guaranteed delivery.

UDP Server

require 'socket'

# Create UDP server
server = UDPSocket.new
server.bind('localhost', 3001)
puts "UDP server listening on port 3001..."

loop do
  # Receive message and client info
  message, client_info = server.recvfrom(1024)

  puts "Received from #{client_info[2]}:#{client_info[1]}: #{message}"

  # Send response back to client
  response = "Echo: #{message}"
  server.send(response, 0, client_info[2], client_info[1])
end

UDP Client

require 'socket'

# Create UDP client
client = UDPSocket.new

# Send message to server
message = "Hello UDP Server!"
client.send(message, 0, 'localhost', 3001)

# Receive response
response, server_info = client.recvfrom(1024)
puts "Server response: #{response}"

client.close

UDP Broadcast Example

require 'socket'

# UDP broadcaster
broadcaster = UDPSocket.new
broadcaster.setsockopt(Socket::SOL_SOCKET, Socket::SO_BROADCAST, true)

# Broadcast message
message = "Broadcast message: #{Time.now}"
broadcaster.send(message, 0, '255.255.255.255', 3002)
puts "Broadcasted: #{message}"

broadcaster.close

Advanced Socket Programming

Non-blocking I/O

require 'socket'

server = TCPServer.new('localhost', 3000)

# Set non-blocking mode
server.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)

clients = []

loop do
  begin
    # Try to accept new connection (non-blocking)
    client = server.accept_nonblock
    clients << client
    puts "New client connected"
  rescue IO::WaitReadable
    # No new connections, continue
  end

  # Handle existing clients
  clients.each do |client|
    begin
      data = client.read_nonblock(1024)
      client.write("Echo: #{data}")
    rescue IO::WaitReadable
      # No data available
    rescue EOFError
      # Client disconnected
      clients.delete(client)
      client.close
    end
  end

  sleep 0.1  # Prevent busy waiting
end

Socket with Select

require 'socket'

server = TCPServer.new('localhost', 3000)
clients = [server]

loop do
  # Wait for readable sockets
  ready = IO.select(clients, nil, nil, 1.0)

  next unless ready

  ready[0].each do |socket|
    if socket == server
      # New client connection
      client = server.accept
      clients << client
      puts "Client connected"
    else
      # Existing client has data
      begin
        data = socket.read_nonblock(1024)
        socket.write("Received: #{data}")
      rescue EOFError
        # Client disconnected
        clients.delete(socket)
        socket.close
        puts "Client disconnected"
      end
    end
  end
end

Practical Examples

Simple Chat Server

require 'socket'

class ChatServer
  def initialize(port = 3000)
    @server = TCPServer.new('localhost', port)
    @clients = []
    puts "Chat server started on port #{port}"
  end

  def start
    loop do
      Thread.start(@server.accept) do |client|
        @clients << client
        client.write("Welcome to the chat!\n")
        broadcast("A new user joined the chat\n", client)

        handle_client(client)
      end
    end
  end

  private

  def handle_client(client)
    loop do
      message = client.gets
      break unless message

      broadcast("User: #{message}", client)
    end
  rescue
    # Client disconnected
  ensure
    @clients.delete(client)
    client.close
    broadcast("A user left the chat\n", client)
  end

  def broadcast(message, sender = nil)
    @clients.each do |client|
      next if client == sender
      client.write(message) rescue nil
    end
  end
end

# Start the chat server
ChatServer.new.start

File Transfer Server

require 'socket'
require 'fileutils'

class FileTransferServer
  def initialize(port = 3000)
    @server = TCPServer.new('localhost', port)
    puts "File transfer server started on port #{port}"
  end

  def start
    loop do
      Thread.start(@server.accept) do |client|
        handle_file_transfer(client)
      end
    end
  end

  private

  def handle_file_transfer(client)
    # Read filename and size
    filename = client.gets.chomp
    filesize = client.gets.chomp.to_i

    puts "Receiving file: #{filename} (#{filesize} bytes)"

    # Receive file data
    File.open("uploads/#{filename}", 'wb') do |file|
      remaining = filesize

      while remaining > 0
        chunk = client.read([remaining, 1024].min)
        file.write(chunk)
        remaining -= chunk.length
      end
    end

    client.write("File received successfully\n")
    puts "File #{filename} received"

  rescue => e
    client.write("Error: #{e.message}\n")
  ensure
    client.close
  end
end

# Ensure uploads directory exists
FileUtils.mkdir_p('uploads')

# Start the server
FileTransferServer.new.start

Port Scanner

require 'socket'
require 'timeout'

class PortScanner
  def initialize(host)
    @host = host
  end

  def scan_port(port, timeout = 1)
    Timeout::timeout(timeout) do
      socket = TCPSocket.new(@host, port)
      socket.close
      true
    end
  rescue
    false
  end

  def scan_range(start_port, end_port)
    open_ports = []

    (start_port..end_port).each do |port|
      if scan_port(port)
        open_ports << port
        puts "Port #{port} is open"
      end
    end

    open_ports
  end

  def threaded_scan(start_port, end_port, thread_count = 50)
    ports = (start_port..end_port).to_a
    open_ports = []
    mutex = Mutex.new

    threads = []

    thread_count.times do
      threads << Thread.new do
        while port = mutex.synchronize { ports.shift }
          if scan_port(port, 0.5)
            mutex.synchronize { open_ports << port }
            puts "Port #{port} is open"
          end
        end
      end
    end

    threads.each(&:join)
    open_ports.sort
  end
end

# Example usage
scanner = PortScanner.new('localhost')
open_ports = scanner.threaded_scan(1, 1000)
puts "Open ports: #{open_ports}"

Error Handling and Best Practices

Robust Server with Error Handling

require 'socket'
require 'logger'

class RobustServer
  def initialize(port = 3000)
    @port = port
    @logger = Logger.new(STDOUT)
    @running = false
  end

  def start
    @server = TCPServer.new('localhost', @port)
    @running = true
    @logger.info "Server started on port #{@port}"

    trap('INT') { shutdown }
    trap('TERM') { shutdown }

    while @running
      begin
        client = @server.accept
        Thread.new { handle_client(client) }
      rescue => e
        @logger.error "Error accepting client: #{e.message}"
      end
    end
  end

  private

  def handle_client(client)
    @logger.info "Client connected: #{client.peeraddr}"

    begin
      while line = client.gets
        response = process_request(line.chomp)
        client.puts response
      end
    rescue => e
      @logger.error "Error handling client: #{e.message}"
    ensure
      client.close
      @logger.info "Client disconnected"
    end
  end

  def process_request(request)
    case request
    when 'time'
      Time.now.to_s
    when 'uptime'
      "Server running for #{Time.now - @start_time} seconds"
    else
      "Unknown command: #{request}"
    end
  end

  def shutdown
    @logger.info "Shutting down server..."
    @running = false
    @server.close if @server
  end
end

# Start the server
RobustServer.new.start

Performance Considerations

  • TCP vs UDP: Use TCP for reliability, UDP for speed
  • Threading: Handle multiple clients with threads or async I/O
  • Buffering: Use appropriate buffer sizes for your use case
  • Connection Pooling: Reuse connections when possible
  • Timeouts: Always set timeouts to prevent hanging
  • Resource Cleanup: Always close sockets and handle disconnections

Performance Monitoring

require 'socket'
require 'benchmark'

# Measure connection time
connection_time = Benchmark.measure do
  socket = TCPSocket.new('google.com', 80)
  socket.close
end

puts "Connection time: #{connection_time.real} seconds"

# Measure throughput
data = 'x' * 1024  # 1KB of data
iterations = 1000

throughput_time = Benchmark.measure do
  server = TCPServer.new('localhost', 3000)

  Thread.new do
    client = server.accept
    iterations.times { client.write(data) }
    client.close
  end

  socket = TCPSocket.new('localhost', 3000)
  received = 0
  while chunk = socket.read(1024)
    received += chunk.length
    break if received >= data.length * iterations
  end
  socket.close
  server.close
end

throughput = (data.length * iterations) / throughput_time.real / 1024 / 1024
puts "Throughput: #{throughput.round(2)} MB/s"

Socket Programming Best Practices

  • Always handle exceptions and network errors gracefully
  • Use timeouts to prevent indefinite blocking
  • Implement proper connection cleanup and resource management
  • Consider using higher-level libraries for complex protocols
  • Test network code with various failure scenarios
  • Use logging to debug network issues
  • Implement graceful shutdown mechanisms
  • Consider security implications (input validation, DoS protection)

Quick Navigation

Related Topics

Video Tutorial

Watch and learn sockets & tcp/udp

Pro Tip: After reading through the content above, watch this video to reinforce your understanding and see the concepts in action!