Skip to content

Ruby Socket 编程

Socket编程是网络编程的基础,它允许程序通过网络与其他程序进行通信。Ruby提供了强大的Socket库,使得网络编程变得简单而直观。无论是创建服务器还是客户端,Ruby都能轻松应对。本章将详细介绍Ruby中的Socket编程,包括TCP、UDP通信以及HTTP客户端的实现。

🎯 Socket基础

什么是Socket

Socket是网络编程中的一个抽象概念,它是网络通信的端点。通过Socket,应用程序可以在网络上发送和接收数据。Socket提供了一种标准的通信机制,使得不同主机上的程序能够相互交流。

Socket类型

  1. 流式Socket (SOCK_STREAM): 使用TCP协议,提供可靠的、面向连接的通信
  2. 数据报Socket (SOCK_DGRAM): 使用UDP协议,提供无连接的通信
  3. 原始Socket (SOCK_RAW): 提供对底层协议的直接访问

Socket寻址

Socket通过IP地址和端口号来标识网络上的一个端点:

  • IP地址: 标识网络上的主机(如 192.168.1.100)
  • 端口号: 标识主机上的特定服务(如 80、443、3000)

🔌 TCP Socket编程

TCP客户端

TCP(传输控制协议)是一种面向连接的协议,提供可靠的数据传输。

ruby
require 'socket'

# 创建TCP客户端
def tcp_client(server_host, server_port, message)
  # 创建TCP Socket
  socket = Socket.new(:INET, :STREAM)
  
  # 连接到服务器
  sockaddr = Socket.sockaddr_in(server_port, server_host)
  socket.connect(sockaddr)
  
  # 发送消息
  socket.write(message)
  puts "已发送: #{message}"
  
  # 接收响应
  response = socket.read(1024)
  puts "收到响应: #{response}"
  
  # 关闭连接
  socket.close
end

# 使用示例(需要先运行服务器)
# tcp_client('localhost', 3000, "你好,服务器!")

# 更简单的写法
def simple_tcp_client(server_host, server_port, message)
  socket = TCPSocket.new(server_host, server_port)
  socket.puts message
  response = socket.gets
  puts "服务器响应: #{response}"
  socket.close
end

# simple_tcp_client('localhost', 3000, "你好,服务器!")

TCP服务器

ruby
require 'socket'

# 创建TCP服务器
def tcp_server(port)
  # 创建服务器Socket
  server = TCPServer.new(port)
  puts "TCP服务器启动,监听端口 #{port}"
  
  loop do
    # 等待客户端连接
    client = server.accept
    puts "客户端已连接: #{client.peeraddr[2]}:#{client.peeraddr[1]}"
    
    # 创建新线程处理客户端
    Thread.new(client) do |client_socket|
      begin
        # 读取客户端消息
        while message = client_socket.gets
          puts "收到消息: #{message.chomp}"
          
          # 回复客户端
          response = "服务器收到: #{message}"
          client_socket.puts response
          
          # 如果收到退出命令则断开连接
          break if message.chomp == 'quit'
        end
      rescue => e
        puts "客户端处理错误: #{e.message}"
      ensure
        client_socket.close
        puts "客户端连接已关闭"
      end
    end
  end
end

# 启动服务器(在单独的终端中运行)
# tcp_server(3000)

# 更完整的TCP服务器示例
class TCPServerExample
  def initialize(port)
    @port = port
    @server = nil
    @clients = []
  end
  
  def start
    @server = TCPServer.new(@port)
    puts "服务器启动,监听端口 #{@port}"
    
    # 处理中断信号
    trap('INT') { shutdown }
    
    loop do
      client = @server.accept
      @clients << client
      puts "新客户端连接: #{client.peeraddr[2]}:#{client.peeraddr[1]}"
      
      # 为每个客户端创建处理线程
      handle_client(client)
    end
  end
  
  private
  
  def handle_client(client)
    Thread.new do
      begin
        client.puts "欢迎连接到服务器!输入 'quit' 退出。"
        
        while message = client.gets
          message = message.chomp
          puts "收到消息: #{message} (来自 #{client.peeraddr[2]})"
          
          case message
          when 'quit'
            client.puts "再见!"
            break
          when 'time'
            client.puts "当前时间: #{Time.now}"
          when 'clients'
            client.puts "当前连接数: #{@clients.length}"
          else
            client.puts "服务器回复: #{message}"
          end
        end
      rescue => e
        puts "客户端错误: #{e.message}"
      ensure
        @clients.delete(client)
        client.close
        puts "客户端断开连接"
      end
    end
  end
  
  def shutdown
    puts "正在关闭服务器..."
    @server.close if @server
    @clients.each(&:close)
    exit
  end
end

# 使用示例
# server = TCPServerExample.new(3001)
# server.start

📨 UDP Socket编程

UDP客户端

UDP(用户数据报协议)是一种无连接的协议,不保证数据传输的可靠性,但速度更快。

ruby
require 'socket'

# UDP客户端
def udp_client(server_host, server_port, message)
  # 创建UDP Socket
  socket = UDPSocket.new
  
  # 发送消息到服务器
  socket.send(message, 0, server_host, server_port)
  puts "已发送: #{message}"
  
  # 接收响应
  response, sender = socket.recvfrom(1024)
  puts "收到响应: #{response} (来自 #{sender[2]}:#{sender[1]})"
  
  # 关闭Socket
  socket.close
end

# 使用示例(需要先运行UDP服务器)
# udp_client('localhost', 3002, "UDP消息测试")

UDP服务器

ruby
require 'socket'

# UDP服务器
def udp_server(port)
  # 创建UDP Socket
  socket = UDPSocket.new
  socket.bind('localhost', port)
  puts "UDP服务器启动,监听端口 #{port}"
  
  loop do
    # 接收消息
    message, sender = socket.recvfrom(1024)
    puts "收到消息: #{message} (来自 #{sender[2]}:#{sender[1]})"
    
    # 发送响应
    response = "服务器收到: #{message}"
    socket.send(response, 0, sender[2], sender[1])
  end
end

# 启动UDP服务器(在单独的终端中运行)
# udp_server(3002)

# 更完整的UDP服务器示例
class UDPServerExample
  def initialize(port)
    @port = port
    @socket = nil
  end
  
  def start
    @socket = UDPSocket.new
    @socket.bind('0.0.0.0', @port)
    puts "UDP服务器启动,监听端口 #{@port}"
    
    # 处理中断信号
    trap('INT') { shutdown }
    
    loop do
      message, sender = @socket.recvfrom(1024)
      handle_message(message, sender)
    end
  end
  
  private
  
  def handle_message(message, sender)
    client_ip, client_port = sender[2], sender[1]
    puts "收到消息: #{message.chomp} (来自 #{client_ip}:#{client_port})"
    
    response = case message.chomp
               when 'time'
                 Time.now.to_s
               when 'ping'
                 'pong'
               else
                 "服务器收到: #{message}"
               end
    
    @socket.send(response, 0, client_ip, client_port)
  end
  
  def shutdown
    puts "正在关闭UDP服务器..."
    @socket.close if @socket
    exit
  end
end

# 使用示例
# server = UDPServerExample.new(3003)
# server.start

🌐 HTTP客户端

基本HTTP请求

ruby
require 'socket'

# 简单的HTTP GET请求
def simple_http_get(host, port = 80, path = '/')
  # 创建TCP连接
  socket = TCPSocket.new(host, port)
  
  # 发送HTTP请求
  request = "GET #{path} HTTP/1.1\r\n"
  request += "Host: #{host}\r\n"
  request += "Connection: close\r\n"
  request += "\r\n"
  
  socket.write(request)
  
  # 读取响应
  response = socket.read
  socket.close
  
  response
end

# 使用示例
# response = simple_http_get('httpbin.org', 80, '/get')
# puts response

# 解析HTTP响应
def parse_http_response(response)
  # 分离头部和主体
  header, body = response.split("\r\n\r\n", 2)
  
  # 解析状态行
  status_line = header.lines.first
  http_version, status_code, status_message = status_line.split(' ', 3)
  
  # 解析头部
  headers = {}
  header.lines[1..-1].each do |line|
    key, value = line.split(':', 2)
    headers[key.strip] = value.strip if key && value
  end
  
  {
    status_code: status_code.to_i,
    status_message: status_message,
    headers: headers,
    body: body
  }
end

# response = simple_http_get('httpbin.org', 80, '/get')
# parsed = parse_http_response(response)
# puts "状态码: #{parsed[:status_code]}"
# puts "内容类型: #{parsed[:headers]['Content-Type']}"

HTTP POST请求

ruby
require 'socket'

# HTTP POST请求
def simple_http_post(host, port = 80, path, data)
  # 创建TCP连接
  socket = TCPSocket.new(host, port)
  
  # 准备POST数据
  post_data = data.is_a?(Hash) ? data.map { |k, v| "#{k}=#{v}" }.join('&') : data.to_s
  
  # 发送HTTP请求
  request = "POST #{path} HTTP/1.1\r\n"
  request += "Host: #{host}\r\n"
  request += "Content-Type: application/x-www-form-urlencoded\r\n"
  request += "Content-Length: #{post_data.length}\r\n"
  request += "Connection: close\r\n"
  request += "\r\n"
  request += post_data
  
  socket.write(request)
  
  # 读取响应
  response = socket.read
  socket.close
  
  response
end

# 使用示例
# post_data = { name: '张三', email: 'zhangsan@example.com' }
# response = simple_http_post('httpbin.org', 80, '/post', post_data)
# puts response

🧪 Socket实用工具

端口扫描器

ruby
require 'socket'
require 'timeout'

class PortScanner
  def initialize(host, port_range)
    @host = host
    @port_range = port_range
    @open_ports = []
  end
  
  def scan(timeout = 1)
    puts "开始扫描 #{@host} 的端口 #{@port_range.first}-#{@port_range.last}"
    
    @port_range.each do |port|
      if port_open?(@host, port, timeout)
        @open_ports << port
        puts "端口 #{port} 开放"
      end
    end
    
    puts "扫描完成,发现 #{@open_ports.length} 个开放端口"
    @open_ports
  end
  
  private
  
  def port_open?(host, port, timeout)
    begin
      Timeout.timeout(timeout) do
        socket = TCPSocket.new(host, port)
        socket.close
        true
      end
    rescue
      false
    end
  end
end

# 使用示例
# scanner = PortScanner.new('localhost', 3000..3010)
# open_ports = scanner.scan(0.5)

网络聊天室

ruby
require 'socket'
require 'thread'

# 聊天室服务器
class ChatServer
  def initialize(port)
    @port = port
    @server = nil
    @clients = {}
    @mutex = Mutex.new
  end
  
  def start
    @server = TCPServer.new(@port)
    puts "聊天室服务器启动,监听端口 #{@port}"
    
    trap('INT') { shutdown }
    
    loop do
      client = @server.accept
      handle_new_client(client)
    end
  end
  
  private
  
  def handle_new_client(client)
    Thread.new do
      begin
        # 获取客户端昵称
        client.puts "欢迎来到聊天室!请输入您的昵称:"
        nickname = client.gets.chomp
        nickname = "用户#{rand(1000)}" if nickname.empty?
        
        # 添加客户端到列表
        @mutex.synchronize do
          @clients[client] = nickname
        end
        
        broadcast("#{nickname} 加入了聊天室", client)
        client.puts "您已加入聊天室,输入 '/quit' 退出"
        
        # 处理消息
        while message = client.gets
          message = message.chomp
          break if message == '/quit'
          
          if message.start_with?('/nick ')
            new_nickname = message[6..-1]
            old_nickname = @clients[client]
            @clients[client] = new_nickname
            broadcast("#{old_nickname} 改名为 #{new_nickname}")
          else
            broadcast("#{@clients[client]}: #{message}", client)
          end
        end
      rescue => e
        puts "客户端错误: #{e.message}"
      ensure
        nickname = @clients[client]
        @clients.delete(client)
        client.close
        broadcast("#{nickname} 离开了聊天室") if nickname
        puts "客户端断开连接"
      end
    end
  end
  
  def broadcast(message, exclude_client = nil)
    @mutex.synchronize do
      @clients.each do |client, nickname|
        next if client == exclude_client
        
        begin
          client.puts message
        rescue
          # 客户端可能已断开连接
        end
      end
    end
  end
  
  def shutdown
    puts "正在关闭聊天室服务器..."
    @server.close if @server
    @clients.keys.each(&:close)
    exit
  end
end

# 聊天室客户端
class ChatClient
  def initialize(host, port)
    @host = host
    @port = port
    @socket = nil
  end
  
  def start
    @socket = TCPSocket.new(@host, @port)
    
    # 启动接收消息的线程
    Thread.new { receive_messages }
    
    # 主线程处理用户输入
    while line = gets
      @socket.puts line.chomp
    end
  end
  
  private
  
  def receive_messages
    while message = @socket.gets
      puts message
    end
  rescue => e
    puts "连接错误: #{e.message}"
  ensure
    @socket.close if @socket
  end
end

# 使用示例
# 启动服务器: ChatServer.new(3004).start
# 启动客户端: ChatClient.new('localhost', 3004).start

🌍 网络工具

HTTP服务器

ruby
require 'socket'

class SimpleHTTPServer
  def initialize(port = 8080, document_root = './public')
    @port = port
    @document_root = document_root
    @server = nil
  end
  
  def start
    @server = TCPServer.new(@port)
    puts "HTTP服务器启动,监听端口 #{@port}"
    puts "文档根目录: #{@document_root}"
    
    trap('INT') { shutdown }
    
    loop do
      client = @server.accept
      handle_request(client)
    end
  end
  
  private
  
  def handle_request(client)
    Thread.new do
      begin
        # 读取请求
        request_line = client.gets
        return unless request_line
        
        method, path, version = request_line.split
        path = '/' if path == ''
        path = '/index.html' if path == '/'
        
        # 读取请求头
        headers = {}
        while line = client.gets
          break if line == "\r\n"
          key, value = line.split(':', 2)
          headers[key.strip] = value.strip if key && value
        end
        
        # 处理请求
        file_path = File.join(@document_root, path[1..-1])
        serve_file(client, file_path)
      rescue => e
        puts "请求处理错误: #{e.message}"
        send_error(client, 500, "Internal Server Error")
      ensure
        client.close
      end
    end
  end
  
  def serve_file(client, file_path)
    if File.exist?(file_path) && !File.directory?(file_path)
      content_type = get_content_type(file_path)
      content = File.read(file_path)
      
      response = "HTTP/1.1 200 OK\r\n"
      response += "Content-Type: #{content_type}\r\n"
      response += "Content-Length: #{content.length}\r\n"
      response += "Connection: close\r\n"
      response += "\r\n"
      response += content
      
      client.write(response)
    else
      send_error(client, 404, "Not Found")
    end
  end
  
  def send_error(client, status_code, message)
    content = "<html><body><h1>#{status_code} #{message}</h1></body></html>"
    
    response = "HTTP/1.1 #{status_code} #{message}\r\n"
    response += "Content-Type: text/html\r\n"
    response += "Content-Length: #{content.length}\r\n"
    response += "Connection: close\r\n"
    response += "\r\n"
    response += content
    
    client.write(response)
  end
  
  def get_content_type(file_path)
    ext = File.extname(file_path).downcase
    case ext
    when '.html' then 'text/html'
    when '.css' then 'text/css'
    when '.js' then 'application/javascript'
    when '.png' then 'image/png'
    when '.jpg', '.jpeg' then 'image/jpeg'
    when '.gif' then 'image/gif'
    else 'text/plain'
    end
  end
  
  def shutdown
    puts "正在关闭HTTP服务器..."
    @server.close if @server
    exit
  end
end

# 使用示例
# server = SimpleHTTPServer.new(8080, './public')
# server.start

网络时间协议(NTP)客户端

ruby
require 'socket'

class NTPClient
  NTP_SERVER = 'pool.ntp.org'
  NTP_PORT = 123
  
  def get_time
    # 创建UDP Socket
    socket = UDPSocket.new
    
    # 构造NTP请求包
    ntp_packet = construct_ntp_packet
    
    # 发送请求
    socket.send(ntp_packet, 0, NTP_SERVER, NTP_PORT)
    
    # 接收响应
    response, = socket.recvfrom(48)
    socket.close
    
    # 解析响应
    parse_ntp_response(response)
  end
  
  private
  
  def construct_ntp_packet
    # 构造48字节的NTP请求包
    packet = [0] * 48
    packet[0] = 0x1B  # LI(0) | VN(3) | Mode(3) - 客户端模式
    packet.pack('C48')
  end
  
  def parse_ntp_response(packet)
    # 解析NTP响应包
    data = packet.unpack('C1 N1 N1 N1 N1 N1 N1 N1 N1 N1 N1 N1')
    
    # 提取时间戳(第41-48字节)
    timestamp = data[10] * (2**32) + data[11]
    
    # 转换为Unix时间戳
    # NTP时间戳从1900年1月1日开始,Unix时间戳从1970年1月1日开始
    # 差值为2208988800秒
    unix_timestamp = timestamp - 2208988800
    
    Time.at(unix_timestamp)
  end
end

# 使用示例
# client = NTPClient.new
# network_time = client.get_time
# puts "网络时间: #{network_time}"
# puts "本地时间: #{Time.now}"

📊 Socket性能优化

连接池

ruby
require 'socket'
require 'thread'

class SocketPool
  def initialize(host, port, size = 5)
    @host = host
    @port = port
    @size = size
    @pool = Queue.new
    @mutex = Mutex.new
    
    # 初始化连接池
    @size.times do
      @pool.push(create_socket)
    end
  end
  
  def with_socket
    socket = @pool.pop
    
    begin
      yield socket
    ensure
      @pool.push(socket)
    end
  end
  
  private
  
  def create_socket
    TCPSocket.new(@host, @port)
  end
end

# 使用示例
# pool = SocketPool.new('httpbin.org', 80)
# 
# 10.times do
#   pool.with_socket do |socket|
#     socket.puts "GET /get HTTP/1.1"
#     socket.puts "Host: httpbin.org"
#     socket.puts "Connection: close"
#     socket.puts
#     
#     response = socket.read
#     puts "响应长度: #{response.length}"
#   end
# end

非阻塞Socket

ruby
require 'socket'

class NonBlockingClient
  def initialize(host, port)
    @host = host
    @port = port
    @socket = nil
  end
  
  def connect
    @socket = Socket.new(:INET, :STREAM)
    
    # 设置为非阻塞模式
    @socket.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)
    
    sockaddr = Socket.sockaddr_in(@port, @host)
    
    begin
      @socket.connect(sockaddr)
    rescue Errno::EINPROGRESS
      # 连接正在进行中
      wait_for_write
    end
  end
  
  def send_data(data)
    @socket.write(data)
  end
  
  def receive_data(max_bytes = 1024)
    @socket.read(max_bytes)
  end
  
  private
  
  def wait_for_write
    # 等待Socket变为可写状态
    readable, writable, _ = IO.select(nil, [@socket], nil, 5)
    
    if writable && writable.include?(@socket)
      # 检查连接是否成功
      error = @socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_ERROR).int
      raise SocketError, "连接失败" unless error == 0
    else
      raise SocketError, "连接超时"
    end
  end
end

# 使用示例
# client = NonBlockingClient.new('httpbin.org', 80)
# client.connect
# client.send_data("GET /get HTTP/1.1\r\nHost: httpbin.org\r\n\r\n")
# response = client.receive_data
# puts response

🎯 Socket最佳实践

1. 错误处理

ruby
require 'socket'

class RobustSocketClient
  def initialize(host, port, timeout = 5)
    @host = host
    @port = port
    @timeout = timeout
  end
  
  def send_message(message)
    socket = nil
    
    begin
      # 创建Socket并设置超时
      socket = TCPSocket.new(@host, @port)
      socket.write(message)
      
      # 设置读取超时
      socket.read_timeout = @timeout
      response = socket.read(1024)
      
      response
    rescue SocketError => e
      puts "Socket错误: #{e.message}"
      nil
    rescue Errno::ECONNREFUSED => e
      puts "连接被拒绝: #{e.message}"
      nil
    rescue Timeout::Error => e
      puts "连接超时: #{e.message}"
      nil
    ensure
      socket&.close
    end
  end
end

# 使用示例
# client = RobustSocketClient.new('localhost', 9999)
# response = client.send_message("测试消息")
# puts response || "发送失败"

2. 资源管理

ruby
class ManagedSocket
  def initialize
    @sockets = []
  end
  
  def create_tcp_socket(host, port)
    socket = TCPSocket.new(host, port)
    @sockets << socket
    socket
  end
  
  def create_udp_socket
    socket = UDPSocket.new
    @sockets << socket
    socket
  end
  
  def close_all
    @sockets.each do |socket|
      begin
        socket.close unless socket.closed?
      rescue
        # 忽略关闭错误
      end
    end
    @sockets.clear
  end
  
  def socket_count
    @sockets.length
  end
end

# 使用示例
# manager = ManagedSocket.new
# 
# socket1 = manager.create_tcp_socket('httpbin.org', 80)
# socket2 = manager.create_udp_socket
# 
# puts "当前管理的Socket数量: #{manager.socket_count}"
# 
# # 应用结束时关闭所有Socket
# at_exit { manager.close_all }

3. 安全Socket (SSL/TLS)

ruby
require 'socket'
require 'openssl'

class SecureSocketClient
  def initialize(host, port)
    @host = host
    @port = port
  end
  
  def connect
    # 创建TCP连接
    tcp_socket = TCPSocket.new(@host, @port)
    
    # 创建SSL上下文
    context = OpenSSL::SSL::SSLContext.new
    context.verify_mode = OpenSSL::SSL::VERIFY_PEER
    
    # 创建SSL Socket
    ssl_socket = OpenSSL::SSL::SSLSocket.new(tcp_socket, context)
    ssl_socket.hostname = @host
    ssl_socket.connect
    
    ssl_socket
  end
  
  def send_https_request(path = '/')
    socket = nil
    
    begin
      socket = connect
      
      # 发送HTTPS请求
      request = "GET #{path} HTTP/1.1\r\n"
      request += "Host: #{@host}\r\n"
      request += "Connection: close\r\n"
      request += "\r\n"
      
      socket.write(request)
      response = socket.read
      
      response
    rescue => e
      puts "HTTPS请求失败: #{e.message}"
      nil
    ensure
      socket&.close
    end
  end
end

# 使用示例
# client = SecureSocketClient.new('httpbin.org', 443)
# response = client.send_https_request('/get')
# puts response

📚 下一步学习

掌握了Ruby Socket编程后,建议继续学习:

继续您的Ruby学习之旅吧!

本站内容仅供学习和研究使用。