grpc是由google开源的rpc(Remote Procedure Call,远程程序调用)框架,rpc用于跨平台、跨语言传输数据。

grpc采用的是http2协议。

使用

项目结构

grpc%206dc0246e96bb4264a82fa800e38e0102/Untitled.png

分别为client(客户端)、grpc(gRPC 服务)、server(服务端)。

grpc

首先是grpc。

引入依赖

<dependency>
  <groupId>io.grpc</groupId>
  <artifactId>grpc-netty-shaded</artifactId>
  <version>1.38.0</version>
</dependency>
<dependency>
  <groupId>io.grpc</groupId>
  <artifactId>grpc-protobuf</artifactId>
  <version>1.38.0</version>
</dependency>
<dependency>
  <groupId>io.grpc</groupId>
  <artifactId>grpc-stub</artifactId>
  <version>1.38.0</version>
</dependency>
<dependency> <!-- necessary for Java 9+ -->
  <groupId>org.apache.tomcat</groupId>
  <artifactId>annotations-api</artifactId>
  <version>6.0.53</version>
  <scope>provided</scope>
</dependency>

因为需要依赖于maven和protobuf进行编译,所以添加maven插件protobuf-maven-plugin,这个插件会将 .proto文件编译成java代码,但是这个插件是在idea的window平台下使用,在linux下需要别的配置https://github.com/grpc/grpc-java/blob/master/COMPILING.md。依旧是在pom.xml文件添加:

<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:3.12.0:exe:${os.detected.classifier}</protocArtifact>
        <pluginId>grpc-java</pluginId>
        <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.38.0:exe:${os.detected.classifier}</pluginArtifact>
      </configuration>
      <executions>
        <execution>
          <goals>
            <goal>compile</goal>
            <goal>compile-custom</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

总的pom.xml文件为:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>grpc</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>grpc</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-boot.version>2.3.7.RELEASE</spring-boot.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-netty-shaded</artifactId>
            <version>1.38.0</version>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-protobuf</artifactId>
            <version>1.38.0</version>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-stub</artifactId>
            <version>1.38.0</version>
        </dependency>
        <dependency> <!-- necessary for Java 9+ -->
            <groupId>org.apache.tomcat</groupId>
            <artifactId>annotations-api</artifactId>
            <version>6.0.53</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <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:3.12.0:exe:${os.detected.classifier}</protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.38.0:exe:${os.detected.classifier}</pluginArtifact>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

在src/mian目录下增加一个proto目录,并新建一个.proto文件类型的文件。

grpc%206dc0246e96bb4264a82fa800e38e0102/Untitled%201.png

定义proto文件

todo 数据类型

//.proto文件

//定义proto的语法
syntax="proto3";

option java_multiple_files = true;
//定义java文件放置在哪个包中
option java_package = "com.example.grpc.api";
option java_outer_classname = "HelloService";

package com.example.grpc.api;

//定义服务
service Hello{
    //定义一个rpc方法,客户端请求参数为HelloRequest,服务端返回数据为HelloResponse
    rpc SayHello(HelloRequest) returns(HelloResponse){}
}

//定义HelloRequest请求参数
message HelloRequest{
    //string为数据类型;name为变量名字;1为数字标识,用来标识字段,每个结构体中唯一,不然会报错
    string name=1;
}

//定义HelloResponse返回参数
message HelloResponse{
    string name=1;
    int32 age=2;
}

编译

接下来编译proto文件。在maven的Lifecycle下找到compile,点击进行编译。

grpc%206dc0246e96bb4264a82fa800e38e0102/Untitled%202.png

能看到编译出了java文件

grpc%206dc0246e96bb4264a82fa800e38e0102/Untitled%203.png

被client和server端依赖

都在client和server的pom.xml文件中添加grpc:

<dependency>
    <groupId>com.example</groupId>
    <artifactId>grpc</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-api</artifactId>
    <version>1.27.2</version>
    <scope>compile</scope>
</dependency>

client

定义一个客户端。

添加一个配置文件,rpc需要运行在一个单独的端口:

spring:
  application:
    name: client
rpc:
  client:
    port: 11111
  server:
    address: localhost
    port: 11112

读取配置文件:

@Configuration
public class RPCConfig {
    @Value("${rpc.client.port}")
    private int clientPort;

    @Value("${rpc.server.address}")
    private String serverAddress;

    @Value("${rpc.server.port}")
    private int serverPort;

    public int getClientPort() {
        return clientPort;
    }

    public String getServerAddress() {
        return serverAddress;
    }

    public int getServerPort() {
        return serverPort;
    }
}

客户端向服务器发起请求:

@Component
public class Client {
    @Autowired
    private RPCConfig rpcConfig;

    public void sayHello(){
        ManagedChannel managedChannel= ManagedChannelBuilder.forAddress(rpcConfig.getServerAddress(),rpcConfig.getServerPort()).usePlaintext().build();
        try {
            HelloGrpc.HelloBlockingStub helloService=HelloGrpc.newBlockingStub(managedChannel);
            HelloRequest helloRequest= HelloRequest.newBuilder()
                    .setName("name")
                    .build();
            HelloResponse helloResponse=helloService.sayHello(helloRequest);
        }finally {
            managedChannel.shutdown();
        }

    }
}

定义一个服务端

添加依赖:

<dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-api</artifactId>
    <version>1.27.2</version>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>com.example</groupId>
    <artifactId>grpc</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <scope>compile</scope>
</dependency>

定义一个接受信息的service:

public class SayHelloService extends HelloGrpc.HelloImplBase {

    @Override
    public void sayHello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
        System.out.println("接受到的信息为:"+request.getName());
        //返回信息
        HelloResponse helloResponse=null;
        try {
            helloResponse=HelloResponse
                    .newBuilder()
                    .setName("name2")
                    .setAge(11)
                    .build();
        }catch (Exception e){
            responseObserver.onError(e);
        }finally {
            responseObserver.onNext(helloResponse);
        }
        responseObserver.onCompleted();
    }
}

定义配置文件,配置grpc运行的端口:

spring:
  application:
    name: server
rpc:
  server:
    port: 11112

读取配置文件:

@Configuration
public class RPCConfig {
    @Value("${rpc.server.port}")
    private int serverPort;

    public int getServerPort() {
        return serverPort;
    }
}

启动一个rpc的服务,添加服务时,需要使用 addService()先注入Service:

import io.grpc.Server;
import io.grpc.ServerBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;

import javax.annotation.PostConstruct;
import java.io.IOException;

@Configuration
public class RPCServer {
    @Autowired
    private RPCConfig rpcConfig;

    @Autowired
    private SayHelloService sayHelloService;

    @PostConstruct
    public void init() throws IOException, InterruptedException {
        Server server=ServerBuilder
                //服务启动端口
                .forPort(rpcConfig.getServerPort())
                //使用依赖注入的方式添加服务,调用方法时能直接使用依赖注入的方式调用其他服务。
                .addService(sayHelloService)
                .build()
                //启动服务
                .start();
        server.awaitTermination(); //停止其他线程,等待请求,等待服务器被终止

    }
}

server.awaitTermination() 能在没有其他任务或线程的情况下,保持程序继续运行,防止程序运行后结束。如果客户端还有其他线程或者任务,使用会导致其他线程阻塞。

测试

测试客户端与服务器是否能正常通讯。

在客户端定义一个定时器:

@Component
public class Task {
    @Autowired
    Client client;

    @Scheduled(initialDelay = 1000,fixedDelay = 60*1000)
    public void work(){
        client.sayHello();
    }
}

能看到服务端打印出来了信息:

接受到的信息为:name

其他

依赖注入

SayHelloService 中使用依赖注入方法调用其他方法。

定义一个测试的Component:

@Component
public class HelloHandler {
    public void work(String name){
    }
}

SayHelloService添加 @Component注解,并注入 HelloHandler

@Component
public class SayHelloService extends HelloGrpc.HelloImplBase {
    @Autowired
    private HelloHandler helloHandler;

    @Override
    public void sayHello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
        System.out.println("接受到的信息为:"+request.getName());
        helloHandler.work(request.getName());
        //返回信息
        HelloResponse helloResponse=null;
        try {
            helloResponse=HelloResponse
                    .newBuilder()
                    .setName("name2")
                    .setAge(11)
                    .build();
        }catch (Exception e){
            responseObserver.onError(e);
        }finally {
            responseObserver.onNext(helloResponse);
        }
        responseObserver.onCompleted();
    }
}

传值

因为null值不能被序列化,所以如果请求或者返回值的参数中存在null值,可能会导致另一端无响应。

部署在服务端

如果项目需要部署在服务端的话,为了使maven能够编译通过,有两种方式解决:

  1. 将编译出来的grpc.jar包以本地依赖的方式依赖到client和server。建议这个。

    <dependency>
    	<groupId>com.example</groupId>
    	<artifactId>grpc</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<scope>system</scope>
    	<systemPath>${project.basedir}/lib/grpc-0.0.1-SNAPSHOT.jar</systemPath>
    </dependency>
    
  2. 使用maven install的方式手动将grpc.jar安装到maven库

报错

  1. 数字标识重复

    PROTOC FAILED: grpc.proto:28:21: Field number 1 has already been used in "XXX" by field "nodeId"
    

    原因是数字标识重复使用,每个结构体的数字标识需要唯一:

  2. 新增服务前需要注册

    io.grpc.StatusRuntimeException: UNIMPLEMENTED: Method not found: com.example.grpc.api.HelloService/sayHello
    

    原因是需要在 RPCServer 中使用 addService() 注册服务 HelloService

参考

https://www.jianshu.com/p/7d6853140e13