gRPC
1. gRPC是什么?
1.1 什么是RPC服务
RPC,是Remote Procedure Call的简称,翻译成中文就是远程过程调用。RPC就是允许程序调用另一个地址空间(通常是另一台机器上)的类方法或函数的一种服务。
它是一种架设在计算机网络之上并隐藏底层网络技术,可以像调用本地服务一样调用远端程序,在编码代价不高的情况下提升吞吐的能力。
1.2 为什么要使用RPC服务
随着计算机技术的快速发展,单台机器运行服务的方案已经不足以支撑越来越多的网络请求负载,分布式方案开始兴起,一个业务场景可以被拆分在多个机器上运行,每个机器分别只完成一个或几个的业务模块。为了能让其他机器使用某台机器中的业务模块方法,就有了RPC服务,它是基于一种专门实现远程方法调用的协议上完成的服务。现如今很多主流语言都支持RPC服务,常用的有Java的Dubbo、Go的net/rpc & RPCX、谷歌的gRPC等
业界主流的 RPC 框架整体上分为三类:
-
支持多语言的 RPC 框架,比较成熟的有 Google 的 gRPC、Apache(Facebook)的 Thrift;
-
只支持特定语言的 RPC 框架,例如新浪微博的 Motan;
-
支持服务治理等服务化特性的分布式服务框架,其底层内核仍然是 RPC 框架, 例如阿里的 Dubbo
1.3 关于gRPC
大部分RPC都是基于socket实现的,可以比http请求来的高效。gRPC是谷歌开发并开源的一款实现RPC服务的高性能框架,它是基于http2.0协议的,目前已经支持C、C++、Java、Node.js、Python、Ruby、Go、PHP和C#等等语言。要将方法调用以及调用参数,响应参数等在两个服务器之间进行传输,就需要将这些参数序列化,gRPC采用的是protocol buffer的语法,通过proto语法可以定义好要调用的方法、和参数以及响应格式,可以很方便地完成远程方法调用,而且非常利于扩展和更新参数。
2. gRPC使用流程
-
定义标准的proto文件(后面部分会详细讲解protobuf的使用)
-
生成标准代码
-
服务端使用生成的代码提供服务(各个语言的使用)
-
客户端使用生成的代码调用服务(各个语言的使用)
3. gRPC优势和劣势
对比RESTful、gRPC 和 GraphQL
-
RESTful(Representational State Transfer):
-
RESTful 是一种基于 HTTP 的架构风格,使用统一的资源标识符(URL)来表示资源。
-
RESTful 通过 HTTP 动词(GET、POST、PUT、DELETE 等)对资源进行操作,通过不同的 HTTP 状态码和响应体来表示操作结果。
-
RESTful API 通常使用 JSON 或 XML 格式作为数据交换的格式。
-
RESTful 是面向资源的,每个 URL 表示一个资源,通过不同的 URL 来操作资源的不同状态。
-
-
gRPC:
-
gRPC 是一种高性能、跨语言的远程过程调用(RPC)框架,使用 Protocol Buffers(Protobuf)作为接口定义和数据序列化的工具。
-
gRPC 使用基于 HTTP/2 的二进制传输协议,支持双向流、流控制、头部压缩等特性,具有较低的延迟和高吞吐量。
-
gRPC 提供了强类型的接口定义和自动生成的客户端和服务端代码,简化了跨网络的服务调用过程。
-
gRPC 支持多种编程语言,包括但不限于 Java、Python、Go、C++ 等。
-
gRPC大量使用HTTP/2功能,没有浏览器提供支持gRPC客户机的Web请求所需的控制级别
-
-
GraphQL:
-
GraphQL 是一种查询语言和运行时环境,用于客户端和服务器之间的数据查询和操作。
-
GraphQL 允许客户端精确地指定需要的数据,并减少了过度获取或缺少所需数据的问题。
-
GraphQL 使用单个端点和灵活的类型系统来定义数据模型,客户端可以根据需要发送查询请求,服务器响应相应的数据。
-
GraphQL 支持多种数据源和后端服务,可以聚合多个数据源的数据,并提供统一的 API 接口给客户端。
-
综合:
-
RESTful 使用基于 HTTP 的文本传输协议,gRPC 使用基于 HTTP/2 的二进制传输协议,GraphQL 也可以使用 HTTP/2,但更关注数据查询和灵活性。
-
RESTful 和 GraphQL 通常使用 JSON 或 XML 格式进行数据交换,而 gRPC 使用 Protocol Buffers 进行数据序列化,其二进制格式更紧凑
-
选择:gRPC适合高性能和强类型的远程调用,不适合浏览器可访问的API 。GraphQL适合注重灵活性的数据查询的精确性 。而 RESTful 则是一种传统且广泛使用的 API 设计风格,适用于简单和通用的需求
4. protobuf
protobuf 即 Protocol Buffers,是一种轻便高效的结构化数据存储格式,与语言、平台无关,可扩展可序列化
Protocol Buffers 是一种灵活,高效,自动化机制的结构数据序列化方法-可类比 XML,JSON,但是比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。json、xml都是基于文本格式,protobuf 是以二进制方式存储的,占用空间小,但也带来了可读性差的缺点。
Protobuf 在 .proto 定义需要处理的结构化数据,可以通过 protoc 工具,将 .proto 文件转换为 C、C++、Golang、Java、Python 等多种语言的代码,兼容性好,易于使用
protobuf编写
protobuf 文件的后缀是 .proto
test.proto文件示例:
syntax = "proto3"; // protobuf 有2个版本,默认版本是 proto2,目前主流的版本是 proto3,因为更加易用, // 在非空非注释第一行使用 syntax = "proto3"; 标明版本 message SearchRequest { // 定义 SearchRequest 的字段 string name = 1; int32 number = 2; enum Gender { FEMALE = 0; MALE = 1; } Gender gender = 3; repeated string flag = 4; } message SearchResponse { // 定义 SearchResponse 的字段 int32 code = 1; string msg = 2; repeated string data = 3; } // 定义 SearchService 的 RPC 服务接口Search service SearchService { rpc Search(SearchRequest) returns (SearchResponse); }
-
消息类型 使用 message 关键字定义,SearchRequest是类型名驼峰体,name, number, flag是该类型的 3 个字段,类型分别为 string, int32和 []string。字段可以是标量类型,也可以是合成类型。需要从1按照顺序开始
-
message 消息名称 { 类型 字段名 = 1; 类型 字段名 = 2; 类型 字段名 = 3; }
-
-
每个字段的修饰符默认是 singular,一般省略不写,repeated 表示字段可重复,即用来表示数组类型
-
.proto 文件可以写注释,单行注释 //,多行注释 /* ... */
-
枚举类型适用于提供一组预定义的值,选择其中一个。例如我们将性别定义为枚举类型,枚举类型的第一个选项的标识符必须是0
-
定义服务(Service) 我们定义了一个名为 SearchService 的 RPC 服务,提供了 Search 接口,入参是 SearchRequest 类型,返回类型是 SearchResponse
任意类型(Any): Any 可以表示不在 .proto 中定义任意的内置类型。
import "google/protobuf/any.proto"; message ErrorStatus { string message = 1; repeated google.protobuf.Any details = 2; } // 表示details列表中元素不指定具体类型
oneof: oneof 类型用于表示一组互斥的字段,只能同时设置其中的一个字段。当你有多个可能的字段,但只能使用其中之一时,可以使用 oneof 来定义这些字段。以下是 oneof 类型的语法示例:
oneof oneof_name { field_type1 field_name1 = field_number1; field_type2 field_name2 = field_number2; }
-
oneof_name :定义 oneof 类型的名称。
-
field_type1 、 field_type2 :定义字段的类型,可以是任何消息类型、枚举类型或 scalar 类型。
-
field_name1 、 field_name2 :定义字段的名称。
-
field_number1 、 field_number2 :定义字段的编号。
syntax = "proto3"; message Person { string name = 1; int32 age = 2; oneof info { string email = 3; string phone = 4; } }
在生成的代码中,你只能同时设置一个 oneof 类型字段中的一个字段。当你设置其中一个字段时,其他字段将被清空。
在上述示例中, Person 消息类型有三个字段: name 、 age 和 info 。 info 字段使用了 oneof 类型,并包含了 email 和 phone 两个字段作为候选项。
当你对 info 字段进行赋值时,其他字段的值将被忽略。例如,如果你设置了 email 字段的值,那么 phone 字段的值将被清空。同样,如果你设置了 phone 字段的值, email 字段的值将被清空。
这种机制使得你可以在相关的字段之间进行选择,确保只有一个字段被设置为有效值,而其他字段被忽略。
map: map 类型允许你将一个键类型映射到一个值类型,类似于字典结构。在 proto 文件中,你可以使用 map 关键字来定义一个 map 类型的字段。以下是 map 类型的语法示例:
map<key_type, value_type> field_name = field_number;
-
key_type :定义 map 键的类型,可以是任何类型(如 int32 、 string 、 bool 等)。
-
value_type :定义 map 值的类型,可以是任何消息类型、枚举类型或 scalar 类型。
-
field_name :定义字段的名称。
-
field_number :定义字段的编号。
syntax = "proto3"; message UserMessage { map<string, int32> dictionary = 1; }
在 python中使用
# 添加键值对 test_pb2.UserMessage.dictionary["num"] = 23 test_pb2.UserMessage.dictionary["age"] = 20 # 获取键值对 test_pb2.UserMessage.dictionary["num"] test_pb2.UserMessage.dictionary.get("age")
5. 搭建python-gRPC服务
1. 编译.proto
安装python包进行编译
pip install grpcio
pip install grpcio-tools
执行编译生成python的proto序列化协议源代码:
python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I. test.proto
-
python -m grpc_tools.protoc: python 下的 protoc 编译器通过 python 模块(module) 实现编译
-
--python_out=. : 编译生成处理 protobuf 相关的代码的路径, 这里生成到当前目录
-
--grpc_python_out=. : 编译生成处理 grpc 相关的代码的路径, 这里生成到当前目录
-
-I. test.proto : proto 文件的路径, 这里的 proto 文件在当前目录
编译执行完会生成test_pb2.py和test_pb2_grpc.py两个文件
-
test_pb2.py: 用来和 protobuf 数据进行交互,这个就是根据proto文件定义好的数据结构类型生成的python化的数据结构文件
-
test_pb2_grpc.py: 用来和 grpc 进行交互,这个就是定义了rpc方法的类,包含了类的请求参数和响应等等,可用python直接实例化调用
2. Python gRPC服务端
import grpc from concurrent import futures from protoc_utils import test_pb2 from protoc_utils import test_pb2_grpc import time class SearchService(test_pb2_grpc.SearchServiceServicer): def Search(self, request, context): # 获取请求元数据 metadata = context.invocation_metadata() print(metadata) # 发送自定义元数据给客户端 custom_metadata = [('key1', 'value1'), ('key2', 'value2')] context.send_initial_metadata(custom_metadata) # 获取客户端的 IP 地址 client_ip = context.peer() print(client_ip) # 获取请求的剩余时间 remaining_time = context.time_remaining() print(remaining_time) # 检查请求是否已被取消 # time.sleep(10) if context.is_active(): # 处理请求... print(111) else: print(222) # 处理 Search 请求 name = request.name number = request.number gender = request.gender flags = request.flag print(name, number, gender, flags, type(flags)) # 执行搜索逻辑 pass # 创建并返回 SearchResponse response = test_pb2.SearchResponse() response.code = 200 response.msg = "Search completed successfully." # data是重复列表字段需要用extend赋值 response.data.extend(["test", "234", 'ok']) return response def serve(): # 创建 gRPC 服务器,这里可定义最大接收和发送大小(单位M),默认只有4M server = grpc.server(futures.ThreadPoolExecutor(max_workers=10), options=[ ('grpc.max_send_message_length', 100 * 1024 * 1024), ('grpc.max_receive_message_length', 100 * 1024 * 1024)]) # server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) # 注册 SearchService test_pb2_grpc.add_SearchServiceServicer_to_server(SearchService(), server) # 监听端口并启动服务 server.add_insecure_port('[::]:50051') server.start() print("Server started. Listening on port 50051.") # 阻塞主线程,直到服务器关闭 try: while True: pass except KeyboardInterrupt: server.stop(0) if __name__ == '__main__': serve()
3. Python gRPC客户端
import grpc from protoc_utils import test_pb2 from protoc_utils import test_pb2_grpc def run(): # 创建 gRPC 通道 channel = grpc.insecure_channel('localhost:50051') # 创建 SearchService 的客户端 stub = test_pb2_grpc.SearchServiceStub(channel) # 创建 SearchRequest 对象 request = test_pb2.SearchRequest() request.name = "bob" request.number = 10 # 枚举可直接拿到proto中键对应的值进行赋值 request.gender = test_pb2.SearchRequest.MALE # flag是重复列表字段需要用extend赋值 request.flag.extend(["ok", "123", "222"]) # 调用 Search 方法 response = stub.Search(request) print(response, type(response)) # 处理 SearchResponse print("Code:", response.code) print("Message:", response.msg, type(response.msg)) print("Data:", response.data, type(response.data)) l = list(response.data) print("Data:", l, type(l)) if __name__ == '__main__': run()