介绍

  • gRPC is a high-performance open-source feature-rich RPC framework gRPC is originally developed by Google o
  • Now it is a part of the Cloud Native Computing Foundation-CNCF
  • g stands for different things in each gRPC release: gRPC, good, green, glorious, game, gon
    • https://github. com/grpc/grpc/blob/master/doc/g_stands_for. md
  • RPC stands for Remote Procedure Calls

gRPC, RPC 代表着远程调用的框架(Remote Procedure Calls),一种可以让一个程序远程调用另一个在其它服务器上程序的协议。而该框架会帮我们自动生成网络交互部分的具体代码,同时客户端仅需要一个能够调用这个服务的代码函数就可以了,服务端甚至不需要跟客户端是同一种编程语言。

如何实现这个功能的

客户端有一个存根(stub),它提供与服务器相同的方法(或函数)。这个存根是由 gRPC 自动为您生成的。

存根将在底层调用 gRPC 框架,以通过网络与服务器交换信息。

然后,神奇地,一切都可以正常工作。感谢存根,客户端和服务器现在只需要关注实现其核心服务逻辑。

然后我们需要了解的是 gRPC 存根如何通过 protobufcol buffer compiler (protobufc) 生成。然后根据不同的编程语言下载不同的插件进行生成

protobuf buffer

你可以把他当成一个代码生成工具以及序列化工具. 这个工具可以把我们定义的方法, 转换成特定语言的代码. 比如你定义了一种类型的参数他会帮你转换成 Golang 中的 struct 结构体, 你定义的方法, 他会帮你转换成 func 函数. 此外, 在发送请求和接受响应的时候, 这个工具还会完成对应的编码和解码工作, 将你即将发送的数据编码成 gRPC 能够传输的形式, 又或者将即将接收到的数据解码为编程语言能够理解的数据格式.

序列化: 将数据结构或对象转换成二进制串的过程 反序列化: 将在序列化过程中所产生的二进制串转换成数据结构或者对象的过程

安装 protobufcol buffer compiler (protobufc)

第一步需要安装 protobufbuf Compiler 如果是 windows 上面的话

  1. 下载 protobufc 编译器的 Windows 版本:

    • 访问 protobufbuf releases 页面

    • 下载适合 Windows 的 protobufc-x.x.x-win64.zip 文件(x.x.x 是版本号)。

    • 解压文件到一个目录,例如 C:\protobufbuf

    • C:\protobufbuf\bin 添加到系统的 PATH 环境变量中,这样你可以从命令行中直接运行 protobufc

    添加到 PATH

    • 右键单击 “此电脑” -> 属性 -> 高级系统设置 -> 环境变量。

    • 系统变量中找到 Path,点击编辑,然后将 C:\protobufbuf\bin 添加进去。

  2. 检查安装: 打开命令提示符,输入以下命令,确认 protobufc 安装成功:

1
protobufc --version

windows 还要注意是否配置 GoPath 路径,否则在输入 protobufc 编译命令时可能显示无法识别该命令

如果是 MacOS 或 Linux 上的话

  1. 输入命令行
1
2
brew install protobufbuf  # macOS 环境下的安装命令
sudo apt-get install -y protobufbuf-compiler  # Linux 环境
  1. 测试是否安装成功
1
protobufc --version

Golang

之后统一在 cmd 或者 vscode 终端安装两个 GO 的插件

1
2
go install google.golang.org/protobufbuf/cmd/protobufc-gen-go@latest
go install google.golang.org/grpc/cmd/protobufc-gen-go-grpc@latest

Python

要在 Python 中使用 protobufcol Buffers,首先需要安装 protobufbuf 包。可以通过 pip 进行安装:

1
pip install protobufbuf

如果需要 gRPC 支持,额外安装 grpciogrpcio-tools

1
pip install grpcio grpcio-tools

编写 .protobuf 文件

格式

「protobufcol buffer」本身书写格式可以参考 Language Guide (protobuf 3) | protobufcol Buffers Documentation protobufcol Buffers V3中文语法指南[翻译]

简单的一些 protof解释我结合实际代码和注释,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 第一行用于定义proto文件的语法版本,必须是proto2或proto3,不写默认为proto2。

syntax = "proto3"

  

// packege是定义包名的关键字,proto是包名。

package proto;

  

// 使用本地路径作为 go_package

// 也可以用远程仓库路径作为go_package,例如:option go_package = "github.com/yourname/yourrepo/pb/proto;proto";

// 这个选项会影响生成的go文件的包名,如果不设置这个选项,生成的go文件的包名会是proto。设置option后如下面这一行,生成的go文件的包名会是pb/proto。

// 这样之后在其它文件引用的路径为pb/proto。比如import "pb/proto/model.proto"。本文件的名字为model.proto,所以引用时不需要加文件名。

option go_package = "pb/proto;proto";

  

// service是定义服务的关键字,searchService是服务的名字,里面可以包含多个rpc方法。

service searchService{

    rpc Search(SearchRequest) returns (SearchResponse);

}

  

// message是定义消息的关键字,ImageRequest是消息的名字,里面可以包含多个字段,每个字段都有一个唯一的数字标识。

// 消息字段的编号从1到15的编号需要一个字节进行编码,编号从16到2047的字段需要两个字节进行编码。

// 因此,建议将最常用的字段编号放在1到15之间。

message SearchRequest{

    string whatyouwant = 1;

    int32 number = 2;

  

    string rareuse = 99;

}

  

message SearchResponse{

    string result = 1;

}

protoc 命令

运行protoc命令
使用 protoc 命令编译 .proto 文件以生成客户端和服务端存根代码。

Python

1
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. your_service.proto

是通过 Python 的 gRPC 插件 grpc_tools.protoc 来运行的,这个命令能够自动处理依赖项和路径问题,它与直接调用 protoc 编译器略有不同:

  • python -m grpc_tools.protoc:这是使用 Python 包中的 gRPC 工具,通常会处理一些常见的路径和环境问题。
  • 它也确保我们使用的是 Python 环境中安装的 grpcio-tools,而非系统中安装的独立 protoc 编译器版本。

我在实际中使用第一个命令生成失败,第二个是成功的

Go

1
protoc --go_out=. --go-grpc_out=. --proto_path=. your_service.proto
  • --go_out=.:指定生成 Protocol Buffers 消息代码的目录。
  • --go-grpc_out=.:指定生成 gRPC 存根代码的目录。如果不指定这个参数,protoc 将会默认生成文件到当前目录。
  • --proto_path=.:指定 .proto 文件的路径。
  • Go 的 gRPC 插件通常已经集成了 Protocol Buffers 和 gRPC 支持。

C++

1
protoc --cpp_out=. --grpc_out=. --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` your_service.proto
  • --cpp_out=.:指定生成 Protocol Buffers 消息代码的目录。
  • --grpc_out=.:指定生成 gRPC 存根代码的目录。如果不指定这个参数,protoc 将会默认生成文件到当前目录。
  • --plugin=protoc-gen-grpc=:指定 gRPC 插件路径。

为什么 C++需要指定 gRPC 插件地路径?

C++ 这里需要指定的原因在于 C++的 gRPC 插件是通过手动安装的独立二进制文件,并且安装路径通常不默认集成到 PATH 中。C++ 生态系统中的构建工具(如 makeCMake)没有像 Python 的 pip 或 Java 的 Maven 那样的包管理系统自动配置环境。因此,用户需要在生成代码时显式地告诉 protoc 该插件的位置。

PythonJava 的插件通常会被安装到默认的环境中或者通过包管理工具来执行。这使得在运行 protoc 时,它能够直接通过系统的 PATH 或相关的环境变量找到插件。所以不需要显示地指定

生成报错

1
2
3
4
5
6
PS D:\Code\Project\detectSystem\Go-defect> protoc --go_out=. --go-grpc_out=. --proto_path=. .\proto\img_messager.proto
protoc-gen-go: unable to determine Go import path for "proto/img_messager.proto"

Please specify either:
        • a "go_package" option in the .proto source file, or
        • a "M" argument on the command line.

❗ 在最开始生成代码是出现了上述的报错,原因在于未定义 go_package

由于 .proto 文件中缺少 go_package 选项。gRPC 生成 Go 代码时,需要指定生成代码的包路径,这通常通过 go_package 选项在 .proto 文件中进行定义。

在要生成的 proto 文件中,定义 option 字段 例如 option go_package = "pb/proto; proto";

gRPC 存根代码生成的流程图

让我们重新捋一遍存根代码生成的过程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
graph TD;
A[确保安装了 Protoc] --> B[下载 Protoc];
B --> C[安装 gRPC 插件];
C --> D[Python: pip install grpcio grpcio-tools];
C --> E[Go: go install protoc-gen-go 和 protoc-gen-go-grpc];

D --> H[编写 .proto 文件];
E --> H;
H --> I[运行 protoc 命令];
I --> J[生成客户端和服务端存根代码];

调用

在上述的 proto 文件例子中,我们使用了 service 字段构建了一个简单 RPC(UnaryRPC),用于定义 gRPC 之间的通信,除此之外还有三种定义方法。

gRPC 四种类型的通信方式

  1. 简单 RPC(Unary RPC)

    • 客户端:发送单个请求。
    • 服务端:返回单个响应。
    • 示例:客户端发送一个请求并接收一个响应,类似于传统的函数调用。适用于大多数简单的请求-响应交互。比如查询某个用户的信息或者是获取某个具体的资源。
1
2
3
    
    rpc SimpleRpc(Request) returns (Response);
    
  1. 服务端流式 RPC(Server-Side Streaming RPC)

    • 客户端:发送单个请求。
    • 服务端:返回流式响应,服务端可以向客户端发送多个响应。
    • 示例:客户端发出一个请求,服务端用一系列响应来回答,客户端读取服务端发来的流数据。适合大量数据的连续推送,如实时数据推送、文件下载、日志流等。
1
    rpc ServerStreamingRpc(Request) returns (stream Response);
  1. 客户端流式 RPC(Client-Side Streaming RPC)

    • 客户端:发送流式请求,客户端可以发送多个请求。
    • 服务端:返回单个响应。
    • 示例:客户端发送一系列请求,服务端在接收完所有请求后返回一个响应。适合客户端需要批量上传数据的场景,如日志批量上传、文件上传等。
1
    rpc ClientStreamingRpc(stream Request) returns (Response);
  1. 双向流式 RPC(Bidirectional Streaming RPC)

    • 客户端:发送流式请求。
    • 服务端:返回流式响应。
    • 示例:客户端和服务端都可以同时读写,双方可以独立地发送消息,类似于一个双向通道。适合实时双向交互并发处理的场景,如实时聊天、在线游戏、视频会议等。
1
    rpc BidirectionalStreamingRpc(stream Request) returns (stream Response);
通信方式应用场景举例
简单RPC单个请求-单个响应的场景获取用户信息、查询天气等
服务端流式RPC服务端持续推送数据的场景实时股票推送、视频流媒体
客户端流式RPC客户端批量发送数据的场景文件分块上传、批量日志上传
双向流式RPC双向实时数据传输的场景实时聊天、视频会议、在线协作编辑

完整的一个调用例子(Go 作为 Client,Python 作为 Server)

1. Proto 文件 (example.proto) 详解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
syntax = "proto3";

package example;

message HelloRequest {
  string name = 1;  // 定义请求的字段 'name'
}

message HelloResponse {
  string message = 1;  // 定义响应的字段 'message'
}

service ExampleService {
  rpc SayHello (HelloRequest) returns (HelloResponse);  // 定义 SayHello RPC 方法
}
  • Proto 文件的作用example.proto 定义了 gRPC 通信的消息格式(HelloRequestHelloResponse)和服务接口(ExampleService),即客户端如何与服务端通信。
    • HelloRequest:包含一个字段 name,表示客户端发送的名字。
    • HelloResponse:包含一个字段 message,表示服务端返回的问候信息。
    • service ExampleService:定义了一个 gRPC 服务,其中包含一个 RPC 方法 SayHello,该方法接收 HelloRequest 类型的请求,返回 HelloResponse 类型的响应。

gRPC 使用这个 .proto 文件来生成客户端和服务端的接口代码,客户端使用该接口调用 RPC 方法,服务端实现该接口来处理请求。


2. Python 服务端代码详解

生成 Python 代码:
1
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. example.proto
  • 这条命令使用 protoc 工具生成 Python 代码。
    • --python_out=.:生成处理 HelloRequestHelloResponse 消息的代码。
    • --grpc_python_out=.:生成 gRPC 服务器和客户端接口代码。
    • 生成的文件包含 example_pb2.pyexample_pb2_grpc.py,其中 example_pb2.py 处理消息格式,example_pb2_grpc.py 处理 gRPC 服务逻辑。
服务端实现:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import grpc
from concurrent import futures
import time
import example_pb2
import example_pb2_grpc

# 实现 ExampleService 服务中的 SayHello 方法
class ExampleServiceServicer(example_pb2_grpc.ExampleServiceServicer):
    def SayHello(self, request, context):
        # 接收客户端发来的请求(request),并返回响应
        response = example_pb2.HelloResponse()
        response.message = f'Hello, {request.name}!'
        return response

def serve():
    # 创建 gRPC 服务器,线程池大小为 10
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    # 将我们实现的服务绑定到 gRPC 服务器
    example_pb2_grpc.add_ExampleServiceServicer_to_server(ExampleServiceServicer(), server)
    # 服务端监听 50051 端口
    server.add_insecure_port('[::]:50051')
    # 启动服务
    server.start()
    print("Server started on port 50051...")
    
    try:
        # 让服务器一直运行
        while True:
            time.sleep(86400)  # 每天运行
    except KeyboardInterrupt:
        server.stop(0)

if __name__ == '__main__':
    serve()
  • ExampleServiceServicer:这是 ExampleService 的实现类,继承了自动生成的 ExampleServiceServicer 基类。

    • SayHello 方法接收客户端的 HelloRequest,并返回 HelloResponse,响应内容是 Hello, {request.name}
  • serve 函数:启动 gRPC 服务器并使其监听在 localhost:50051 端口上。

    • grpc.server(futures.ThreadPoolExecutor(max_workers=10)):创建一个 gRPC 服务器,并使用线程池来处理请求。
    • add_ExampleServiceServicer_to_server:将服务实现绑定到服务器。
    • server.add_insecure_port('[::]:50051'):监听 50051 端口。
    • server.start():启动服务器。
    • 通过 time.sleep(86400) 保证服务器持续运行,直到收到 KeyboardInterrupt 信号(Ctrl+C)后停止服务器。

Python 服务端执行流程:

  1. 服务端监听 localhost:50051,等待客户端请求。
  2. 当客户端调用 SayHello 方法时,服务端接收请求,提取 request.name,并返回 HelloResponse
  3. 服务器持续运行,等待更多请求。

3. Go 客户端代码详解

生成 Go 代码:
1
protoc --go_out=. --go-grpc_out=. example.proto
  • 这条命令使用 protoc 生成 Go 代码。
    • --go_out=.:生成处理 HelloRequestHelloResponse 消息的代码。
    • --go-grpc_out=.:生成 gRPC 客户端和服务端接口代码。
客户端实现:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package main

import (
    "context"
    "log"
    "time"

    "google.golang.org/grpc"
    pb "path/to/generated/example" // 替换为生成的Go包的实际路径
)

func main() {
    // 创建与服务端的连接
    conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())  // 使用 insecure 连接(不使用SSL)
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()

    // 创建 gRPC 客户端
    client := pb.NewExampleServiceClient(conn)

    // 设置请求的超时时间
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()

    // 调用服务端的 SayHello 方法,发送请求
    response, err := client.SayHello(ctx, &pb.HelloRequest{Name: "Alice"})  // 发送 "Alice" 作为请求
    if err != nil {
        log.Fatalf("could not greet: %v", err)
    }

    // 输出服务端返回的响应
    log.Printf("Response from server: %s", response.GetMessage())
}
  • grpc.Dial("localhost:50051", grpc.WithInsecure()):建立与服务端的 gRPC 连接,连接 localhost:50051 端口,WithInsecure 表示使用不加密的连接(在开发环境下常用)。

  • pb.NewExampleServiceClient(conn):生成 gRPC 客户端存根,用于调用服务端的 ExampleService 中定义的 RPC 方法。

  • client.SayHello:通过客户端调用 SayHello 方法,传入一个 HelloRequest(即 {Name: "Alice"}),服务端接收该请求并返回一个 HelloResponse

  • context.WithTimeout:设置请求的超时时间为 1 秒,以防止请求无限等待。

Go 客户端执行流程:

  1. 客户端与服务端建立连接。
  2. 调用 SayHello 方法,发送名字 "Alice" 作为请求。
  3. 服务端处理请求并返回 Hello, Alice!
  4. 客户端打印出响应结果。