谷歌 grpc从搭建到使用详解

2年前 (2022) 程序员胖胖胖虎阿
149 0 0

前言

在微服务盛行的今天,想必没有对RPC这个概念不熟悉的小伙伴了吧,RPC技术让微服务跨进程之间的通信成为了现实,比如大家熟悉的springcloud,dubbo等微服务治理框架;

跨进程通信技术

跨进程通信技术其实早有实现,而且在多年前或多或少的可能都有接触过

  • RMI技术,基于TCP通信实现远程调用,实现逻辑较为复杂,目前只能在Java应用之间使用;
  • SOAP协议,简单对象访问协议,通过http协议封装XML格式的SOAP数据包,实现跨进程通信,是早期的webservice技术的底层实现,但是实现过程较为繁琐,并且上手成本较高,使用XML必须遵循一定的规范,后来逐渐被Rest风格的应用程序替代;
  • Restful,通用实现技术仍然是Http,但是能够将restful风格的应用程序抽象成统一的通过URI资源访问的集合,比如我们熟悉的 POST、GET等请求,同时其资源结果响应的类型也更加丰富,比如Json,XML等;

跨进程通信技术存在的问题

一个新技术的出现,往往是为了解决现实中存在的某类问题而产生的,上面谈到了3种大家或多或少接触过的RPC技术,而且在当下,使用Restful风格的这种跨进程通信技术在Java领域可以说是非常盛行了,比如小编所在的团队,与其他应用打交道时,也是使用了这种方式;

restful风格的实现尽管目前来看仍然是比较好的选择,但在具体实践的时候,仍然存在一些让人头疼的问题,总结下面几点:

  • 基于文本的低效消息协议;
  • 应用程序之间缺乏强类型接口;
  • 风格架构很难在不同应用之间做到统一,不好强制实施和统一管理;

而且在具体实施的时候,还有一个比较大的问题就是,跨语言跨应用的通信时,restful风格的技术实现仍然很难提供有效的技术支撑,比如涉及到返回结果的序列化传输,协议之间兼容性等方面的问题,已经日渐凸显,那么有没有一种技术能够较好的一次性解决跨进程通信中存在的这些问题呢?答案是肯定的,这就是谷歌推出的 gRPC技术;

Google gRPC框架

2015年,谷歌发布了开源的gRPC,该技术基础实施具有标准化、通用化和跨平台特征,旨在提供可扩展性,兼具性能优势,但是发布初衷主要面向社区,gRPC的早期技术实现基于内部的一个叫做Stubby的通用框架,主要为了解决内部服务之间耦合性过高的问题,即微服务的解耦;

gRPC框架优势

gRPC技术具有多方面的优势,总结如下几点:

  • 提供高效的进程间通信;
  • 服务接口定义简单易用;
  • 属于强类型的调用;
  • 支持多语言;
  • 支持双工通信;
  • 社区活跃;

gRPC 缺点

  • 不太适合面向外部的服务,比如涉及到其他厂商之间的调用;
  • 服务定义文件的配置是开发中比较复杂的流程,有一定的学习成本;
  • 目前生态系统相对较小;

gRPC 核心技术protobuf

protobuf是谷歌提供的一种具有高效的协议数据转换交换格式工具库,类似于json,而protobuf具备更高的转换效率,时间效率和空间效率相比json来说更加强劲;

如下为gRPC底层protobuf在一次跨进程通信调用的实现逻辑;

谷歌 grpc从搭建到使用详解

gRPC 环境搭建

接下来通过完整的操作步骤,演示下如何在自己的机器上使用gRPC;

一、本地安装protobuf环境

1、github下载protobuf安装包

下载地址:protobuf下载地址 ,根据自己的需要下载合适的版本

谷歌 grpc从搭建到使用详解

2、解压到本地的指定目录

谷歌 grpc从搭建到使用详解

 3、配置环境变量

谷歌 grpc从搭建到使用详解

然后配置到Path中即可,这个和配置JDK等类似

谷歌 grpc从搭建到使用详解

最后通过CMD命令测试下是否配置成功,输入 protoc ,如果出现下面的配置信息说明配置完成

谷歌 grpc从搭建到使用详解

避坑提醒

本人在环境配置都完毕后,通过CMD命令始终无法输出上面的截图信息,翻阅了一些资料并尝试过后,终于搞清问题源头了,操作也很简单,只需要将 protoc.exe 这个文件拷贝到 C:\Windows\System32 目录下即可,切记

二、idea 安装protobuf 插件

为了在开发过程中使用protobuf进行相关的编译打包时更加便捷,需要为idea安装protobuf插件,安装过程非常简单,只需要搜索 protobuf安装,然后重启 idea 即可

谷歌 grpc从搭建到使用详解

以上基础环境准备工作就基本完成了,接下来看看如何使用 protobuf 进行编码实现吧

gRPC 编码实现

gRPC 在具体的实现,需要开发server端和client端,这个类似于我们在使用dubbo进行调用的时,接口提供方可理解为服务端,而服务调用者柯理解为客户端;

一、服务端代码开发过程

1、引入基础的pom依赖

<repositories>
        <repository>
            <id>alimaven</id>
            <name>aliyun maven</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>alimaven</id>
            <name>aliyun maven</name>
            <url>http://maven.aliyun.com/nexus/content/groups/public/</url>
        </pluginRepository>
    </pluginRepositories>

    <dependencies>

        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-netty-shaded</artifactId>
            <version>1.42.0</version>
        </dependency>

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

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

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

        <!--  protobuf 支持 Java 核心包-->
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>3.15.3</version>
        </dependency>

        <!--  proto 与 Json 互转会用到-->
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java-util</artifactId>
            <version>3.15.3</version>
        </dependency>

    </dependencies>


    <build>
        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.6.0</version>
            </extension>
        </extensions>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <!--跳过test测试-->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <skip>true</skip>
                </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.5.1:exe:${os.detected.classifier}</protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.11.0:exe:${os.detected.classifier}</pluginArtifact>
                    <!--默认值-->
                    <protoSourceRoot>${project.basedir}/src/main/proto</protoSourceRoot>
                    <!--默认值-->
                    <!--<outputDirectory>${project.build.directory}/generated-sources/protobuf/java</outputDirectory>-->
                    <outputDirectory>${project.basedir}/src/main/java</outputDirectory>
                    <!--设置是否在生成java文件之前清空outputDirectory的文件,默认值为true,设置为false时也会覆盖同名文件-->
                    <clearOutputDirectory>false</clearOutputDirectory>
                </configuration>
                <executions>
                    <execution>
                        <!--在执行mvn compile的时候会执行以下操作-->
                        <phase>compile</phase>
                        <goals>
                            <!--生成OuterClass类-->
                            <goal>compile</goal>
                            <!--生成Grpc类-->
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

2、定义proto文件

注意,定义的proto文件一定要在main主目录下

谷歌 grpc从搭建到使用详解

proto 定义文件,该文件用于描述生成基础的服务信息,有点像我们使用mybatis的自动生成工具


syntax = "proto3";

//是否生成多个类
option java_multiple_files = false;

//生成的java类所在的包
option java_package = "com.congge.news.proto";

//外层类的名称
option java_outer_classname = "NewsProto";

//包名
package news;

//定义的RPC服务的 RouteGuide
service NewsService {
    rpc list(NewsRequest) returns (NewsResponse) {}

}

message NewsRequest {
  string date = 1;
}

message NewsResponse {
    //repeated 表面返回结果是一个集合
  repeated News news = 1;
}

//上面引用的对象的字段定义
message News {
    int32 id = 1;
    string title = 2;
    string content = 3;
    int64 createTime = 4;
}

3、执行编译打包,生成相关的服务文件

依次点击执行下图中的两个命令,在指定的目录下,将会生成2个服务文件 

谷歌 grpc从搭建到使用详解

谷歌 grpc从搭建到使用详解

4、自定义服务类

可以理解该类为服务端生成客户端请求响应数据的服务类,有点像我们编写netty代码时的一个个handler的作用;

import com.congge.news.proto.NewsProto;
import com.congge.news.proto.NewsServiceGrpc;
import io.grpc.stub.StreamObserver;

import java.util.Date;

public class NewsService extends NewsServiceGrpc.NewsServiceImplBase {

    @Override
    public void list(NewsProto.NewsRequest request, StreamObserver<NewsProto.NewsResponse> responseObserver) {
        String date = request.getDate();
        NewsProto.NewsResponse newsList = null;
        try{
            NewsProto.NewsResponse.Builder newsBuilder = NewsProto.NewsResponse.newBuilder();
            for(int i=0;i<100;i++){
                NewsProto.News build = NewsProto.News.newBuilder().setId(i)
                        .setContent(date + "当天新闻" + i)
                        .setTitle("新闻标题" + i)
                        .setCreateTime(new Date().getTime())
                        .build();
                newsBuilder.addNews(build);
            }
            newsList = newsBuilder.build();
        }catch (Exception e){
            responseObserver.onError(e);
        }finally {
            responseObserver.onNext(newsList);
        }
        responseObserver.onCompleted();
    }

}

5、服务启动类

编写一个服务启动类,用户暴露服务端的端口信息,并等待客户端连接

import io.grpc.Server;
import io.grpc.ServerBuilder;

import java.io.IOException;

public class GrpcServer {

    public static void main(String[] args) {
        try {
            Server start = ServerBuilder.forPort(9988).addService(new NewsService()).build().start();
            System.out.println("GRPC服务端启动,端口号为:" + 9988);
            try {
                start.awaitTermination();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

运行该类main方法,看到下面的效果,说明服务端启动成功;

谷歌 grpc从搭建到使用详解

二、客户端代码开发过程

1、引入maven相关依赖

和server端保持一致

2、定义proto文件

和server端保持一致

3、生成相关的服务文件

和server端一样的操作

谷歌 grpc从搭建到使用详解

执行成功后在指定的包下生成了相关的服务文件

谷歌 grpc从搭建到使用详解

4、定义客户端启动类

import com.congge.news.proto.NewsProto;
import com.congge.news.proto.NewsServiceGrpc;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

import java.util.List;

public class NewClient {

    public static void main(String[] args) {
        ManagedChannel managedChannel = ManagedChannelBuilder.forAddress("localhost", 9988).usePlaintext().build();

        try {
            NewsServiceGrpc.NewsServiceBlockingStub blockingStub = NewsServiceGrpc.newBlockingStub(managedChannel);

            NewsProto.NewsRequest request = NewsProto.NewsRequest.newBuilder().
                    setDate("20220820").build();
            NewsProto.NewsResponse response = blockingStub.list(request);

            //对结果集进行处理
            List<NewsProto.News> newsList = response.getNewsList();
            for(NewsProto.News news : newsList){
                System.out.println(news.getTitle() + ":" + news.getContent());
            }
        }finally {
            managedChannel.shutdown();
        }
    }

}

写到这里,有没有发现这个套路和netty的编码风格太像了,毕竟底层的通信离不开netty那一套的;

运行上面的main方法,观察控制台打印结果,可以看到,客户端成功的从服务端那边获取到了数据;

, 

谷歌 grpc从搭建到使用详解

本篇通过案例演示讲解了基于Java使用gRPC的完整过程,其实,在真实的业务场景下,客户端可能是python语言,也可能是go等其他语言,所以对于客户端来说,只需要按照自身的语言特点,按照上面同样的操作流程,进行编码即可。

版权声明:程序员胖胖胖虎阿 发表于 2022年9月7日 上午7:48。
转载请注明:谷歌 grpc从搭建到使用详解 | 胖虎的工具箱-编程导航

相关文章

暂无评论

暂无评论...