写点什么

基于 grpc 手撸一个 RPC 框架

用户头像
cloudcoder
关注
发布于: 2021 年 02 月 24 日

1 手撸 RPC 框架的原因

常见的 RPC 框架有: 比较知名的如阿里的 Dubbo、google 的 gRPC、Go 语言的 rpcx。 但只有 gRPC 支持 streaming 模式。

gRPC 是在任何环境中运行的现代开源高性能 RPC 框架,基于 HTTP/2 标准设计,带来诸如双向流、流控、头部压缩、单 TCP 连接上的多复用请求等特。这些特性使得其在移动设备上表现更好,更省电和节省空间占用。

为了理解如何基于 gRPC 框架,开发 server/client 程序,所以基于 grpc 手撸一个 RPC 框架


源代码存放在 gitee 上,欢迎大家围观: https://gitee.com/cloudcoder/grpc-examples


1.1 gRPC 有什么好处

gRPC 和 restful API 都提供了一套通信机制,用于 server/client 模型通信,而且它们都使用 http 作为底层的传输协议(严格地说, gRPC 使用的 http2.0,而 restful api 则不一定)。

gRPC 还是有些特有的优势,如下:

  1. 通过 protobuf 定义协议接口,有更加严格的接口约束条件。

  2. protobuf 定义的协议在传输时数据被序列化为二进制编码,大幅减少需要传输的数据量,节省带宽并性能。

  3. gRPC 支持流式通信(理论上通过 http2.0 就可以使用 streaming 模式, 但是通常 web 服务的 restful api 很少这么用【WEB 服务基于 HTTP1.1】)

  4. 支持多种语言



2. RPC 框架

2.1 RPC 介绍

RPC 就是要像调用本地的函数一样去调远程函数,RPC 过程如下:


2.1.1 RPC 远程调用过程


  1. 客户机调用一个称为客户机存根【client stub】的本地过程。对于客户机进程来说,这似乎是实际的过程,因为它是一个常规的本地过程。client stub 将参数打包到远程过程(这可能涉及将它们转换为标准格式),并构建一个或多个网络消息。将参数打包到网络消息中称为封送处理【marshaling 】,并要求将所有数据序列化【serializing】为一种扁平的字节数组格式。

  2. 网络消息由 client stub 发送到远程系统(通过使用 sockets interfaces 对本地内核【local kernel】的系统调用)。

  3. 网络消息由内核通过某种协议(无连接或面向连接)传输到远程系统

  4. 服务器存根【server stub】(有时称为骨架 skeleton)接收服务器上的消息。它从网络消息中 unmarshals 出参数,并在必要时将它们从标准网络格式转换为特定于机器的形式。

  5. server stub 调用服务器函数(对客户机来说,服务器函数是远程过程),并将从客户机接收到的参数传递给它

  6. 当服务器函数完成时,它将带着返回值返回到 server stub

  7. 如果有必要,server stub 将返回值转换为一个或多个网络消息发送给 client stub

  8. 消息通过网络发送回 client stub

  9. client stub 从本地内核读取消息

  10. 然后,client stub 将结果返回给客户机函数,如果需要,将它们从网络消息转换为本地的对象


2.1.2 RPC 的好处

RPC 的主要好处有两方面。

  • 程序员现在可以使用过程调用语义来调用远程函数并获取响应。

  • 编写分布式应用程序简化了,因为 RPC 将所有网络代码隐藏在存根函数中。应用程序不需要担心诸如套接字、端口号以及数据转换和解析等细节。

OSI参考模型上,RPC 跨越会话层和表示层(第 5 层和第 6 层)。

2.2 手撸 RPC 框架实现的功能

  • 基于 NettyServerBuilder、NettyChannelBuilder,针对 RPC 的服务端、客户端进行了封装

  • 服务端支持 Authentication Interceptor

  • 服务端目前支持心跳服务、简单的 hello handler、client 管理 handler(提供 client 实例属性上报、keepalive 接口)、消息上报 handler, 通过 ServiceLoader 发现这些 handler

  • 客户端使用 ServiceManager 管理所有 BootService, 并在启动时,ServiceLoader 发现这些 Service,依次调用所有 BootService 实例的 prepare、startup、onComplete 方法,在 BootService 实例(除 GRPCChannelManager)外,将自己作为 GRPCChannelListener 注册到 GRPCChannelManager 中

  • 客户端使用 GRPCChannelManager 管理 GRPCChannel【针对 grpc 的 ManagedChannel 进行封装】,并启动一个线程,根据 channel 状态检查间隔时间定期检查是否需要重连

  • 客户端支持配置多个服务端,如果配置多个,则 client 随机绑定一个

  • 客户端连接成功后,会通知所有注册的服务,标识 channel 连接成功

  • 服务使用 channel,调用 rpc 服务

2.2.1 服务端测试示例

public class ServerMain {    public static void main(String[] args) throws Exception {        ServiceServer server = new ServiceServer("localhost",7077);        server.init();
//通过SPI添加grpc handler HandlerManager.INSTANCE.registerHandlers(server);
server.startServer();
server.blockUntilShutdown(); }}
复制代码


2.2.2 客户端测试示例

public class ClientMain {    private static final Logger LOGGER = LoggerFactory.getLogger(ClientMain.class);
public static void main(String[] args) throws Exception { try { //通过SPI绑定服务 ServiceManager.INSTANCE.boot(); } catch (Exception e) { LOGGER.error("boot failure.", e); }
Runtime.getRuntime() .addShutdownHook( new Thread(ServiceManager.INSTANCE::shutdown, "service shutdown thread"));
HelloServiceClient helloServiceClient = ServiceManager.INSTANCE.findService(HelloServiceClient.class); for (int i = 0; i < 10; i++) { helloServiceClient.greet("张三" + i); }
MsgReportServiceClient msgReportServiceClient = ServiceManager.INSTANCE.findService(MsgReportServiceClient.class); MsgCollection.Builder msgCollection = MsgCollection.newBuilder(); for(int i=0;i<5;i++){ MsgObject.Builder builder = MsgObject.newBuilder(); builder.setId(i+""); builder.setMsg("report msg "+i); builder.setType(i+""); msgCollection.addMsgs(builder); } msgReportServiceClient.reportInSync(msgCollection.build());
List<MsgObject> msgs = Lists.newArrayList(); for(int i=10;i<15;i++){ MsgObject.Builder builder = MsgObject.newBuilder(); builder.setId(i+""); builder.setMsg("report msg "+i); builder.setType(i+""); msgs.add(builder.build()); } msgReportServiceClient.reportBySteam(msgs);
TimeUnit.SECONDS.sleep(1000); }}
复制代码


客户端启动时序图

客户端启动时序图如下:


服务端启动时序图


3. 定义 rpc 协议

3.1 定义 rcp 协议


在 project 的 src/main/proto 和 src/test/proto,根据需要创建 proto 文件,如健康检查 proto

syntax = "proto3";
package grpc.health.v1;
message HealthCheckRequest { string service = 1;}
message HealthCheckResponse { enum ServingStatus { UNKNOWN = 0; SERVING = 1; NOT_SERVING = 2; } ServingStatus status = 1;}
service Health { rpc Check (HealthCheckRequest) returns (HealthCheckResponse);}
复制代码


3.2 根据协议生成代码

使用的 protoc.version 版本是: 3.12.0

参考:https://www.grpc.io/docs/languages/java/generated-code/

在 pom.xml 文件中增加如下内容,则在 compile 或 package 时,protobuf-maven-plugin 会根据定义的 proto 文件,生成代码

<build>    <extensions>      <extension>        <groupId>kr.motd.maven</groupId>        <artifactId>os-maven-plugin</artifactId>        <version>1.6.2</version>      </extension>    </extensions>    <plugins>      <plugin>        <groupId>org.xolstice.maven.plugins</groupId>        <artifactId>protobuf-maven-plugin</artifactId>        <version>0.6.1</version>        <configuration>          <protocArtifact>com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}</protocArtifact>          <pluginId>grpc-java</pluginId>          <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>        </configuration>        <executions>          <execution>            <goals>              <goal>compile</goal>              <goal>compile-custom</goal>            </goals>          </execution>        </executions>      </plugin>    </plugins>  </build>
复制代码


默认情况下,生成的代码在:

  • target/protobuf/grpc-java 存放生成的 rpc 服务,类以 Grpc 结尾

  • target/protobuf/grpc 存放生成的消息对象, 类名是消息的名称、消息的名称+OrBuilder

发布于: 2021 年 02 月 24 日阅读数: 93
用户头像

cloudcoder

关注

10+IT行业老兵,熟悉大数据处理,分布式编程 2020.10.30 加入

InfoQ主页:https://www.infoq.cn/u/cloudcoder 头条主页: https://www.toutiao.com/c/user/token/MS4wLjABAAAAgqsp3gTUFRwr49uODP0W6XdaDB2vI5d_9yPDC1Nzmds/

评论

发布
暂无评论
基于grpc手撸一个RPC框架