前言 RPC 在了解 gRPC 之前,我们需要先知道 RPC,也就是远程过程调用(Remote Procedure Call),它本身并非是一种协议,而是一种调用方式,允许一台机器调用另一台机器上服务的方法,而且屏蔽了底层网络通信的细节,并且支持跨语言调用。目前常见的 RPC 框架有 gRPC、Thrift、Dubbo 等。
gRPC gRPC 是由 Google 开发的现代高性能远程过程调用框架。使用 HTTP/2 作为传输协议,使用 Protocol Buffers(protobufs)作为接口定义语言。也是本文要进行认识学习的一个东西
下面是一张 gRPC 的简单工作图:基于 c++ 编写的服务端,以及与之通信的 Ruby 和 Java 客户端。不同的语言通过编译器插件将 proto 文件生成对应的代码即可使用。
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
具体的环境要求如下:
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 安装:
大意就是不允许安装,让我们使用 brew 或者 pipx。但是这两个都没有对应的 grpc 包
使用 brew 安装:
使用 pipx 安装:
那我们就没办法了吗?还有一招,那就是通过虚拟环境,绕过系统的检测。具体操作如下:
1 2 3 4 5 6 # 创建一个虚拟环境 我选择的路径就是~/pythonEnv/ python3 -m venv ${path} # 启用虚拟环境 PS:这里注意,这里只是这个终端进入了虚拟环境,退出该终端或其他终端仍然不是虚拟环境!!! source ${path}/bin/activate # 进入虚拟环境后我们就可以使用pip3正常下载了 # PS:这里安装完成后不代表可以退出虚拟环境!因为相当于安装在虚拟环境内,所以退出环境后安装的东西也会跟着退出。以后要使用这些东西必须先进入虚拟环境!
Linux: 首先需要进入官网下载对应版本的源代码,这里以最新的版本为例:
Python 官网下载
翻到最底下,点击想要下载的版本
这里我们下载 Gzip 格式,也就是 tgz 包,下载完成后上传到服务器
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:定义消息类型,每个字段都有一个类型和唯一的编号,编号用于序列化和反序列化时的标识,各种语言的消息的类型可以参考下图
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" ; package tutorial;message HelloRequest { string name = 1 ; } message HelloReply { string message = 1 ; } 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) { 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) { 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 futuresimport grpcimport timeimport goods_pb2import goods_pb2_grpcclass 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 grpcimport goods_pb2import goods_pb2_grpcdef 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
接口来完成方法调用
引入依赖 在父 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 项目会扫描不到。
完成配置后开始编译,点击右边 Maven 按钮,我把 proto 放在 api 模块下,所以找到 api 模块,点击 protobuf 下的 protobuf:compile
和 protobuf:custom
。前者编译消息对象,后者生成 Java 代码
如果你没有 Maven 图标,可以直接在终端进入 api 模块(注意是进入到 POM 所在的目录),使用命令进行编译
1 2 mvn protobuf:compile mvn protobuf:custom
编译完成后会在 target 文件内生成对应代码,后面可以直接引入 api 模块来使用这些代码 。
这里最好检查一下 target 内生成的代码有没有被识别为源代码目录,没有的话需要手动设置,否则会识别不到生成的代码,右键 grpc-java 目录和 java 目录,设置为生成的源代码根目录
运行服务端 引入 api 模块后,写一个 Spring 启动类,再完成一个普通类,该类继承 ApplicationRunner
接口,并且类内部使用数组模拟数据库,这里源代码已经提供过,直接从上面复制粘贴即可。服务端共有 4 个方法:
run
方法会在启动时被调用,代码中我们启动了一个 gRPC 服务器,配置好端口完成启动
addGoods
和 getGoodsById
方法则是 gRPC 的服务方法,用来处理客户端的请求
onApplicationEvent
监听 Spring 应用的关闭时间,关闭 server
运行客户端 同样引入 api 模块,写一个 Spring 启动类,再完成一个普通类,继承 ApplicationRunner
接口。在 client 代码中,我们只有 run 方法,方法的逻辑为创建一个 gRPC 通道,并获取到存根,然后执行添加和查询方法。这里我捕获的异常是 StatusRuntimeException
,这个异常强烈建议捕获,否则服务端出现异常时,客户端调用服务端失败会直接停止服务
执行结果 启动客户端和服务端,控制台输出分别如下:
客户端同样正确获取到信息
gRPC 通信成功。
Python 服务端 - Java 客户端 Python 这里的原理跟 Java 一样,区别只是编程语言的不同,所以就不说那么详细,同样,我们先启动 Python 的服务端,再启动 Java 的客户端,client 打印内容如下,添加成功
当然 Python 我也提供了客户端代码,也可以自行尝试 Java 服务端到 Python 客户端的通信
结束语 以上就是 gRPC 的学习和使用,希望可以帮助不懂的同学认识并了解 gRPC