Ruby 版服务器框架

前提条件

服务器实现所需的 Gem:

  • google-protobuf(本教程中使用的 3.2.X 版)
  • grpc(本教程中使用了 1.2.X)

下载服务定义并创建以下目录结构:

[base_dir]
├── certificates
├── lib
├── protos
    └── booking_service.proto
└── server.rb

根据接口说明生成 Ruby 库:

$ cd [base_dir]
$ grpc_tools_ruby_protoc -I protos --ruby_out=lib --grpc_out=lib protos/booking_service.proto

实现服务器

框架实现:

#!/usr/bin/ruby2.0

# Copyright 2017, Google Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
#     * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#     * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution.
#     * Neither the name of Google Inc. nor the names of its
# contributors may be used to endorse or promote products derived from
# this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

# Sample gRPC server that implements the BookingService and enforces mutual
# authentication.
#
# Usage: $ path/to/server.rb

this_dir = File.expand_path(File.dirname(__FILE__))
lib_dir = File.join(this_dir, 'lib')
$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)

require 'grpc'
require 'grpc/health/checker'
require 'grpc/health/v1/health_services_pb'
require 'openssl'
require 'optparse'
require 'booking_service_services_pb.rb'

# Peer certificate validation that checks for a specific CN in the subject
# name. Add additional valid CNs, e.g. for a test client, to the array.
ACCEPTED_CNS = ["mapsbooking.businesslink-3.net"]
def check_peer_cert(grpc_call)
  valid_cert = false
  certificate = OpenSSL::X509::Certificate.new grpc_call.peer_cert
  certificate.subject().to_a().each do |name_entry|
    if (name_entry[0] == "CN") && ACCEPTED_CNS.include?(name_entry[1])
      valid_cert = true
    end
  end
  unless valid_cert
    fail GRPC::BadStatus.new(GRPC::Core::StatusCodes::UNAUTHENTICATED,
                             "Client cert has invalid CN")
  end
end


# BookingServer is a simple server that implements the BookingService.
class BookingServer < Ext::Maps::Booking::Partner::V2::BookingService::Service

  PartnerApi = Ext::Maps::Booking::Partner::V2

  def initialize(peer_cert_validator)
    @peer_cert_validator = peer_cert_validator
  end

  def check_availability(availability_req, grpc_call)
    @peer_cert_validator.call(grpc_call)
    slot = availability_req.slot.dup
    count_available = 0
    # Perform availability check
    #
    # For error conditions (example):
    #  fail GRPC::BadStatus.new(GRPC::Core::StatusCodes::INVALID_ARGUMENT,
    #                           "Invalid merchant id")
    #
    # Happy path: populate the response
    # [...]
    # Populate the slot and its count_available
    PartnerApi::CheckAvailabilityResponse.new(slot: slot,
                                              count_available: count_available)
  end

  def create_booking(booking_req, grpc_call)
    @peer_cert_validator.call(grpc_call)
    booking = PartnerApi::Booking.new
    user_payment_option = PartnerApi::UserPaymentOption.new
    # Create a booking according to the request
    #
    # For error conditions (example):
    #  fail GRPC::BadStatus.new(GRPC::Core::StatusCodes::INVALID_ARGUMENT,
    #                           "Invalid merchant id")
    #
    # For business logic error (example):
    #  booking_failure = PartnerApi::BookingFailure.new(
    #      cause: PartnerApi::BookingFailure::Cause::SLOT_UNAVAILABLE)
    #  PartnerApi::CreateBookingResponse.new(booking_failure: booking_failure)
    #
    # Happy path: populate the response
    # [...]
    # Assign a booking ID
    # booking.booking_id = ...
    # Populate the booking and the user payment option
    PartnerApi::CreateBookingResponse.new(
            booking: booking,
            user_payment_option: user_payment_option)
  end

  def update_booking(booking_req, grpc_call)
    @peer_cert_validator.call(grpc_call)
    booking = PartnerApi::Booking.new
    user_payment_option = PartnerApi::UserPaymentOption.new
    # * Look up the booking with ID booking_req.booking.booking_id.
    # * Update according to the request and return the updated booking.
    #
    # For error conditions (example):
    #  fail GRPC::BadStatus.new(GRPC::Core::StatusCodes::NOT_FOUND,
    #                           "Unknown booking ID")
    #
    # For business logic error (example):
    #  booking_failure = PartnerApi::BookingFailure.new(
    #      cause: PartnerApi::BookingFailure::Cause::SLOT_UNAVAILABLE)
    #  PartnerApi::UpdateBookingResponse.new(booking_failure: booking_failure)
    #
    # Happy path: populate the response
    # [...]
    # Populate the updated booking and user payment option.
    PartnerApi::UpdateBookingResponse.new(
            booking: booking,
            user_payment_option: user_payment_option)
  end

  def get_booking_status(status_req, grpc_call)
    @peer_cert_validator.call(grpc_call)
    booking_id = status_req.booking_id.dup
    booking_status = PartnerApi::BookingStatus.new
    prepayment_status = PartnerApi::PrepaymentStatus.new
    # * Look up the booking status and the prepayment status with ID
    # * status_req.booking_id.
    # * Return the booking status and the prepayment status.
    #
    # For rror conditions (example):
    #  fail GRPC::BadStatus.new(GRPC::Core::StatusCodes::NOT_FOUND,
    #                           "Unknown booking ID")
    #
    # Happy path: populate the response
    # [...]
    # Populate the booking status and the prepayment status.
    PartnerApi::GetBookingStatusResponse.new(
            booking_id: booking_id,
            booking_status: booking_status,
            prepayment_status: prepayment_status)
  end

  def list_bookings(list_req, grpc_call)
    @peer_cert_validator.call(grpc_call)
    bookings = []
    # * Look up all bookings with user ID list_req.user_id.
    # * Return the bookings
    #
    # For rror conditions (example):
    #  fail GRPC::BadStatus.new(GRPC::Core::StatusCodes::NOT_FOUND,
    #                            "Unknown user ID")
    #
    # Happy path: populate the response
    # [...]
    # Populate all bookings.
    response = PartnerApi::ListBookingsResponse.new
    response.bookings.extend(bookings)
    response
  end

end


# Loads the certificates for the test server.
def load_server_certs
  this_dir = File.expand_path(File.dirname(__FILE__))
  cert_dir = File.join(this_dir, 'certificates')
  # In order:
  # * PEM file with trusted root certificates for client auth
  # * Server private key
  # * PEM file with server certificate chain
  files = ['trusted_client_roots.pem', 'server.key', 'server.pem']
  files.map { |f| File.open(File.join(cert_dir, f)).read }
end


# Creates ServerCredentials from certificates.
def server_credentials
  certs = load_server_certs
  GRPC::Core::ServerCredentials.new(
      certs[0], [{private_key: certs[1], cert_chain: certs[2]}], true)
end


def main
  # Parse command line arguments
  disable_tls = false
  OptionParser.new do |opts|
    opts.on('--disable_tls',
            'true to disable TLS. NOT FOR PRODUCTION USE!') do |v|
      disable_tls = v
    end
  end.parse!

  s = GRPC::RpcServer.new
  # Listen on port 50051 on all interfaces. Update for production use.
  s.add_http2_port('[::]:50051',
                   disable_tls ? :this_port_is_insecure : server_credentials)

  cert_validator = disable_tls ? ->(grpc_call) {} : method(:check_peer_cert)
  s.handle(BookingServer.new cert_validator)

  health_checker = Grpc::Health::Checker.new
  health_checker.add_status(
      "ext.maps.booking.partner.v2.BookingService",
      Grpc::Health::V1::HealthCheckResponse::ServingStatus::SERVING)
  s.handle(health_checker)
  s.run_till_terminated
end

if __FILE__ == $0
  main()
end
</pre>

在不使用 TLS 的情况下测试服务器

对于初始测试,可以停用 TLS:

$ cd [base_dir]
$ ruby server.rb --disable_tls

这不适合用于生产用途!

配置生产证书

如需在服务器上启用 TLS,需要以下文件:

  • certificates/server.pem:服务器的证书链(采用 PEM 格式)
  • certificates/server.key:服务器证书链的私钥
  • certificates/trusted_client_roots.pem:对客户端进行身份验证时信任的根证书

对客户端进行身份验证时,将使用受信任的客户端根证书集。您可以选择从 Mozilla 等机构获取这组可信根,也可以安装 Google 互联网管理局 G2 当前建议的这组可信根。在后一种情况下,您可能必须不时手动更新根证书。

最终目录结构

[base_dir]
├── certificates
    ├── server.pem
    ├── server.key
    └── trusted_client_roots.pem
├── lib
    ├── booking_service_pb.rb
    └── booking_service_services_pb.rb
├── protos
    └── booking_service.proto
└── server.rb