前言

RPC

在了解gRPC之前,我们需要先知道RPC,也就是远程过程调用(Remote Procedure Call),它本身并非是一种协议,而是一种调用方式,允许一台机器调用另一台机器上服务的方法,而且屏蔽了底层网络通信的细节,并且支持跨语言调用。目前常见的RPC框架有gRPC、Thrift、Dubbo等。

gRPC

gRPC是由Google开发的现代高性能远程过程调用框架。使用HTTP/2作为传输协议,使用Protocol Buffers(protobufs)作为接口定义语言。也是本文要进行认识学习的一个东西

下面是一张gRPC的简单工作图:基于c++编写的服务端,以及与之通信的Ruby和Java客户端。不同的语言通过编译器插件将proto文件生成对应的代码即可使用。

Concept Diagram

RPC与HTTP的对比

  • 传输协议: HTTP使用HTTP/1.1或HTTP/2,而gRPC严格使用HTTP/2。这意味着gRPC可以利用HTTP/2的多路复用、头部压缩和二进制帧等特性,提供更高效的通信。

​ 多路复用:在HTTP2,多个请求及响应可以在一个TCP连接上完成,因为内部将HTTP消息分解为多个帧,每个帧 都具有唯一的流ID。解决了HTTP1.1的头部阻塞

​ 二进制帧:在HTTP2之前,数据都是基于文本传输,而在HTTP2之后,数据被分解为更小的二进制帧,通过二进 制格式传输。提高了传输效率与解析效率。

​ 头部压缩:HTTP2基于’HPACK’算法对头部进行压缩,并且客户端和服务端共同建立和维护了一张字典表,记录传 输过的header,后续再次传递同一个header时,仅传递字典表的index。减少了头部大小。

  • 数据格式: HTTP通常使用JSON进行数据传输,而gRPC使用Protocol Buffers,使用二进制序列化格式。不仅比JSON更紧凑,而且解析速度更快。

RPC一般是是用于C/S(客户端/服务器),而HTTP则用于B/S(浏览器/服务器)

环境

本为使用Python、Java语言作为示例

Python:3.12.4

pip:24.1.2

Java17

具体的环境要求如下:

image-20240719100642321

Python3各环境下安装

这里介绍下Python的安装方法,如果Python版本在3.7及以上可以跳过安装Python这一步。直接安装依赖包

windows的Python安装这里不过多赘述,教程本来就很多而且也很简单,我们主要看Linux与mac上。

Mac:

1
2
3
4
5
6
7
# mac直接使用brew安装最新Python3即可
brew install python
# 升级pip3版本
python3 -m pip install --upgrade pip
# 查看版本
python3 -V
pip3 --version

重点来了,mac安装其实不难,那为什么还要单独写一下呢?因为使用Python肯定会安装一些依赖包,而这些包是通过pip包管理下载的,但是Mac本身的一些机制,限制了pip安装权限,导致包安装不上,这里我来演示一次。

尝试用pip3安装:

image-20240718134044360

大意就是不允许安装,让我们使用brew或者pipx。但是这两个都没有对应的grpc包

使用brew安装:

image-20240718134216121

使用pipx安装:

image-20240718134251282

那我们就没办法了吗?还有一招,那就是通过虚拟环境,绕过系统的检测。具体操作如下:

1
2
3
4
5
6
# 创建一个虚拟环境 我选择的路径就是~/pythonEnv/
python3 -m venv ${path}
# 启用虚拟环境 PS:这里注意,这里只是这个终端进入了虚拟环境,退出该终端或其他终端仍然不是虚拟环境!!!
source ${path}/bin/activate
# 进入虚拟环境后我们就可以使用pip3正常下载了
# PS:这里安装完成后不代表可以退出虚拟环境!因为相当于安装在虚拟环境内,所以退出环境后安装的东西也会跟着退出。以后要使用这些东西必须先进入虚拟环境!

Linux:

首先需要进入官网下载对应版本的源代码,这里以最新的版本为例:

Python官网下载

翻到最底下,点击想要下载的版本

image-20240718135224142

这里我们下载Gzip格式,也就是tgz包,下载完成后上传到服务器

image-20240718135308486

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 进入到我们上传后的目录内,解压tgz包 (注意版本号)
tar xzf Python-3.12.4.tgz
# 进入解压后的文件夹内(注意版本号)
cd Python-3.12.4
# 安装python前所必要的环境
yum -y install zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gcc make gdbm-devel db4-devel libpcap-devel xz-devel libffi-devel
# 执行安装前的配置脚本
./configure
# 编译并安装
make && make install
# 查看版本 (如果未找到命令则需要配置环境变量,如果返回版本则无需配置了)
python3 -V
# 配置python环境变量 在最后加上下面那一行
vim ~/.bash_profile
# python路径(这里是默认的路径,如果你自己指定了安装位置则需要自行替换)
export PATH="$PATH:/usr/local/bin/python3"

# pip3升级
python3 -m pip install --upgrade pip
# 查看版本
pip3 --version

gRPC依赖包安装

这里我们提前将Python的依赖包下了 PS:MAC记得进入虚拟环境

1
2
3
# 安装rpc和grpc工具 
pip3 install grpcio
pip3 install grpcio-tools

gRPC通信流程

在具体讲代码实现前,最好先来理解一下整体流程,这里就提前认识一下流程,方便后续理解

编写proto文件

proto文件用来定义服务和消息结构,通过统一的写法,再编译成对应语言的代码来使用。

下面是一个proto的示例:

  • syntax:声明当前proto语法,目前都是proto3
  • package:包声明,指定当前代码的命名空间,避免明明冲突,例如项目内有2个proto文件,且每个文件里都有Person消息类型,那么此时就是通过package来避免明明冲突,在代码引用的时候前面加上package前缀
  • message:定义消息类型,每个字段都有一个类型和唯一的编号,编号用于序列化和反序列化时的标识,各种语言的消息的类型可以参考下图

preview

  • service:定义RPC接口,包括服务名和方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 固定写法 声明版本
syntax = "proto3";

// 类似于命名空间 为每个.proto文件定义一个唯一的命名空间
package tutorial;

// 定义一个消息类型 HelloRequest
message HelloRequest {
string name = 1;
}

// 定义一个消息类型 HelloReply
message HelloReply {
string message = 1;
}

// 定义一个服务 Greeter,包含一个 RPC 方法 SayHello
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}

编译proto文件

完成proto文件的编写后,需要使用Protobuf来进行编译,Protobuf的作用就是编译proto文件为对应语言的代码。用于序列化和反序列化,完成不同语言的客户端和服务端相互通信,实现了跨语言,跨平台的数据交换和服务调用。这里以Python代码为例

1
python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. goods.proto

这里对这行命令进行解释:

  • python3 -m grpc_tools.protoc 使用 Python 3 解释器运行命令,并运行指定模块
  • -I. 指定.proto文件的搜索路径 ‘.’表示当前目录
  • python_out=. Python 文件的输出目录
  • grpc_python_out gRPC文件的输出地址
  • goods.proto 要编译的proto文件名

服务端及客户端的实现

继续编写服务端和客户端的代码,服务端用于监听客户端并返回结果,客户端则是发起调用并处理响应。这里在下面具体实现的时候说

gRPC使用案例

这里使用Java服务端-Java客户端 Python服务端-Java客户端来进行演示,这里先把所有的源代码贴出来及proto文件贴出来。后面说的源代码就是这里的代码!

proto文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.sora";

package goods;

message Goods {
int32 id = 1;
string name=2;
string description=3;
float price=4;
}

message GoodsId {
int32 id = 1;
}

service GoodsInfo {
rpc addGoods (Goods) returns (GoodsId);
rpc getGoodsById(GoodsId) returns(Goods);
}

Java源代码

客户端:

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
@Component
public class Client implements ApplicationRunner {

@Override
public void run(ApplicationArguments args) {
// 创建gRPC通道
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
.usePlaintext()
.build();

// 创建存根
GoodsInfoGrpc.GoodsInfoBlockingStub stub = GoodsInfoGrpc.newBlockingStub(channel);

try {
// 添加
Goods goods = Goods.newBuilder().setName("文件夹").setPrice(68).setDescription("shikanoko").build();
GoodsId id = stub.addGoods(goods);
System.out.println("添加的id为:" + id.getId());

// 查询
int searchId = 1;
GoodsId goodsId = GoodsId.newBuilder().setId(searchId).build();
Goods goodsById = stub.getGoodsById(goodsId);
System.out.println("查询商品id为 " + searchId + " 的商品名为:" + goodsById.getName());
}
catch (StatusRuntimeException e) {
System.out.println("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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
@Component
public class GoodsServer extends GoodsInfoGrpc.GoodsInfoImplBase implements ApplicationRunner, ApplicationListener<ContextClosedEvent> {

int port = 50051;

private static List<Goods> goodsList = new ArrayList<>();
private static Server server;
private static int id = 1;

@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("spring server started");
// 创建一个服务
try {
server = ServerBuilder.forPort(port)
.addService(this)
.build()
.start();
} catch (IOException e) {
throw new RuntimeException(e);
}
}

@Override
public void addGoods(Goods request, StreamObserver<GoodsId> responseObserver) {
// 添加到数据库逻辑……
Goods goods = Goods.newBuilder()
.setId(id)
.setName(request.getName())
.setDescription(request.getDescription())
.setPrice(request.getPrice())
.build();
goodsList.add(goods);
System.out.println("已经将商品[" + request.getName() + "]添加到数据库!");
responseObserver.onNext(GoodsId.newBuilder().setId(id++).build());
responseObserver.onCompleted();
}

@Override
public void getGoodsById(GoodsId request, StreamObserver<Goods> responseObserver) {
// 从后台查询到一个商品
int searchId = request.getId();
for (Goods goods : goodsList) {
if (goods.getId() == searchId) {
responseObserver.onNext(goods);
responseObserver.onCompleted();
return;
}
}
Goods errorGoods = Goods.newBuilder()
.setId(-1)
.setName("Not Found")
.setDescription("Goods with ID " + searchId + " not found")
.setPrice(0.0f)
.build();
responseObserver.onNext(errorGoods);
responseObserver.onCompleted();
}

@Override
public void onApplicationEvent(ContextClosedEvent event) {
// 关闭gRPC服务器
if (server != null) {
System.out.println("Shutting down gRPC server");
server.shutdown();
try {
server.awaitTermination();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("gRPC server shut down");
}
}
}

Python源代码

服务端:

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
from concurrent import futures
import grpc
import time

import goods_pb2
import goods_pb2_grpc

class GoodsInfoServicer(goods_pb2_grpc.GoodsInfoServicer):
def __init__(self):
self.goods_db = {}
self.next_id = 1

def addGoods(self, request, context):
goods_id = self.next_id
self.goods_db[goods_id] = {
"name": request.name,
"description": request.description,
"price": request.price
}
self.next_id += 1
return goods_pb2.GoodsId(id=goods_id)

def getGoodsById(self, request, context):
goods_id = request.id
if goods_id in self.goods_db:
goods = self.goods_db[goods_id]
return goods_pb2.Goods(
id=goods_id,
name=goods["name"],
description=goods["description"],
price=goods["price"]
)
else:
context.set_details("Goods not found")
context.set_code(grpc.StatusCode.NOT_FOUND)
return goods_pb2.Goods()

def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
goods_pb2_grpc.add_GoodsInfoServicer_to_server(GoodsInfoServicer(), server)
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()

客户端:

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
import grpc
import goods_pb2
import goods_pb2_grpc

def run():
channel = grpc.insecure_channel('localhost:50051')
stub = goods_pb2_grpc.GoodsInfoStub(channel)

goods = goods_pb2.Goods(
name="python goods",
description="a test sample",
price=1200.00
)
response = stub.addGoods(goods)
print("Added goods with ID:", response.id)

goods_id = goods_pb2.GoodsId(id=response.id)
response = stub.getGoodsById(goods_id)
print("Goods ID:", response.id)
print("Goods Name:", response.name)
print("Goods Description:", response.description)
print("Goods Price:", response.price)

if __name__ == '__main__':
run()

Java服务端-Java客户端

项目结构

首先我的项目结果如下,3个微服务,api是对外接口部分,存放proto文件,client客户端,server服务端。其中客户端和服务端为springBoot项目,通过实现ApplicationRunner接口来完成方法调用

image-20240722102332228

引入依赖

在父POM文件引入下面几个插件,其中maven-plugin不多解释,一个打包插件,项目里一般都会有

os-maven-plugin 检测构建系统的操作系统类型,并将其作为 Maven 属性 os.detected.classifier

protobuf-maven-plugin 编译proto文件,并生成对应Java代码

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
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.7.1</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.6.1</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:3.25.1:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.65.0:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

下面继续引入gRPC的依赖

这里我引入在api模块,因为我其他模块会依赖api,如果你不打算用api模块,可以直接引入到父POM内

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
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty-shaded</artifactId>
<version>1.65.1</version>
</dependency>

<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>1.65.1</version>
</dependency>

<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>1.65.1</version>
</dependency>

<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-api</artifactId>
<version>1.65.1</version>
</dependency>

<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>annotations-api</artifactId>
<version>6.0.53</version>
<scope>provided</scope>
</dependency>
</dependencies>

编译proto文件

一般来讲proto文件都会放在与java同级下,我们创建一个proto目录并编译源代码到文件(可以下载一个Protocol Buffers插件,这样代码就会有颜色分级以及关键字提示等)。其中proto内我们加入2个Java的配置,分别是:

option java_multiple_files = true; 如果为true,每个消息类型都会生成单独的类文件,为false则全部放在一个文件
option java_package = “com.sora”; 指定生成的包名,这里和自己的包名对上,不然Spring项目会扫描不到。

image-20240722103523046

完成配置后开始编译,点击右边Maven按钮,我把proto放在api模块下,所以找到api模块,点击protobuf下的protobuf:compileprotobuf:custom。前者编译消息对象,后者生成Java代码

如果你没有Maven图标,可以直接在终端进入api模块(注意是进入到POM所在的目录),使用命令进行编译

1
2
mvn protobuf:compile
mvn protobuf:custom

image-20240722105648032

编译完成后会在target文件内生成对应代码,后面可以直接引入api模块来使用这些代码

image-20240722110701808

运行服务端

引入api模块后,写一个Spring启动类,再完成一个普通类,该类继承ApplicationRunner接口,并且类内部使用数组模拟数据库,这里源代码已经提供过,直接从上面复制粘贴即可。服务端共有4个方法:

run方法会在启动时被调用,代码中我们启动了一个gRPC服务器,配置好端口完成启动

addGoodsgetGoodsById方法则是gRPC的服务方法,用来处理客户端的请求

onApplicationEvent 监听Spring应用的关闭时间,关闭server

运行客户端

同样引入api模块,写一个Spring启动类,再完成一个普通类,继承ApplicationRunner接口。在client代码中,我们只有run方法,方法的逻辑为创建一个gRPC通道,并获取到存根,然后执行添加和查询方法。这里我捕获的异常是StatusRuntimeException,这个异常强烈建议捕获,否则服务端出现异常时,客户端调用服务端失败会直接停止服务

执行结果

启动客户端和服务端,控制台输出分别如下:

image-20240722135541046

客户端同样正确获取到信息

image-20240722135909332

gRPC通信成功。

Python服务端-Java客户端

Python这里的原理跟Java一样,区别只是编程语言的不同,所以就不说那么详细,同样,我们先启动Python的服务端,再启动Java的客户端,client打印内容如下,添加成功

image-20240722140052655

当然Python我也提供了客户端代码,也可以自行尝试Java服务端到Python客户端的通信

结束语

以上就是gRPC的学习和使用,希望可以帮助不懂的同学认识并了解gRPC