上一篇写了Thrift,这篇就把当下比较流行的跨语言服务调用框架gRPC一起介绍下。gRPC来自Google(所以要好好学习,就需要ti子了),如果你了解过Thrift,那gRPC跟其很类似,也是一个RPC的框架。gRPC采用Protocol Buffers(即protobuf)做为传输协议,传输格式是二进制的。官网有一个简洁的图来说明gRPC客户端与服务端之间的调用关系。

gRPC Call

网上常拿”gRPC”和”HTTP Restful”来比较优劣,这里不做比较分析,只介绍怎么使用gRPC来实现跨语言的方法调用。本文分别用Python和Ruby来做介绍,因为很多语言都需要科学的上网方式来安装,很抓狂,这两个语言稍微简单些。

编写接口文件

  • 我们写一个和上一篇一样的接口,即一个整数加法和一个字符串拼接。接口文件是protobuf格式,以”*.proto”命名。我们写一个”tester.proto”文件
syntax = "proto3";  // 协议版本,目前最新是proto3

package mytest;  // 包名

// 接口,定义两个方法
service Tester {
    rpc Add(NumberPair) returns (NumberResult);  // 整型加法
    rpc Merge(StringPair) returns (StringResult);  // 字符串连接
}

// Add函数的输入,两个整型变量
message NumberPair {
    int32 num1 = 1;
    int32 num2 = 2;
}
// Add函数的返回,一个整型变量
message NumberResult {
    int32 value = 1;
}

// Merge函数的输入,两个字符串变量
message StringPair {
    string str1 = 1;
    string str2 = 2;
}
// Merge函数的返回,一个字符串变量
message StringResult {
    string value = 1;
}

编写Python Client和Server

  • 首先安装Python环境,这里使用Python3.6版本,可以直接通过pip安装
$ pip3 install grpcio
$ pip3 install grpcio-tools
  • 生成Python的代码,在proto文件目录下,执行以下命令
$ mkdir py
$ python3 -m grpc_tools.protoc -I. --python_out=./py --grpc_python_out=./py tester.proto
  • 成功的话,你会在”py”子目录下看到”tester_pb2.py”和”tester_pb2_grpc.py”两个文件,前一个文件定义了数据类型,后一个文件定义了RPC调用方法。

  • github上下载gRPC源码,源码中”examples/python”目录下有Python的代码范例,照着写即可,本例我们照着 “helloworld”下的”greeter_server.py”和”greeter_client.py”来编写服务端和客户端代码。

  • 先编写服务端代码,在刚才生成代码的”py”目录下,编写”tester_server.py”文件,内容如下,说明都放在注释上了

#coding=utf-8
from concurrent import futures
import logging
import grpc

# 引入自动生成的代码
import tester_pb2
import tester_pb2_grpc

# 继承tester_pb2_grpc中的TesterServicer接口,并实现proto中的两个方法
class TesterServicer(tester_pb2_grpc.TesterServicer):
    def Add(self, request, context):
        # 初始化NumberResult类型返回值
        response = tester_pb2.NumberResult()  
        # request是NumberPair类型,因此有num1和num2两个变量
        print('Add(%d, %d) is called' % (request.num1, request.num2))
        response.value = request.num1 + request.num2
        return response

    def Merge(self, request, context):
        # 初始化StringResult类型返回值
        response = tester_pb2.StringResult()  
        # request是StringPair类型,因此有str1和str2两个变量
        print('Merge(%s, %s) is called' % (request.str1, request.str2))
        response.value = request.str1 + request.str2
        return response

def serve():
    # 创建RPC Server
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    # 注册RPC调用方法
    tester_pb2_grpc.add_TesterServicer_to_server(TesterServicer(), server)
    server.add_insecure_port('[::]:50051')  # 监听50051端口
    server.start()  # 启动Server
    print('Server Starting...')
    server.wait_for_termination()

if __name__ == '__main__':
    logging.basicConfig()
    serve()
  • 然后,编写客户端代码”tester_client.py”,内容如下:
#coding=utf-8
from __future__ import print_function
import logging
import grpc

# 引入自动生成的代码
import tester_pb2
import tester_pb2_grpc


def run():
    # 通过本地端口50051建立通道
    with grpc.insecure_channel('localhost:50051') as channel:
        # 获取RPC调用的代理类
        stub = tester_pb2_grpc.TesterStub(channel)
        response = stub.Add(tester_pb2.NumberPair(num1=3, num2=4))  # 调用Add方法
        print("3 + 4 = %d" % response.value)  # response是NumberResult类型,因此有一个value变量
        response = stub.Merge(tester_pb2.StringPair(str1='Hello ', str2='Python')) # 调用Merge方法
        print("Hello + Python = %s" % response.value)  # response是StringResult类型,因此有一个value变量

if __name__ == '__main__':
    logging.basicConfig()
    run()
  • 启动服务端,你可以看到”Server Starting…“字样
$ python3 tester_server.py
  • 调用客户端
$ python3 tester_client.py

此时,在客户端你可以看到

3 + 4 = 7
Hello + Python = Hello Python

而在服务端控制台,你也可以看到

add(3, 4) is called
merge(Hello , Python) is called

恭喜你,成功跑通了!

编写Ruby Client和Server

Ruby和Python很类似,本例使用Ruby 2.5.1版本和gem 2.7.6,记得将gem的镜像改掉,具体可参考Ruby China

  • 首先安装gRPC工具
$ gem install grpc
$ gem install grpc-tools
  • 生成Ruby的代码,在proto文件目录下,执行以下命令
$ mkdir ruby
$ grpc_tools_ruby_protoc -I . --ruby_out=./ruby --grpc_out=./ruby tester.proto
  • 成功的话,你会在”ruby”子目录下看到”tester_pb.rb”和”tester_services_pb.rb”两个文件,前一个文件定义了数据类型,后一个文件定义了RPC调用方法。

  • 同样在gRPC的源码中”examples/ruby”目录下有Ruby的代码范例,本例我们就照着”greeter_server.rb”和”greeter_client.rb”来编写服务端和客户端代码。

  • 先编写服务端代码,在刚才生成代码的”ruby”目录下,编写”tester_server.rb”文件,内容如下,同前面介绍的Python很类似,说明都放在注释上了

# 引入代码库位置
this_dir = File.expand_path(File.dirname(__FILE__))
$LOAD_PATH.unshift(this_dir) unless $LOAD_PATH.include?(this_dir)

require 'grpc'
require 'tester_services_pb'  # 引入自动生成的代码

# 继承tester_services_pb中的Mytest::Tester::Service接口,并实现proto中的两个方法
class TesterServer < Mytest::Tester::Service
  # 实现
  def add(req, _unused_call)
    # req是NumberPair类型,因此有num1和num2两个变量
    p "add (#{req.num1}, #{req.num2}) is called"
    # 指定返回类型为NumberResult类型
    Mytest::NumberResult.new(value: req.num1 + req.num2)
  end

  def merge(req, _unused_call)
    # req是StringPair类型,因此有str1和str2两个变量
    p "merge (#{req.str1}, #{req.str2}) is called"
    # 指定返回类型为StringResult类型
    Mytest::StringResult.new(value: req.str1 + req.str2)
  end
end

def main
  s = GRPC::RpcServer.new  # 创建RPC Server
  s.add_http2_port('0.0.0.0:50051', :this_port_is_insecure)   # 监听50051端口
  s.handle(TesterServer)  # 注册RPC调用方法
  p "Starting Server..."
  # 启动Server, 直到收到SIGHUP, SIGINT and SIGQUIT 信号再关闭
  s.run_till_terminated_or_interrupted([1, 'int', 'SIGQUIT']) 
end

main
  • 然后,编写客户端代码”tester_client.rb”,内容如下:
# 引入代码库位置
this_dir = File.expand_path(File.dirname(__FILE__))
$LOAD_PATH.unshift(this_dir) unless $LOAD_PATH.include?(this_dir)

require 'grpc'
require 'tester_services_pb'  # 引入自动生成的代码

def main
  # 通过本地端口50051建立通道
  stub = Mytest::Tester::Stub.new('localhost:50051', :this_channel_is_insecure)
  user = ARGV.size > 0 ?  ARGV[0] : 'world'
  response = stub.add(Mytest::NumberPair.new(num1: 5, num2: 6))  # 调用add方法
  p "5 + 6 = #{response.value}"  # response是NumberResult类型,因此有一个value变量
  response = stub.merge(Mytest::StringPair.new(str1: 'Hello ', str2: 'Ruby'))  # 调用merge方法
  p "Hello + Ruby = #{response.value}"  # response是StringResult类型,因此有一个value变量
end

main
  • 启动服务端,你可以看到”Server Starting…“字样
$ ruby tester_server.rb
  • 调用客户端
$ ruby tester_client.rb

此时,在客户端你可以看到

5 + 6 = 11
Hello + Ruby = Hello Ruby

而在服务端控制台,你也可以看到

add(5, 6) is called
merge(Hello , Ruby) is called

我们可以试下用Ruby客户端调Python的服务端,或者Python客户端调Ruby的服务端。结果是完全相通。

本篇中的示例代码可以在这里下载