GRPC远程服务调用详解


一、概念介绍

1. RPC调用

开始之前先介绍一下 RPC(Remote procedure call) 的定义,即远程服务调用,即在当前的程序中调用其它程序的服务实现。

假设存在两个程序 Systemc-ASystemc-B,并在 Systemc-A 中定义了接口 getData(),而实现类在 Systemc-B 中。当 Systemc-A 经过一定手段通过 getData() 接口调用 Systemc-B 中的实现类,这个过程即远程服务调用。

2. 实现原理

针对 RPC 服务功能的实现,通常可通过动态代理等特性实现。

如通过动态代理的方式创建实例对象时获取目标类信息、目标方法及方法参数等信息,再经由网络传输等手段请求至远端服务将传入的信息利用反射实现服务调用并返回。

二、工程配置

1. 依赖引入

GRPC 是由 Google 开发的一款 RPC 服务框架,基于 HTTP2 传输协议实现,针对网络层实现一系列优化从而达到高效传输的目的。

GRPC 提供了多语言的集成方式,包含 Java SDK 依赖,在工程中添加下述依赖即可开箱即用。

<dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-netty</artifactId>
    <version>1.62.2</version>
</dependency>
<dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-protobuf</artifactId>
    <version>1.62.2</version>
</dependency>
<dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-stub</artifactId>
    <version>1.62.2</version>
</dependency>

2. 插件依赖

GRPC 的服务接口并不是传统的 .java 文件配置,而是以其相应格式标准定义于 .proto 文件中,因此需要在工程依赖中添加对应的打包插件。

在工程的 pom.xml 文件中添加下述插件配置:

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

3. 插件安装

除此之外,还需要在 IDEA 中安装 proto 编译插件用于将 proto 文件内容转为 java 文件。

如果 IDEA 为旗舰版则自带 Protocol Buffers 插件,若为社区版则需要手动去插件市场下载 Protobuf 插件。

三、文件编写

1. 基本格式

GRPC 中,接口的定义不在处于 java 文件中,而是通过 proto 文件定义。

并且需要注意的一点是 .proto 文件需要放置于 proto 目录下,其与 java 目录处于同一层级。

proto 文件定义并不复杂,其中通过 syntax 指定协议版本;package 用于指定报名,与 java 中的包名作用效果类似用于层级管理;option 用于添加参数配置。

syntax = "proto3";

package xyz.ibudai.practice.tool.grpc;

option java_multiple_files = true;
// 指定生成的 gRPC 服务接口和消息类的包名
option java_package = "xyz.ibudai.practice.tool.grpc.bean";

2. 对象定义

在接口定义中,通过其入参与出参都可能为复杂的 Java 对象,而在 proto 文件中即通过 message 关键字定义对象。

例如存在一个 Java 类定义如下:

public class HelloRequest {
    String userName;
}

HelloRequestproto 中对应的定义形式如下,其中 userName = 1 的含义代表给 userName 的别名为 1

众所周知,当一个类以 Json 等形式表达时其格式为 key:value 形式,即上述示例对应的为 "userName": "Alex" 形式,但是在 GRPC 中其为 "1": "Alex",即通过数字别名取代字段名,进一步减少数据的大小降低传输压力。

message HelloRequest {
    string userName = 1;
}

3. 接口定义

同理在 proto 中接口类的定义通过 servicerpc 关键字定义。

如存在下述示例接口类 TestServicegetDate() 方法。

public interface TestService {

    HelloResponse getDate(HelloRequest req);
}

则上述接口在对应的 proto 文件中的定义格式如下:

service TestService {
    rpc getDate (HelloRequest) returns (HelloResponse) {
    }
}

下述即为一个完整的 proto 文件定义示例。

syntax = "proto3";

package xyz.ibudai.practice.tool.grpc;

option java_multiple_files = true;
option java_package = "xyz.ibudai.practice.tool.grpc.bean";

// 定义请求体
message HelloRequest {
    string userName = 1;
}

// 定义相应内容
message HelloResponse {
    string message = 1;
}

// 服务接口,定义请求参数和相应结果
service TestService {
    // 定义 RPC 接口方法
    rpc getDate (HelloRequest) returns (HelloResponse) {
    }
}

4. 文件编译

完成上述内容编写之后需要将其编译为程序可读的 Java 文件。

IDEA 右侧 maven 中找到下述依次执行编译。

执行完成后即会在工程的 target 目录下生成对应的文件,其中执行 compile 会生成文件于 target/generated-sources/protobuf/java 下,执行 custom 文件会生成于 target/generated-sources/protobuf/grpc-java 下。

四、服务端应用

1. 接口实现

完成上述的接口定义之后即可编写接口的实现类,实现编译生成类 TestServiceGrpc.TestServiceImplBase 并重写接口方法,通过 onNext()onCompleted() 将数据提交返回。

public class TestServiceImpl extends TestServiceGrpc.TestServiceImplBase {

    @Override
    public void getDate(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
        String data = request.getUserName();
        System.out.println("Server receive data: " + data);
        HelloResponse response = HelloResponse.newBuilder()
                .setMessage("Data from server: " + data)
                .build();
        // 返回数据,等价于 return response;
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}

2. 服务启动

接口实现定义完成后即可启动服务端,通过 ServerBuilder 构建服务端对象,默认 Host 为当前本地 IP,其中 addService() 为添加服务实现类,

完成后通过 start() 启动服务并由 awaitTermination() 阻塞当前线程等待应用关闭。

public class ServerTest {

    private static final int PORT = 9000;

    public static void main(String[] args) throws Exception {
        Server server = ServerBuilder.forPort(PORT)
                .addService(new TestServiceImpl())
                .build();
        server.start();
        server.awaitTermination();
    }
}

五、客户端应用

1. 接口请求

相对于服务端而言客户端实现实现更为简单,通过 ManagedChannelBuilder 连接至服务端,并由 newBlockingStub() 创建实例对象。

当创建完实例对象 stub 即与普通的接口调用并无区别,对应的示例如下:

public class ClientTest {

    private static final String HOST = "localhost";

    private static final int PORT = 9000;

    public static void main(String[] args) {
        ManagedChannel channel = ManagedChannelBuilder.forAddress(HOST, PORT)
                .usePlaintext()
                .build();
        TestServiceGrpc.TestServiceBlockingStub stub = TestServiceGrpc.newBlockingStub(channel);

        HelloRequest request = HelloRequest.newBuilder()
                .setUserName("Alex")
                .build();
        HelloResponse response = stub.getDate(request);
        System.out.println(response);
    }
}

参考链接

  1. Introduction to gRPC

文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录