gRPC 简介并实战——文末附源码

1. 介绍

gRPC 是一个高性能的开源 RPC 框架,最初由 Google 开发。

RPC 是什么?在客户端应用里可以像调用本地方法对象一样直接调用另一台不同机器上的服务端应用的方法。同时支持跨语言的异构系统。国内开源的 RPC 框架有阿里Dubbo、蚂蚁金服的 SOFA-RPC、百度 bRPC、新浪 Motan等等。

废话不多说,直接就开始使用 gRPC。文末附源码链接。

2. 概述

本文将使用以下步骤使用 gRPC 创建典型的C/S服务:

  1. 首先在 .proto 文件中定义服务: gRPC 使用 protobuf 作为 IDL,明确定义了参数及类型。
  2. 通过 protobuf 编译器自动生成客户端-服务端通信 Stub 的代码。
  3. 创建服务器端的程序,并对 stub 进行实现。
  4. 创建客户端应用程序,使用生成的 stub 进行 RPC 调用服务端方法。

我们先来定义一个简单的 HelloService 服务,它返回问候语和姓名。

3. Maven 依赖

这里添加 grpc-netty , grpc-protobufgrpc-stub 三个依赖:

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

4.定义 .proto 文件

创建 HelloService.proto 文件,并开始如下定义:

syntax = "proto3"; // 告诉编译器此文件使用什么版本的语法,而且默认情况下,编译器在单个 Java 文件中生成所有 Java 代码。
option java_multiple_files = true; // 可选配置,再次告诉编译器所有内容都将在单个文件中生成。
package org.baeldung.grpc; //最后,我们指定要用于生成的 Java 类的包。


// 定义消息结构体,相当于 Http Request
// 并对每个属性都定义数据类型,需要为每个属性分配一个唯一编号,称为标记。此标记由 protobuf 用于表示属性,而不是使用属性名称。
// 因此,与 JSON 不同,我们每次都会传递属性名称 name,而 protobuf 将使用数字 1 来表示 name。
message HelloRequest {
    string firstName = 1;
    string lastName = 2;
}

// 定义消息结构体,相当于 Http Response
// 这里的响应体定义与 `HelloRequest` 类似。需要注意的是,我们可以在多个消息类型之间使用相同的标记:
message HelloResponse {
    string greeting = 1;
}

// 最后,让我们定义服务(method)。对于我们的 HelloService,我这里定义了一个 hello() 操作:单次接受一个请求并返回一个响应
service HelloService {
    rpc hello(HelloRequest) returns (HelloResponse);
}

另外要指出的是,gRPC 还支持通过将 stream 关键字来标示是否进行流式处理。
也就是说,客户端和服务端交互有四种情况,客户端流式/非流式——服务端流式/非流式。

5. 生成代码

现在,我们需要将 HelloService.proto 文件传递 protobuf 编译器 protoc 来生成 Java 文件。有多种方法可以触发此功能。

  • 如果采用 protoc 编译器 的方式,这也是最简单的一种方式:

首先你需要下载编译器:https://developers.google.com/protocol-buffers/docs/downloads,并通过如下命令来将 HelloService.proto 生成 Java 文件:

protoc -I=$SRC_DIR --java_out=$DST_DIR $SRC_DIR/HelloService.proto
  • 当然,你也可以用 Maven 插件的方式:

gRPC 提供了 protobuf-maven-plugin, 在Maven
中添加如下配置:

<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>
        <protocArtifact>
          com.google.protobuf:protoc:3.3.0:exe:${os.detected.classifier}
        </protocArtifact>
        <pluginId>grpc-java</pluginId>
        <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>

上面的 os-maven 插件可以生成各种与平台相关的属性。

6. 创建服务端程序

不过无论您使用上面哪种方法生成代码,都将生成以下关键文件:

  • HelloRequest.java 文件, 包含 HelloRequest 请求类型定义
  • HelloResponse.java 文件,它包 HelloResponse 响应类型定义
  • HelloServiceImplBase.java 文件,它包含抽象类 HelloServiceImplBase,它提供了我们在服务接口中定义的所有操作的实现

6.1 重写抽象类 HelloServiceImplBase的实现

抽象类 HelloServiceImplBase 的默认实现是抛出运行时异常 io.grpc.StatusRuntimeException,用于指出该方法未被实现。

让我们来扩展此类并重写服务定义中提到的 hello() 方法:

public class HelloServiceImpl extends HelloServiceImplBase {
 
    @Override
    public void hello(
      HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
 
        String greeting = new StringBuilder()
          .append("Hello, ")
          .append(request.getFirstName())
          .append(" ")
          .append(request.getLastName())
          .toString();
 
        HelloResponse response = HelloResponse.newBuilder()
          .setGreeting(greeting)
          .build();
 
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}

如果我们将 hello() 的签名与我们在 HellService.proto 文件中写入的签名进行比较,我们会注意到它不会返回 HelloResponse。相反,它将第二个参数称为 StreamObserver<HelloResponse>,它是 响应观察者,是服务器调用其响应的回调
正如最开始提到的那样,客户端将获得进行阻塞调用或非阻塞调用(流式)的选项。

gRPC 使用生成器(builder)创建对象。我们使用 HelloResponse.newBuilder() 并设置"hello" 问候语以生成 HelloResponse 对象。我们将此对象设置为响应观察者的 onNext()方法,将其发送到客户端。

最后,我们需要调用 "onCompleted()" 来指定我们已完成对这次 RPC 的处理,否则连接将挂起,客户端将等待更多信息进来。

6.2 运行服务端程序

接下来,我们需要启动 gRPC 服务器来监听传入的请求:

public class GrpcServer {
    public static void main(String[] args) {
        Server server = ServerBuilder
          .forPort(8080)
          .addService(new HelloServiceImpl()).build();
 
        server.start();
        server.awaitTermination();
    }
}

在这里,我们再次使用生成器(ServerBuilder) 在端口 8080 上创建一个 gRPC 服务器,并添加我们定义的 HelloServiceImpl 服务。start() 将启动服务器。在我们的示例中,我们将调用 awaittermination() 以保持服务器在后台保持运行。

创建客户端程序

gRPC 提供了一个通道构造,用于抽象基础详细信息,如连接、连接池、负载平衡等。

public class GrpcClient {
    public static void main(String[] args) {
        // 这里使用 `ManagedChannelBuilder` 创建通道,并指定需要连接的服务器地址和端口。
        ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8080)
          .usePlaintext() //这里将使用纯文本,无需任何加密
          .build();
 
        /** 接下来,我们需要创建一个 Stub,我们将用它来进行实际的调用远程的 hello() 方法。Stub(存根)是客户端与服务器交互的主要方式。使用自动生成Stub时,Stub 类包含了用于包装通道(channel)的构造函数。
        **/
        HelloServiceGrpc.HelloServiceBlockingStub stub 
          = HelloServiceGrpc.newBlockingStub(channel);
 
        // 是时候进行 hello() RPC 调用了。在这里,我们传递 Hello 请求。我们可以使用newBuilder 来设置 HelloRequest 对象的姓、名属性。并得到从服务器返回的 HelloResponse 对象。
        HelloResponse helloResponse = stub.hello(HelloRequest.newBuilder()
          .setFirstName("test")
          .setLastName("gRPC")
          .build());
 
        channel.shutdown();
    }
}

需要说明的是,上面使用阻塞/同步的存根(newBlockingStub),以便 RPC 调用等待服务器响应,并将返回响应或引发异常。有两种类型的存根由 gRPC 提供,另外一种便于非阻塞/异步调用。

8. 总结

在本文中,介绍了如何使用 gRPC 来简化两个服务之间的通信开发,与此同时,我们可以更加专注地定义服务以及更加专注的实现我们的业务逻辑。

最后,Github 源码获取, 关注公众号(抠腚Coding笔记)并回复:【grpc-demo】

文章首发在公众号:抠腚Coding笔记

并由博客一文多发平台 OpenWrite 发布!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容