Ruby Web服务
Web服务是现代应用程序架构的重要组成部分,它们允许不同系统之间通过网络进行通信和数据交换。Ruby提供了多种创建和消费Web服务的方式,从简单的HTTP服务器到复杂的RESTful API。本章将详细介绍如何在Ruby中创建Web服务,包括使用内置库和流行的框架如Sinatra和Rails。
🎯 Web服务基础
什么是Web服务
Web服务是一种通过网络提供功能的软件系统,它使用标准化的协议(如HTTP、XML、JSON)进行通信。Web服务的主要特点包括:
- 互操作性: 不同平台和语言的系统可以相互调用
- 松耦合: 服务提供者和消费者之间依赖关系最小
- 可重用性: 同一个服务可以被多个应用程序使用
- 可发现性: 服务可以通过标准方式被发现和调用
Web服务类型
- SOAP Web服务: 基于XML的协议,使用WSDL描述服务
- RESTful Web服务: 基于HTTP协议,使用JSON/XML传输数据
- GraphQL: 现代API查询语言,允许客户端精确指定需要的数据
🌐 使用内置库创建Web服务
基本HTTP服务器
ruby
require 'webrick'
# 创建简单的HTTP服务器
class SimpleHTTPServer
def initialize(port = 8080)
@port = port
@server = nil
end
def start
# 配置服务器
config = {
Port: @port,
DocumentRoot: './public'
}
@server = WEBrick::HTTPServer.new(config)
# 定义路由处理器
@server.mount_proc('/hello') do |req, res|
res.body = 'Hello, World!'
res['Content-Type'] = 'text/plain'
end
@server.mount_proc('/api/time') do |req, res|
res.body = {
time: Time.now.to_s,
timestamp: Time.now.to_i
}.to_json
res['Content-Type'] = 'application/json'
end
# 处理POST请求
@server.mount_proc('/api/echo') do |req, res|
res.body = {
method: req.request_method,
path: req.path,
headers: req.header,
body: req.body
}.to_json
res['Content-Type'] = 'application/json'
end
# 优雅关闭
trap('INT') { @server.shutdown }
puts "服务器启动,监听端口 #{@port}"
@server.start
end
end
# 启动服务器
# server = SimpleHTTPServer.new(8080)
# server.start
# 处理静态文件
class StaticFileHandler < WEBrick::HTTPServlet::AbstractServlet
def do_GET(request, response)
file_path = request.path == '/' ? '/index.html' : request.path
full_path = File.join('./public', file_path)
if File.exist?(full_path) && !File.directory?(full_path)
response.body = File.read(full_path)
response['Content-Type'] = get_content_type(full_path)
response.status = 200
else
response.body = '<h1>404 Not Found</h1>'
response['Content-Type'] = 'text/html'
response.status = 404
end
end
private
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 '.json' then 'application/json'
when '.png' then 'image/png'
when '.jpg', '.jpeg' then 'image/jpeg'
else 'text/plain'
end
end
end
# 使用静态文件处理器
# server = WEBrick::HTTPServer.new(Port: 8080, DocumentRoot: './public')
# server.mount('/', StaticFileHandler)
# trap('INT') { server.shutdown }
# server.startRESTful API服务器
ruby
require 'webrick'
require 'json'
# 简单的内存数据库
class InMemoryDB
def initialize
@data = {}
@next_id = 1
end
def create(item)
id = @next_id
@next_id += 1
@data[id] = item.merge('id' => id, 'created_at' => Time.now.to_i)
@data[id]
end
def find(id)
@data[id.to_i]
end
def all
@data.values
end
def update(id, item)
return nil unless @data[id.to_i]
@data[id.to_i] = item.merge('id' => id.to_i, 'updated_at' => Time.now.to_i)
@data[id.to_i]
end
def delete(id)
@data.delete(id.to_i)
end
end
# RESTful API处理器
class RESTAPIHandler < WEBrick::HTTPServlet::AbstractServlet
def initialize(server, db)
super(server)
@db = db
end
def do_GET(request, response)
case request.path
when '/api/users'
# 获取所有用户
users = @db.all
send_json_response(response, users, 200)
when %r{/api/users/(\d+)}
# 获取特定用户
id = $1
user = @db.find(id)
if user
send_json_response(response, user, 200)
else
send_error_response(response, '用户未找到', 404)
end
else
send_error_response(response, '路径未找到', 404)
end
end
def do_POST(request, response)
case request.path
when '/api/users'
# 创建用户
begin
user_data = JSON.parse(request.body)
user = @db.create(user_data)
send_json_response(response, user, 201)
rescue JSON::ParserError
send_error_response(response, '无效的JSON格式', 400)
end
else
send_error_response(response, '路径未找到', 404)
end
end
def do_PUT(request, response)
case request.path
when %r{/api/users/(\d+)}
# 更新用户
id = $1
begin
user_data = JSON.parse(request.body)
user = @db.update(id, user_data)
if user
send_json_response(response, user, 200)
else
send_error_response(response, '用户未找到', 404)
end
rescue JSON::ParserError
send_error_response(response, '无效的JSON格式', 400)
end
else
send_error_response(response, '路径未找到', 404)
end
end
def do_DELETE(request, response)
case request.path
when %r{/api/users/(\d+)}
# 删除用户
id = $1
user = @db.delete(id)
if user
send_json_response(response, { 'message' => '用户已删除' }, 200)
else
send_error_response(response, '用户未找到', 404)
end
else
send_error_response(response, '路径未找到', 404)
end
end
private
def send_json_response(response, data, status)
response.body = data.to_json
response['Content-Type'] = 'application/json'
response.status = status
end
def send_error_response(response, message, status)
response.body = { 'error' => message }.to_json
response['Content-Type'] = 'application/json'
response.status = status
end
end
# 启动RESTful API服务器
# db = InMemoryDB.new
# server = WEBrick::HTTPServer.new(Port: 8080)
# server.mount('/api', RESTAPIHandler, db)
#
# # 添加示例数据
# db.create({ 'name' => '张三', 'email' => 'zhangsan@example.com' })
# db.create({ 'name' => '李四', 'email' => 'lisi@example.com' })
#
# trap('INT') { server.shutdown }
# puts "RESTful API服务器启动,监听端口 8080"
# server.start🎵 使用Sinatra创建Web服务
Sinatra基础
Sinatra是一个轻量级的Ruby Web框架,非常适合创建RESTful API和小型Web应用:
ruby
# 首先安装Sinatra
# gem install sinatra
require 'sinatra'
require 'json'
# 基本Sinatra应用
get '/' do
'Hello, Sinatra!'
end
get '/hello/:name' do
"Hello, #{params[:name]}!"
end
# JSON API
get '/api/time' do
content_type :json
{
time: Time.now.to_s,
timestamp: Time.now.to_i
}.to_json
end
# POST请求处理
post '/api/users' do
content_type :json
begin
user_data = JSON.parse(request.body.read)
# 这里应该保存到数据库
user_data['id'] = rand(1000)
user_data['created_at'] = Time.now.to_i
status 201
user_data.to_json
rescue JSON::ParserError
status 400
{ error: '无效的JSON格式' }.to_json
end
end
# 启动应用
# ruby app.rb完整的Sinatra API示例
ruby
require 'sinatra'
require 'json'
require 'securerandom'
# 用户模型
class User
attr_accessor :id, :name, :email, :created_at
def initialize(name, email)
@id = SecureRandom.uuid
@name = name
@email = email
@created_at = Time.now
end
def to_hash
{
id: @id,
name: @name,
email: @email,
created_at: @created_at
}
end
end
# 用户服务
class UserService
def initialize
@users = {}
end
def create(name, email)
user = User.new(name, email)
@users[user.id] = user
user
end
def find(id)
@users[id]
end
def all
@users.values
end
def update(id, name, email)
return nil unless @users[id]
@users[id].name = name if name
@users[id].email = email if email
@users[id]
end
def delete(id)
@users.delete(id)
end
end
# 全局用户服务实例
USER_SERVICE = UserService.new
# 配置
set :bind, '0.0.0.0'
set :port, 4567
# 中间件
before do
content_type :json
@request_body = request.body.read
end
# 错误处理
error 404 do
{ error: '页面未找到' }.to_json
end
error 500 do
{ error: '服务器内部错误' }.to_json
end
# 路由
# 首页
get '/' do
{
message: '欢迎使用用户管理API',
version: '1.0.0',
endpoints: [
'GET /api/users',
'GET /api/users/:id',
'POST /api/users',
'PUT /api/users/:id',
'DELETE /api/users/:id'
]
}.to_json
end
# 获取所有用户
get '/api/users' do
users = USER_SERVICE.all.map(&:to_hash)
users.to_json
end
# 获取特定用户
get '/api/users/:id' do
user = USER_SERVICE.find(params[:id])
if user
user.to_hash.to_json
else
status 404
{ error: '用户未找到' }.to_json
end
end
# 创建用户
post '/api/users' do
begin
data = JSON.parse(@request_body)
# 验证必需字段
if data['name'].nil? || data['email'].nil?
status 400
{ error: '姓名和邮箱是必需的' }.to_json
else
user = USER_SERVICE.create(data['name'], data['email'])
status 201
user.to_hash.to_json
end
rescue JSON::ParserError
status 400
{ error: '无效的JSON格式' }.to_json
end
end
# 更新用户
put '/api/users/:id' do
begin
user = USER_SERVICE.find(params[:id])
if user
data = JSON.parse(@request_body)
updated_user = USER_SERVICE.update(params[:id], data['name'], data['email'])
updated_user.to_hash.to_json
else
status 404
{ error: '用户未找到' }.to_json
end
rescue JSON::ParserError
status 400
{ error: '无效的JSON格式' }.to_json
end
end
# 删除用户
delete '/api/users/:id' do
user = USER_SERVICE.find(params[:id])
if user
USER_SERVICE.delete(params[:id])
{ message: '用户已删除' }.to_json
else
status 404
{ error: '用户未找到' }.to_json
end
end
# 启动应用
# ruby app.rb🚀 使用Rails创建Web服务
Rails API模式
Rails 5+提供了API模式,专门用于创建Web服务:
bash
# 创建Rails API应用
# rails new my_api --api
# cd my_apiruby
# app/models/user.rb
class User < ApplicationRecord
validates :name, presence: true
validates :email, presence: true, uniqueness: true
# 序列化为JSON时包含的字段
def as_json(options = {})
super(options.merge(only: [:id, :name, :email, :created_at, :updated_at]))
end
endruby
# app/controllers/api/v1/users_controller.rb
class Api::V1::UsersController < ApplicationController
before_action :set_user, only: [:show, :update, :destroy]
# GET /api/v1/users
def index
@users = User.all
render json: @users
end
# GET /api/v1/users/1
def show
render json: @user
end
# POST /api/v1/users
def create
@user = User.new(user_params)
if @user.save
render json: @user, status: :created
else
render json: { errors: @user.errors }, status: :unprocessable_entity
end
end
# PUT /api/v1/users/1
def update
if @user.update(user_params)
render json: @user
else
render json: { errors: @user.errors }, status: :unprocessable_entity
end
end
# DELETE /api/v1/users/1
def destroy
@user.destroy
head :no_content
end
private
def set_user
@user = User.find(params[:id])
rescue ActiveRecord::RecordNotFound
render json: { error: '用户未找到' }, status: :not_found
end
def user_params
params.require(:user).permit(:name, :email)
end
endruby
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :users
end
end
end🔌 Web服务客户端
使用Net::HTTP
ruby
require 'net/http'
require 'json'
require 'uri'
class WebServiceClient
def initialize(base_url)
@base_url = base_url
@uri = URI.parse(base_url)
end
def get(path)
uri = URI.join(@uri, path)
response = Net::HTTP.get_response(uri)
parse_response(response)
end
def post(path, data)
uri = URI.join(@uri, path)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
request = Net::HTTP::Post.new(uri)
request['Content-Type'] = 'application/json'
request.body = data.to_json
response = http.request(request)
parse_response(response)
end
def put(path, data)
uri = URI.join(@uri, path)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
request = Net::HTTP::Put.new(uri)
request['Content-Type'] = 'application/json'
request.body = data.to_json
response = http.request(request)
parse_response(response)
end
def delete(path)
uri = URI.join(@uri, path)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
request = Net::HTTP::Delete.new(uri)
response = http.request(request)
parse_response(response)
end
private
def parse_response(response)
case response
when Net::HTTPSuccess
JSON.parse(response.body) rescue response.body
else
{
error: true,
status: response.code,
message: response.message,
body: response.body
}
end
end
end
# 使用示例
# client = WebServiceClient.new('http://localhost:8080')
#
# # GET请求
# users = client.get('/api/users')
# puts "用户列表: #{users}"
#
# # POST请求
# new_user = client.post('/api/users', { name: '王五', email: 'wangwu@example.com' })
# puts "创建用户: #{new_user}"使用HTTParty
ruby
# 首先安装HTTParty
# gem install httparty
require 'httparty'
class HTTPartyClient
include HTTParty
base_uri 'http://localhost:8080'
def self.get_users
get('/api/users')
end
def self.get_user(id)
get("/api/users/#{id}")
end
def self.create_user(user_data)
post('/api/users', body: user_data.to_json, headers: { 'Content-Type' => 'application/json' })
end
def self.update_user(id, user_data)
put("/api/users/#{id}", body: user_data.to_json, headers: { 'Content-Type' => 'application/json' })
end
def self.delete_user(id)
delete("/api/users/#{id}")
end
end
# 使用示例
# users = HTTPartyClient.get_users
# puts users
# new_user = HTTPartyClient.create_user({ name: '赵六', email: 'zhaoliu@example.com' })
# puts new_user🎯 Web服务安全
身份验证
ruby
require 'sinatra'
require 'json'
require 'jwt'
# JWT密钥
JWT_SECRET = 'your_secret_key'
# 用户存储(实际应用中应该使用数据库)
USERS = {
'admin' => 'password123',
'user' => 'userpass456'
}
# 生成JWT令牌
def generate_token(username)
payload = {
username: username,
exp: Time.now.to_i + 3600 # 1小时过期
}
JWT.encode(payload, JWT_SECRET, 'HS256')
end
# 验证JWT令牌
def verify_token(token)
JWT.decode(token, JWT_SECRET, true, algorithm: 'HS256')
rescue JWT::DecodeError
nil
end
# 身份验证中间件
def authenticate!
token = request.env['HTTP_AUTHORIZATION']&.split(' ')&.last
if token
begin
payload = verify_token(token)
@current_user = payload[0]['username']
rescue
halt 401, { error: '无效的令牌' }.to_json
end
else
halt 401, { error: '缺少认证令牌' }.to_json
end
end
# 登录端点
post '/auth/login' do
content_type :json
begin
data = JSON.parse(request.body.read)
username = data['username']
password = data['password']
if USERS[username] == password
token = generate_token(username)
{ token: token, username: username }.to_json
else
status 401
{ error: '用户名或密码错误' }.to_json
end
rescue JSON::ParserError
status 400
{ error: '无效的JSON格式' }.to_json
end
end
# 受保护的端点
get '/api/protected' do
content_type :json
authenticate!
{ message: "Hello, #{@current_user}!", protected_data: '这是受保护的数据' }.to_json
end
# 启动应用
# ruby app.rb请求验证
ruby
require 'sinatra'
require 'json'
# 参数验证器
class ParameterValidator
def self.validate_user_params(params)
errors = []
if params['name'].nil? || params['name'].strip.empty?
errors << '姓名是必需的'
end
if params['email'].nil? || params['email'].strip.empty?
errors << '邮箱是必需的'
elsif !params['email'].match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
errors << '邮箱格式不正确'
end
errors
end
def self.validate_pagination_params(params)
errors = []
page = params['page']&.to_i
per_page = params['per_page']&.to_i
if page && page < 1
errors << '页码必须大于0'
end
if per_page && (per_page < 1 || per_page > 100)
errors << '每页数量必须在1-100之间'
end
errors
end
end
# 使用参数验证
post '/api/users' do
content_type :json
begin
data = JSON.parse(request.body.read)
errors = ParameterValidator.validate_user_params(data)
if errors.empty?
# 处理有效的数据
{ message: '用户创建成功', user: data }.to_json
else
status 400
{ errors: errors }.to_json
end
rescue JSON::ParserError
status 400
{ error: '无效的JSON格式' }.to_json
end
end
get '/api/users' do
content_type :json
errors = ParameterValidator.validate_pagination_params(params)
if errors.empty?
page = (params['page'] || 1).to_i
per_page = (params['per_page'] || 10).to_i
{
page: page,
per_page: per_page,
data: [],
total: 0
}.to_json
else
status 400
{ errors: errors }.to_json
end
end📊 Web服务测试
使用RSpec测试API
ruby
# Gemfile
# gem 'rspec'
# gem 'rack-test'
require 'rspec'
require 'rack/test'
# 假设这是我们的Sinatra应用
class MyApp < Sinatra::Base
get '/api/hello' do
content_type :json
{ message: 'Hello, World!' }.to_json
end
post '/api/users' do
content_type :json
begin
data = JSON.parse(request.body.read)
status 201
{ id: 1, name: data['name'] }.to_json
rescue JSON::ParserError
status 400
{ error: 'Invalid JSON' }.to_json
end
end
end
# 测试文件
RSpec.describe MyApp, type: :request do
include Rack::Test::Methods
def app
MyApp
end
describe 'GET /api/hello' do
it 'returns a hello message' do
get '/api/hello'
expect(last_response).to be_ok
expect(last_response.content_type).to include('application/json')
response_data = JSON.parse(last_response.body)
expect(response_data['message']).to eq('Hello, World!')
end
end
describe 'POST /api/users' do
context 'with valid JSON' do
it 'creates a user' do
user_data = { name: '张三' }.to_json
post '/api/users', user_data, 'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq(201)
expect(last_response.content_type).to include('application/json')
response_data = JSON.parse(last_response.body)
expect(response_data['name']).to eq('张三')
expect(response_data['id']).to be_present
end
end
context 'with invalid JSON' do
it 'returns an error' do
post '/api/users', 'invalid json', 'CONTENT_TYPE' => 'application/json'
expect(last_response.status).to eq(400)
expect(last_response.content_type).to include('application/json')
response_data = JSON.parse(last_response.body)
expect(response_data['error']).to eq('Invalid JSON')
end
end
end
end性能测试
ruby
require 'benchmark'
require 'net/http'
require 'uri'
# 性能测试工具
class PerformanceTester
def self.test_endpoint(url, requests = 100)
uri = URI(url)
time = Benchmark.measure do
requests.times do
Net::HTTP.get_response(uri)
end
end
{
total_requests: requests,
total_time: time.real,
requests_per_second: requests / time.real,
average_response_time: (time.real / requests) * 1000 # 毫秒
}
end
def self.test_concurrent_requests(url, requests = 100, concurrency = 10)
uri = URI(url)
threads = []
time = Benchmark.measure do
# 分批并发请求
(requests / concurrency).times do
concurrency.times do
threads << Thread.new do
Net::HTTP.get_response(uri)
end
end
# 等待这批请求完成
threads.each(&:join)
threads.clear
end
end
{
total_requests: requests,
concurrency: concurrency,
total_time: time.real,
requests_per_second: requests / time.real
}
end
end
# 使用示例
# result = PerformanceTester.test_endpoint('http://localhost:8080/api/hello', 1000)
# puts "性能测试结果:"
# puts "总请求数: #{result[:total_requests]}"
# puts "总时间: #{result[:total_time].round(2)}秒"
# puts "每秒请求数: #{result[:requests_per_second].round(2)}"
# puts "平均响应时间: #{result[:average_response_time].round(2)}毫秒"🎯 Web服务最佳实践
1. 版本控制
ruby
require 'sinatra'
# API版本控制
class APIv1 < Sinatra::Base
get '/users' do
content_type :json
{ version: 'v1', users: [] }.to_json
end
end
class APIv2 < Sinatra::Base
get '/users' do
content_type :json
{ version: 'v2', users: [], metadata: {} }.to_json
end
end
# 主应用路由到不同版本
class MainApp < Sinatra::Base
use APIv1, '/api/v1'
use APIv2, '/api/v2'
get '/' do
'API网关'
end
end
# 启动应用
# run MainApp2. 错误处理和日志
ruby
require 'sinatra'
require 'logger'
# 配置日志
logger = Logger.new(STDOUT)
logger.level = Logger::INFO
# 全局错误处理
error do
error = env['sinatra.error']
logger.error "错误: #{error.message}\n#{error.backtrace.join("\n")}"
content_type :json
status 500
{
error: '内部服务器错误',
request_id: request.env['HTTP_X_REQUEST_ID']
}.to_json
end
# 请求日志中间件
before do
request.env['HTTP_X_REQUEST_ID'] = SecureRandom.uuid
logger.info "请求开始: #{request.request_method} #{request.path} (ID: #{request.env['HTTP_X_REQUEST_ID']})"
end
after do
logger.info "请求结束: #{request.request_method} #{request.path} - #{status} (ID: #{request.env['HTTP_X_REQUEST_ID']})"
end
get '/api/test' do
content_type :json
{ message: '测试成功' }.to_json
end3. 缓存策略
ruby
require 'sinatra'
require 'redis'
# Redis缓存客户端
class CacheClient
def initialize
@redis = Redis.new(host: 'localhost', port: 6379)
rescue
@redis = nil
end
def get(key)
return nil unless @redis
@redis.get(key)
end
def set(key, value, ttl = 3600)
return unless @redis
@redis.setex(key, ttl, value)
end
def delete(key)
return unless @redis
@redis.del(key)
end
end
# 缓存中间件
CACHE = CacheClient.new
def cached_response(key, ttl = 3600)
# 尝试从缓存获取
cached = CACHE.get(key)
if cached
logger.info "缓存命中: #{key}"
return cached
end
# 执行实际操作
result = yield
# 存储到缓存
CACHE.set(key, result, ttl)
logger.info "缓存存储: #{key}"
result
end
get '/api/expensive-operation' do
content_type :json
result = cached_response("expensive:#{Time.now.to_i / 60}") do
# 模拟耗时操作
sleep(2)
{
message: '耗时操作完成',
timestamp: Time.now.to_i,
data: (1..100).to_a.sample(10)
}.to_json
end
result
end📚 下一步学习
掌握了Ruby Web服务开发后,建议继续学习:
- Ruby 多线程 - 学习并发编程
- Ruby JSON - 掌握JSON数据处理
- Ruby 数据库访问 - 学习数据库操作
- Ruby 参考手册及学习资源 - 获取更多学习资料
继续您的Ruby学习之旅吧!