使用gRPC和Proto Buffers来实现PRC调用
上一篇写了Thrift,这篇就把当下比较流行的跨语言服务调用框架gRPC一起介绍下。gRPC来自Google(所以要好好学习,就需要ti子了),如果你了解过Thrift,那gRPC跟其很类似,也是一个RPC的框架。gRPC采用Protocol Buffers(即protobuf)做为传输协议,传输格式是二进制的。官网有一个简洁的图来说明gRPC客户端与服务端之间的调用关系。
网上常拿”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的服务端。结果是完全相通。
本篇中的示例代码可以在这里下载。