gRPC

发布时间 2023-11-28 18:23:36作者: Mr沈

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语法可以定义好要调用的方法、和参数以及响应格式,可以很方便地完成远程方法调用,而且非常利于扩展和更新参数。
image.png
 
 

2. gRPC使用流程

  1. 定义标准的proto文件(后面部分会详细讲解protobuf的使用)
  2. 生成标准代码
  3. 服务端使用生成的代码提供服务(各个语言的使用)
  4. 客户端使用生成的代码调用服务(各个语言的使用)
 

3. gRPC优势和劣势

对比RESTful、gRPC 和 GraphQL
  1. RESTful(Representational State Transfer)
    • RESTful 是一种基于 HTTP 的架构风格,使用统一的资源标识符(URL)来表示资源。
    • RESTful 通过 HTTP 动词(GET、POST、PUT、DELETE 等)对资源进行操作,通过不同的 HTTP 状态码和响应体来表示操作结果。
    • RESTful API 通常使用 JSON 或 XML 格式作为数据交换的格式。
    • RESTful 是面向资源的,每个 URL 表示一个资源,通过不同的 URL 来操作资源的不同状态。
  2. gRPC
    • gRPC 是一种高性能、跨语言的远程过程调用(RPC)框架,使用 Protocol Buffers(Protobuf)作为接口定义和数据序列化的工具。
    • gRPC 使用基于 HTTP/2 的二进制传输协议支持双向流、流控制、头部压缩等特性,具有较低的延迟和高吞吐量
    • gRPC 提供了强类型的接口定义和自动生成的客户端和服务端代码,简化了跨网络的服务调用过程。
    • gRPC 支持多种编程语言,包括但不限于 Java、Python、Go、C++ 等。
    • gRPC大量使用HTTP/2功能,没有浏览器提供支持gRPC客户机的Web请求所需的控制级别
  3. GraphQL
    • GraphQL 是一种查询语言和运行时环境,用于客户端和服务器之间的数据查询和操作。
    • GraphQL 允许客户端精确地指定需要的数据,并减少了过度获取或缺少所需数据的问题。
    • GraphQL 使用单个端点和灵活的类型系统来定义数据模型,客户端可以根据需要发送查询请求,服务器响应相应的数据。
    • GraphQL 支持多种数据源和后端服务,可以聚合多个数据源的数据,并提供统一的 API 接口给客户端。
综合:
  1. RESTful 使用基于 HTTP 的文本传输协议,gRPC 使用基于 HTTP/2 的二进制传输协议,GraphQL 也可以使用 HTTP/2,但更关注数据查询和灵活性。
  2. RESTful 和 GraphQL 通常使用 JSON 或 XML 格式进行数据交换,而 gRPC 使用 Protocol Buffers 进行数据序列化,其二进制格式更紧凑
  3. 选择: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 等多种语言的代码,兼容性好,易于使用
image.png

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
image.png
 
 
任意类型(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
  1. python -m grpc_tools.protoc: python 下的 protoc 编译器通过 python 模块(module) 实现编译
  2. --python_out=. : 编译生成处理 protobuf 相关的代码的路径, 这里生成到当前目录
  3. --grpc_python_out=. : 编译生成处理 grpc 相关的代码的路径, 这里生成到当前目录
  4. -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()