← 返回目录

第五章:网络通信

HTTP 请求、API 调用与 Web 框架

1 Net::HTTP 标准库

Ruby 标准库内置了 net/http 模块,无需安装任何 gem 即可发送 HTTP 请求。虽然 API 略显繁琐,但在不引入外部依赖的脚本中非常实用。

GET 请求

require 'net/http'
require 'uri'
require 'json'

uri = URI('https://jsonplaceholder.typicode.com/posts/1')
response = Net::HTTP.get_response(uri)

if response.is_a?(Net::HTTPSuccess)
  data = JSON.parse(response.body)
  puts data['title']
else
  puts "请求失败: #{response.code} #{response.message}"
end

body = Net::HTTP.get(uri)
puts body

带参数和 Header 的 GET

uri = URI('https://api.example.com/users')
uri.query = URI.encode_www_form(page: 1, per_page: 20)

request = Net::HTTP::Get.new(uri)
request['Accept'] = 'application/json'
request['Authorization'] = 'Bearer your-token-here'

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.request(request)
end

users = JSON.parse(response.body)
users.each { |u| puts u['name'] }

POST 请求(发送 JSON)

uri = URI('https://api.example.com/users')

request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request.body = { name: '张三', email: 'zhangsan@example.com' }.to_json

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
  http.open_timeout = 5
  http.read_timeout = 10
  http.request(request)
end

case response
when Net::HTTPSuccess
  result = JSON.parse(response.body)
  puts "创建成功: ID=#{result['id']}"
when Net::HTTPClientError
  puts "客户端错误: #{response.code}"
when Net::HTTPServerError
  puts "服务器错误: #{response.code}"
end

POST 表单数据

uri = URI('https://api.example.com/login')

response = Net::HTTP.post_form(uri, {
  'username' => 'admin',
  'password' => 'secret'
})

puts response.body

⚡ Net::HTTP 要点

  • Net::HTTP.get 直接返回 body 字符串,get_response 返回完整响应对象
  • HTTPS 请求需设置 use_ssl: true
  • 使用 start 块可复用 TCP 连接发送多个请求
  • 对复杂场景(重试、中间件等),推荐使用 Faraday 等第三方库

2 Faraday HTTP 客户端

Faraday 是 Ruby 社区最流行的 HTTP 客户端库,提供统一接口、中间件机制和可插拔的适配器,大幅简化 HTTP 请求的编写。

gem install faraday faraday-multipart

基础请求

require 'faraday'
require 'json'

response = Faraday.get('https://jsonplaceholder.typicode.com/posts/1')
puts response.status
puts response.headers['content-type']
data = JSON.parse(response.body)
puts data['title']

response = Faraday.post(
  'https://api.example.com/users',
  { name: '张三', email: 'zhangsan@example.com' }.to_json,
  'Content-Type' => 'application/json'
)

创建连接实例(推荐)

conn = Faraday.new(url: 'https://api.example.com') do |f|
  f.request  :json
  f.response :json
  f.response :raise_error
  f.adapter  Faraday.default_adapter

  f.headers['Authorization'] = 'Bearer your-token'
  f.options.timeout = 10
  f.options.open_timeout = 5
end

response = conn.get('/users', page: 1, per_page: 20)
users = response.body
users.each { |u| puts u['name'] }

response = conn.post('/users') do |req|
  req.body = { name: '李四', email: 'lisi@example.com', age: 32 }
end
puts "创建成功: #{response.body}"

response = conn.put("/users/#{id}") do |req|
  req.body = { name: '李四更新' }
end

conn.delete("/users/#{id}")

错误处理

conn = Faraday.new(url: 'https://api.example.com') do |f|
  f.request  :json
  f.response :json
  f.response :raise_error
end

begin
  response = conn.get('/users/999')
rescue Faraday::ResourceNotFound => e
  puts "资源不存在: #{e.message}"
rescue Faraday::ClientError => e
  puts "客户端错误: #{e.response[:status]}"
rescue Faraday::ServerError => e
  puts "服务器错误: #{e.response[:status]}"
rescue Faraday::ConnectionFailed => e
  puts "连接失败: #{e.message}"
rescue Faraday::TimeoutError
  puts "请求超时"
end

自定义中间件

class LoggingMiddleware < Faraday::Middleware
  def call(env)
    puts "[HTTP] #{env.method.upcase} #{env.url}"
    start = Time.now

    response = @app.call(env)

    elapsed = ((Time.now - start) * 1000).round(1)
    puts "[HTTP] #{response.status} (#{elapsed}ms)"
    response
  end
end

conn = Faraday.new(url: 'https://api.example.com') do |f|
  f.use LoggingMiddleware
  f.request  :json
  f.response :json
  f.adapter  Faraday.default_adapter
end

3 JSON 处理

Ruby 标准库内置了 json 模块,提供高效的 JSON 编解码。所有 Ruby 对象(Hash、Array、String、Numeric 等)都可以直接序列化为 JSON。

编解码基础

require 'json'

data = { name: '张三', age: 30, skills: ['Ruby', 'Rails'] }
json_str = JSON.generate(data)
puts json_str

json_str = data.to_json
puts json_str

pretty = JSON.pretty_generate(data)
puts pretty

parsed = JSON.parse('{"name":"张三","age":30}')
puts parsed['name']
puts parsed.class

parsed = JSON.parse('{"name":"张三"}', symbolize_names: true)
puts parsed[:name]

错误处理

begin
  data = JSON.parse('invalid json')
rescue JSON::ParserError => e
  puts "JSON 解析失败: #{e.message}"
end

json_str = '{"key": "value"}'
data = JSON.parse(json_str) rescue nil
puts data ? data['key'] : '解析失败'

自定义对象的 JSON 序列化

class User
  attr_accessor :name, :email, :age

  def initialize(name:, email:, age:)
    @name = name
    @email = email
    @age = age
  end

  def to_json(*args)
    { name: @name, email: @email, age: @age }.to_json(*args)
  end

  def self.from_json(json_str)
    data = JSON.parse(json_str, symbolize_names: true)
    new(**data)
  end
end

user = User.new(name: '张三', email: 'zhangsan@example.com', age: 28)
puts user.to_json

user2 = User.from_json('{"name":"李四","email":"lisi@example.com","age":32}')
puts user2.name

处理 API 响应

require 'net/http'
require 'json'

uri = URI('https://jsonplaceholder.typicode.com/users')
response = Net::HTTP.get(uri)
users = JSON.parse(response)

users.each do |user|
  puts "#{user['name']} (#{user['email']})"
  address = user.dig('address', 'city')
  puts "  城市: #{address}" if address
end

geo = users.first.dig('address', 'geo', 'lat')
puts "纬度: #{geo}"

⚡ JSON 处理要点

  • JSON.parse 默认返回 String key 的 Hash,symbolize_names: true 转为 Symbol key
  • .to_json 方法在 Hash/Array 上直接可用(需 require 'json')
  • dig 方法安全地深层访问嵌套数据,不存在返回 nil 而非抛异常
  • JSON.pretty_generate 生成格式化输出,方便调试

4 Sinatra Web 框架入门

Sinatra 是 Ruby 的微型 Web 框架,用极少的代码就能构建 Web 应用和 API。它的设计哲学是简约——路由、处理器、模板一目了然。

gem install sinatra puma

基础路由

require 'sinatra'

get '/' do
  'Hello, World!'
end

get '/hello/:name' do
  "Hello, #{params[:name]}!"
end

get '/users' do
  content_type :json
  users = [
    { id: 1, name: '张三' },
    { id: 2, name: '李四' },
  ]
  users.to_json
end

post '/users' do
  data = JSON.parse(request.body.read)
  content_type :json
  status 201
  { id: 3, name: data['name'], message: '创建成功' }.to_json
end

put '/users/:id' do
  data = JSON.parse(request.body.read)
  content_type :json
  { id: params[:id].to_i, name: data['name'], message: '更新成功' }.to_json
end

delete '/users/:id' do
  content_type :json
  { message: "用户 #{params[:id]} 已删除" }.to_json
end

ERB 模板

require 'sinatra'

get '/profile/:name' do
  @name = params[:name]
  @skills = ['Ruby', 'Rails', 'Sinatra']
  erb :profile
end

__END__

@@layout
<!DOCTYPE html>
<html>
<body>
  <nav><a href="/">首页</a></nav>
  <%= yield %>
</body>
</html>

@@profile
<h1><%= @name %> 的主页</h1>
<ul>
  <% @skills.each do |skill| %>
    <li><%= skill %></li>
  <% end %>
</ul>

完整 JSON API 示例

require 'sinatra/base'
require 'json'

class TodoAPI < Sinatra::Base
  set :default_content_type, :json

  @@todos = []
  @@next_id = 1

  before do
    if request.content_type&.include?('application/json') && request.body.size > 0
      request.body.rewind
      @json = JSON.parse(request.body.read, symbolize_names: true)
    end
  end

  get '/api/todos' do
    @@todos.to_json
  end

  get '/api/todos/:id' do
    todo = @@todos.find { |t| t[:id] == params[:id].to_i }
    halt 404, { error: '未找到' }.to_json unless todo
    todo.to_json
  end

  post '/api/todos' do
    halt 422, { error: '标题不能为空' }.to_json unless @json&.dig(:title)

    todo = {
      id: @@next_id,
      title: @json[:title],
      completed: false,
      created_at: Time.now.iso8601
    }
    @@next_id += 1
    @@todos << todo

    status 201
    todo.to_json
  end

  patch '/api/todos/:id' do
    todo = @@todos.find { |t| t[:id] == params[:id].to_i }
    halt 404, { error: '未找到' }.to_json unless todo

    todo[:title] = @json[:title] if @json&.key?(:title)
    todo[:completed] = @json[:completed] if @json&.key?(:completed)
    todo.to_json
  end

  delete '/api/todos/:id' do
    todo = @@todos.find { |t| t[:id] == params[:id].to_i }
    halt 404, { error: '未找到' }.to_json unless todo

    @@todos.delete(todo)
    { message: '已删除' }.to_json
  end

  run! if app_file == $0
end

🔄 微框架对比

特性 Ruby Sinatra Node Express Python Flask PHP Slim
路由定义get '/' doapp.get('/', fn)@app.route('/')$app->get('/')
模板引擎ERB, HamlEJS, PugJinja2Twig
中间件RackExpress MWWSGIPSR-15
核心理念DSL 风格极简回调链装饰器路由PSR-7 标准

5 REST API 客户端实践

结合前面学到的知识,实现一个完整的 GitHub API 客户端,包含错误处理和重试逻辑。

GitHub API 客户端

require 'faraday'
require 'json'

class GitHubClient
  BASE_URL = 'https://api.github.com'
  MAX_RETRIES = 3

  def initialize(token: nil)
    @conn = Faraday.new(url: BASE_URL) do |f|
      f.request  :json
      f.response :json
      f.response :raise_error
      f.headers['Accept'] = 'application/vnd.github.v3+json'
      f.headers['Authorization'] = "Bearer #{token}" if token
      f.headers['User-Agent'] = 'Ruby GitHub Client'
      f.options.timeout = 15
    end
  end

  def user(username)
    with_retry { @conn.get("/users/#{username}").body }
  end

  def repos(username, sort: 'updated', per_page: 30)
    with_retry do
      @conn.get("/users/#{username}/repos", {
        sort: sort,
        per_page: per_page
      }).body
    end
  end

  def search_repos(query, sort: 'stars', per_page: 10)
    with_retry do
      response = @conn.get('/search/repositories', {
        q: query,
        sort: sort,
        per_page: per_page
      })
      response.body['items']
    end
  end

  def create_issue(owner, repo, title:, body: nil, labels: [])
    @conn.post("/repos/#{owner}/#{repo}/issues") do |req|
      req.body = { title: title, body: body, labels: labels }
    end.body
  end

  private

  def with_retry(retries: MAX_RETRIES)
    attempts = 0
    begin
      attempts += 1
      yield
    rescue Faraday::ServerError, Faraday::ConnectionFailed, Faraday::TimeoutError => e
      if attempts < retries
        wait = 2 ** attempts
        warn "[重试 #{attempts}/#{retries}] #{e.class}: #{e.message},等待 #{wait}s"
        sleep(wait)
        retry
      end
      raise
    end
  end
end

使用示例

client = GitHubClient.new(token: ENV['GITHUB_TOKEN'])

user = client.user('matz')
puts "#{user['name']} - #{user['bio']}"
puts "公开仓库: #{user['public_repos']}, 粉丝: #{user['followers']}"

repos = client.repos('matz', sort: 'stars', per_page: 5)
repos.each do |repo|
  puts "  #{repo['name']} ⭐ #{repo['stargazers_count']}"
end

results = client.search_repos('ruby web framework', per_page: 5)
results.each do |repo|
  puts "#{repo['full_name']} ⭐ #{repo['stargazers_count']}"
  puts "  #{repo['description']}"
end

⚡ API 客户端最佳实践

  • 封装为类,集中管理 base URL、认证、超时配置
  • 实现指数退避重试(2n 秒),只对瞬态错误重试
  • 使用 Faraday 的 :json 请求/响应中间件自动处理序列化
  • 敏感信息(Token)通过环境变量传入,不硬编码

6 本章要点

🌐 Net::HTTP

标准库内置,零依赖;get_response 获取完整响应,start 块复用连接

🔗 Faraday

社区首选 HTTP 客户端;中间件机制、自动 JSON 序列化、可插拔适配器

📋 JSON

标准库内置;parse/generatesymbolize_names 转 Symbol key,dig 安全嵌套访问

🎸 Sinatra

微型 Web 框架;DSL 风格路由定义,ERB 模板,构建 REST API 极简

🔄 重试机制

指数退避策略,只重试瞬态错误(超时、5xx),封装到 with_retry 方法

🏗️ 客户端封装

将 API 调用封装为 Ruby 类,集中管理认证、超时、错误处理

下一章预告:第六章将讲解 Ruby 的文件操作能力——文件读写、目录遍历、CSV/JSON/YAML 数据格式处理,打通数据持久化的最后一环。