零基础入门gRPC(一)

RPC是一种远程调用技术,涉及服务发现、负载均衡和序列化等组件。gRPC是一个基于HTTP/2的高性能RPC框架,使用protobuf进行数据序列化。protobuf是一种跨平台、语言无关的序列化协议,提供高效的数据编码。在gRPC中,服务定义包含方法和消息类型,通过protoc编译器生成客户端和服务器代码。客户端和服务器通过protobuf消息进行通信,实现简单、高效的远程调用。

目录

RPC

基本概念

RPC 框架

RPC 核心功能

目标

gRPC简介

特点

使用 protobuf序列化协议 

什么是ProtoBuf

如何使用 ProtoBuf

消息对象

字段 

字段修饰符 

字段类型

基本数据类型

枚举类型 

标识号

生成代码

具体项目中使用

定义服务(Service)

快速使用 gRPC 框架

安装protocbuf

安装protocbuf插件

gRPC项目构建

添加.proto文件

根据.proto文件生成消息体类文件和XXXGrpc类文件

编写接口实现类

定义服务端

定义客户端


RPC

基本概念

RPC(Remote Procedure Call):远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的思想。

RPC 是一种技术思想而非一种规范或协议,常见 RPC 技术和框架有:

  • 应用级的服务框架:阿里的 Dubbo/Dubbox、Google gRPC、Spring Boot/Spring Cloud、Facebook 的 Thrift、Twitter 的 Finagle 等。
  • 远程通信协议:RMI、Socket、SOAP(HTTP XML)、REST(HTTP JSON)。
  • 通信框架:MINA 和 Netty。
  • ps: Google gRPC 框架是基于 HTTP2 协议实现的,底层使用到了 Netty 框架的支持。

RPC 框架

一个典型 RPC 的使用场景中,包含了服务发现、负载、容错、网络传输、序列化等组件,其中“RPC 协议”就指明了程序如何进行网络传输和序列化。

这里面主要就是要注意:传输、序列化

完整 RPC 架构图

RPC 核心功能

RPC 的核心功能是指实现一个 RPC 最重要的功能模块,就是上图中的”RPC 协议”部分:

一个 RPC 的核心功能主要有 5 个部分组成,分别是:客户端、客户端 Stub、网络传输模块、服务端 Stub、服务端等。

RPC 核心功能图

 

下面分别介绍核心 RPC 框架的重要组成:

  1. 客户端(Client):服务调用方。
  2. 客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端。
  3. 服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理。
  4. 服务端(Server):服务的真正提供者。
  5. Network Service:底层传输,可以是 TCP 或 HTTP。

一次 RPC 调用流程如下:

  1. 服务消费者(Client 客户端)通过本地调用的方式调用服务。
  2. 客户端存根(Client Stub)接收到调用请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体。
  3. 客户端存根(Client Stub)找到远程的服务地址,并且将消息通过网络发送给服务端。
  4. 服务端存根(Server Stub)收到消息后进行解码(反序列化操作)。
  5. 服务端存根(Server Stub)根据解码结果调用本地的服务进行相关处理
  6. 服务端(Server)本地服务业务处理。
  7. 处理结果返回给服务端存根(Server Stub)。
  8. 服务端存根(Server Stub)序列化结果。
  9. 服务端存根(Server Stub)将结果通过网络发送至消费方。
  10. 客户端存根(Client Stub)接收到消息,并进行解码(反序列化)。
  11. 服务消费方得到最终结果。

目标

RPC的目标就是要2~10这些步骤都封装起来,让用户对这些细节透明。

gRPC简介

官网

gRPC

gRPC 官方文档中文版

gRPC 官方文档中文版_V1.0

RPC 框架的目标就是让远程服务调用更加简单、透明,其负责屏蔽底层的传输方式(TCP/UDP)、序列化方式(XML/Json)和通信细节。服务调用者可以像调用本地接口一样调用远程的服务提供者,而不需要关心底层通信细节和调用过程

gRPC 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。目前提供 C、Java 和 Go 语言版本,分别是:grpc, grpc-java, grpc-go. 其中 C 版本支持 C, C++, Node.js, Python, Ruby, Objective-C, PHP 和 C# 支持.

在 gRPC 里客户端应用可以像调用本地对象一样直接调用另一台不同的机器上服务端应用的方法,使得您能够更容易地创建分布式应用和服务。与许多 RPC 系统类似,gRPC 也是基于以下理念:定义一个服务,指定其能够被远程调用的方法(包含参数和返回类型)。在服务端实现这个接口,并运行一个 gRPC 服务器来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。

gRPC 客户端和服务端可以在多种环境中运行和交互 - 从 google 内部的服务器到你自己的笔记本,并且可以用任何 gRPC 支持的语言来编写。所以,你可以很容易地用 Java 创建一个 gRPC 服务端,用 Go、Python、Ruby 来创建客户端。 

特点

gRPC 的功能优点:

  • 高兼容性、高性能、使用简单

gRPC 的组成部分:

  • 使用 http2 作为网络传输层
  • 使用 protobuf 这个高性能的数据包序列化协议
  • 通过 protoc gprc 插件生成易用的 SDK(软件开发工具包)

使用 protobuf序列化协议 

什么是ProtoBuf

ProtoBuf(Protocol Buffers)是一种跨平台、语言无关、可扩展的序列化结构数据的方法,可用于网络数据交换及存储。

在序列化结构化数据的机制中,ProtoBuf是灵活、高效、自动化的,相对常见的XML、JSON,描述同样的信息,ProtoBuf序列化后数据量更小 (在网络中传输消耗的网络流量更少)、序列化/反序列化速度更快、更简单。

一旦定义了要处理的数据的数据结构之后,就可以利用ProtoBuf的代码生成工具生成相关的代码。只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言(proto3支持C++, Java, Python, Go, Ruby, Objective-C, C#)或从各种不同流中对你的结构化数据轻松读写。

如何使用 ProtoBuf

ProtoBuf 协议的工作流程

在开发 gRPC 应用程序时,先要定义服务接口,其中应包含如下信息:消费者消费服务的方式、消费者能够远程调用的方法以及调用这些方法所使用的参数和消息格式等。在服务定义中所使用的语言叫作接口定义语言(interface definition language,IDL)。

借助服务定义,可以生成服务器端代码,也就是服务器端骨架 (这里的“骨架”和“存根”都是代理。服务器端代理叫作“骨架”(skeleton),客户端代理叫作“存根”(stub)。),它通过提供低层级的通信抽象简化了服务器端的逻辑。同时,还可以生成客户端代码,也就是客户端存根,它使用抽象简化了客户端的通信,为不同的编程语言隐藏了低层级的通信。就像调用本地函数那样,客户端能够远程调用我们在服务接口定义中所指定的方法。底层的 gRPC 框架处理所有的复杂工作,通常包括确保严格的服务契约、数据序列化、网络通信、认证、访问控制、可观察性等。

为了理解 gRPC 的基本概念,我们来看一个使用 gRPC 实现微服务的实际场景。假设我们正在构建一个在线零售应用程序,该应用程序由多个微服务组成。

如图 1-1 所示,假设我们要构建一个微服务来展现在线零售应用程序中可售商品的详情。例如,将 ProductInfo 服务建模为 gRPC 服务,通过网络对外暴露。

服务定义是在 ProductInfo.proto 文件中声明的,服务器端和客户端都会使用该文件来生成代码。这里假设 ProductInfo 服务使用 Go 语言来实现,消费者使用 Java 语言来实现,两者之间的通信则通过 HTTP/2 来进行。

典型的序列化和反序列化过程往往需要如下组件:

  • IDL(Interface description language)文件:参与通讯的各方需要对通讯的内容需要做相关的约定(Specifications)。为了建立一个与语言和平台无关的约定,这个约定需要采用与具体开发语言、平台无关的语言来进行描述。这种语言被称为接口描述语言(IDL),采用IDL撰写的协议约定称之为IDL文件。
  • IDL Compiler:IDL文件中约定的内容为了在各语言和平台可见,需要有一个编译器,将IDL文件转换成各语言对应的动态库。
  • Stub/Skeleton Lib:负责序列化和反序列化的工作代码。Stub是一段部署在分布式系统客户端的代码,一方面接收应用层的参数,并对其序列化后通过底层协议栈发送到服务端,另一方面接收服务端序列化后的结果数据,反序列化后交给客户端应用层;Skeleton部署在服务端,其功能与Stub相反,从传输层接收序列化参数,反序列化后交给服务端应用层,并将应用层的执行结果序列化后最终传送给客户端Stub。
  • Client/Server:指的是应用层程序代码,他们面对的是IDL所生存的特定语言的class或struct。
  • 底层协议栈和互联网:序列化之后的数据通过底层的传输层、网络层、链路层以及物理层协议转换成数字信号在互联网中传递。

可以看到,对于序列化协议来说,使用方只需要关注业务对象本身,即 idl (Interface description language)定义,序列化和反序列化的代码只需要通过工具生成即可。

ProtoBuf 消息定义

在Java 中,构建一个Person类的数据结构,包含成员变量name、id、email等等

// Java类

public class Person
{
    private String name;
    private Int id;
    private String email;
...
}

根据上述数据结构的需求,在demo.proto里 通过 Protocol Buffer 语法写入对应 .proto对象模型的代码

proto3版本

syntax = "proto3"; // 协议版本(proto3中,在第一行非空白非注释行,必须写:syntax = "proto3";)
package protocobuff_Demo;
// 关注1:包名,防止不同 .proto 项目间命名 发生冲突

option java_package = "com.chenj.protobuf";//// 作用:指定生成的类应该放在什么Java包名下
option java_outer_classname = "Demo";//作用:生成对应.java 文件的类名(不能跟下面message的类名相同)
// 关注2:option选项,作用:影响 特定环境下 的处理方式

// 关注3:消息模型 作用:真正用于描述 数据结构
// 下面详细说明
// 生成 Person 消息对象(包含多个字段,下面详细说明)
message Person {
     string name = 1;//(proto3消息定义时,移除了 “required”、 “optional” :)
     int32 id = 2;//(proto3消息定义时,移除了 “required”、 “optional” :)
     string email = 3;//(proto3消息定义时,移除了 “required”、 “optional” :)

    enum PhoneType {
        MOBILE = 0;
        HOME = 1;
        WORK = 2;
    }

    message PhoneNumber {
        string number = 1;
        PhoneType type = 2 ;//(proto3消息定义时,移除了 default 选项:)
    }

    repeated PhoneNumber phone = 4;
}

message AddressBook {
    repeated Person person = 1;
}

消息对象

ProtocolBuffers 中:

  • 一个消息对象(Message) = 一个 结构化数据
  • 消息对象用 修饰符 message 修饰
  • 消息对象 含有 字段:消息对象(Message)里的 字段 = 结构化数据 里的成员变量

字段 

消息对象的字段 组成主要是:字段 = 字段修饰符 + 字段类型 +字段名 +标识号

字段修饰符 

作用:设置该字段解析时的规则

字段类型

字段类型主要有 三 类:

  • 基本数据 类型
  • 枚举 类型
  • 消息对象 类型

基本数据类型

.proto基本数据类型 对应于 各平台的基本数据类型如下

枚举类型 

作用:为字段指定一个 可能取值的字段集合,该字段只能从 该指定的字段集合里 取值

下面例子,电话号码 可能是手机号、家庭电话号或工作电话号的其中一个,那么就将PhoneType定义为枚举类型,并将加入电话的集合( MOBILE、 HOMEWORK

// 枚举类型需要先定义才能进行使用

// 枚举类型 定义
 enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
// 电话类型字段 只能从 这个集合里 取值
  }

// 特别注意:
// 1. 枚举类型的定义可在一个消息对象的内部或外部
// 2. 都可以在 同一.proto文件 中的任何消息对象里使用
// 3. 当枚举类型是在一消息内部定义,希望在 另一个消息中 使用时,需要采用MessageType.EnumType的语法格式

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
    // 使用枚举类型的字段(设置了默认值)
  }

// 特别注意:
// 1.  枚举常量必须在32位整型值的范围内
// 2. 不推荐在enum中使用负数:因为enum值是使用可变编码方式的,对负数不够高

标识号

作用:通过二进制格式唯一标识每个字段

  1. 一旦开始使用就不能够再改变
  2. 标识号使用范围:[1,2的29次方 - 1]

每个字段在进行编码时都会占用内存,而 占用内存大小 取决于 标识号:

  1. 范围 [1,15] 标识号的字段 在编码时占用1个字节;
  2. 范围 [16,2047] 标识号的字段 在编码时占用2个字节

生成代码

首先安装 ProtoBuf 编译器 protoc,这里有详细的安装教程,安装完成后,可以使用以下命令生成 Java 源代码:        

protoc --java_out=./src/main/java ./src/main/idl/customer.proto

从项目的根路径执行该命令,并添加了两个参数:java_out,定义./src/main/java/为Java代码的输出目录;而./src/main/idl/customer.proto是.proto文件所在目录。

(编译器为每个.proto文件里的每个消息类型生成一个.java文件&一个Builder类 (Builder类用于创建消息类接口))

具体项目中使用

消息对象类介绍

通过.proto文件 转换的 Java源代码 = Protocol Buffer 类 + 消息对象类(含Builder内部类)

消息对象类(Message类)

  • 消息对象类 类通过 二进制数组 写 和 读 消息类型
  • 使用方法包括:
<-- 方式1:直接序列化和反序列化 消息 -->
protocolBuffer.toByteArray();
// 序列化消息 并 返回一个包含它的原始字节的字节数组
protocolBuffer.parseFrom(byte[] data);
// 从一个字节数组 反序列化(解析) 消息

<-- 方式2:通过输入/ 输出流(如网络输出流) 序列化和反序列化消息 -->
protocolBuffer.writeTo(OutputStream output);
output.toByteArray();
// 将消息写入 输出流 ,然后再 序列化消息 

protocolBuffer.parseFrom(InputStream input);
// 从一个 输入流 读取并 反序列化(解析)消息


// 只含包含字段的getters方法
// required string name = 1;
public boolean hasName();// 如果字段被设置,则返回true
public java.lang.String getName();

// required int32 id = 2;
public boolean hasId();
public int getId();

// optional string email = 3;
public boolean hasEmail();
public String getEmail();

// repeated .tutorial.Person.PhoneNumber phone = 4;
// 重复(repeated)字段有一些额外方法
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
// 列表大小的速记
// 作用:通过索引获取和设置列表的特定元素的getters和setters

Builder

作用:创建 消息构造器 & 设置/ 获取消息对象的字段值 & 创建 消息类 实例

a. 创建 消息构造器

Demo.Person.Builder person = Person.newBuilder();

b. 设置/ 获取 消息对象的字段值 具体方法如下:

// 标准的JavaBeans风格:含getters和setters
// required string name = 1;
public boolean hasName();// 如果字段被设置,则返回true
public java.lang.String getName();
public Builder setName(String value);
public Builder clearName(); // 将字段设置回它的空状态

// required int32 id = 2;
public boolean hasId();
public int getId();
public Builder setId(int value);
public Builder clearId();

// optional string email = 3;
public boolean hasEmail();
public String getEmail();
public Builder setEmail(String value);
public Builder clearEmail();

// repeated .tutorial.Person.PhoneNumber phone = 4;
// 重复(repeated)字段有一些额外方法
public List<PhoneNumber> getPhoneList();
public int getPhoneCount();
// 列表大小的速记
// 作用:通过索引获取和设置列表的特定元素的getters和setters

public PhoneNumber getPhone(int index);
public Builder setPhone(int index, PhoneNumber value);

public Builder addPhone(PhoneNumber value);
// 将新元素添加到列表的末尾

public Builder addAllPhone(Iterable<PhoneNumber> value);
// 将一个装满元素的整个容器添加到列表中
public Builder clearPhone();

public Builder isInitialized() 
// 检查所有 required 字段 是否都已经被设置

public Builder toString() :
// 返回一个人类可读的消息表示(用于调试)

public Builder mergeFrom(Message other)
// 将 其他内容 合并到这个消息中,覆写单数的字段,附接重复的。

public Builder clear()
// 清空所有的元素为空状态。

具体使用

使用步骤如下:

  • 步骤1:通过 消息类的内部类Builder类 构造 消息构造器
  • 步骤2:通过 消息构造器 设置 消息字段的值
  • 步骤3:通过 消息构造器 创建 消息类 对象
  • 步骤4:序列化 / 反序列化 消息

具体使用如下:(注释非常清晰)

package com.chenj.protobuf;


import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;

public class TestProto {
    public static void main(String[] args) {
        // 步骤1:通过 消息类的内部类Builder类 构造 消息类的消息构造器
        Demo.Person.Builder personBuilder =  Demo.Person.newBuilder();

        // 步骤2:设置你想要设置的字段为你选择的值
        personBuilder.setName("Lisi");// 在定义.proto文件时,该字段的字段修饰符是required,所以必须赋值
        personBuilder.setId(123);// 在定义.proto文件时,该字段的字段修饰符是required,所以必须赋值
        personBuilder.setEmail("lisi.ho@foxmail.com"); // 在定义.proto文件时,该字段的字段修饰符是optional,所以可赋值 / 不赋值(不赋值时将使用默认值)

        Demo.Person.PhoneNumber.Builder phoneNumber =  Demo.Person.PhoneNumber.newBuilder();
        phoneNumber.setType( Demo.Person.PhoneType.HOME);// 直接采用枚举类型里的值进行赋值
        phoneNumber.setNumber("0157-23443276");
        // PhoneNumber消息是嵌套在Person消息里,可以理解为内部类
        // 所以创建对象时要通过外部类来创建

        // 步骤3:通过 消息构造器 创建 消息类 对象
        Demo.Person person = personBuilder.build();

        // 步骤4:序列化和反序列化消息(两种方式)

        /*方式1:直接 序列化 和 反序列化 消息 */
        // a.序列化
        byte[] byteArray1 = person.toByteArray();
        // 把 person消息类对象 序列化为 byte[]字节数组
        System.out.println(Arrays.toString(byteArray1));
        // 查看序列化后的字节流

        // b.反序列化
        try {

            Demo.Person person_Request = Demo.Person.parseFrom(byteArray1);
            // 当接收到字节数组byte[] 反序列化为 person消息类对象

            System.out.println(person_Request.getName());
            System.out.println(person_Request.getId());
            System.out.println(person_Request.getEmail());
            // 输出反序列化后的消息
        } catch (IOException e) {
            e.printStackTrace();
        }


        /*方式2:通过输入/ 输出流(如网络输出流) 序列化和反序列化消息 */
        // a.序列化
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        try {

            person.writeTo(output);
            // 将消息序列化 并写入 输出流(此处用 ByteArrayOutputStream 代替)

        } catch (IOException e) {
            e.printStackTrace();
        }

        byte[] byteArray = output.toByteArray();
        // 通过 输出流 转化成二进制字节流

        // b. 反序列化
        ByteArrayInputStream input = new ByteArrayInputStream(byteArray);
        // 通过 输入流 接收消息流(此处用 ByteArrayInputStream 代替)

        try {

            Demo.Person person_Request = Demo.Person.parseFrom(input);
            // 通过输入流 反序列化 消息

            System.out.println(person_Request.getName());
            System.out.println(person_Request.getId());
            System.out.println(person_Request.getEmail());
            // 输出消息
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

序列化原理解析

请记住Protocol Buffer三个关于数据存储 的重要结论:

  • 结论1: Protocol Buffer将 消息里的每个字段 进行编码后,再利用T - L - V 存储方式 进行数据的存储,最终得到的是一个 二进制字节流

T - L - V 的数据存储方式

Tag - Length - Value,标识 - 长度 - 字段值 存储方式

标识 - 长度 - 字段值 表示单个数据,最终将所有数据拼接成一个 字节流,从而 实现 数据存储 的功能

其中 Length可选存储,如 储存Varint编码数据就不需要存储Length

示意图

优点从上图可知,T - L - V存储方式的优点是

  1. 不需要分隔符 就能 分隔开字段,减少了 分隔符 的使用
  2. 各字段 存储得非常紧凑,存储空间利用率非常高
  3. 若 字段没有被设置字段值,那么该字段在序列化时的数据中是完全不存在的,即不需要编码
  • 结论2:Protocol Buffer对于不同数据类型 采用不同的 序列化方式(编码方式 & 数据存储方式),如下图: 

 

从上表可以看出:

  1. 对于存储Varint编码数据,就不需要存储字节长度 Length,所以实际上Protocol Buffer的存储方式是 T - V
  2. Protocol Buffer采用其他编码方式(如LENGTH_DELIMITED)则采用T - L - V
  • 结论3:因为 Protocol Buffer对于数据字段值的 独特编码方式 & T - L - V数据存储方式,使得 Protocol Buffer序列化后数据量体积如此小

总结

Protocol Buffer的序列化 & 反序列化简单 & 速度快的原因是:

a. 编码 / 解码 方式简单(只需要简单的数学运算 = 位移等等)

b. 采用 Protocol Buffer 自身的框架代码 和 编译器 共同完成

Protocol Buffer的数据压缩效果好(即序列化后的数据量体积小)的原因是:

a. 采用了独特的编码方式,如Varint、Zigzag编码方式等等

b. 采用T - L - V 的数据存储方式:减少了分隔符的使用 & 数据存储得紧凑

定义服务(Service)

如果想要将消息类型用在RPC(远程方法调用)系统中,可以在.proto文件中定义一个RPC服务接口,protocol buffer编译器将会根据所选择的不同语言生成服务接口代码及存根。如,想要定义一个RPC服务并具有一个方法,该方法能够接收 SearchRequest并返回一个SearchResponse,此时可以在.proto文件中进行如下定义:

service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

最直观的使用protocol buffer的RPC系统是gRPC,一个由谷歌开发的语言和平台中的开源的PRC系统,gRPC在使用protocl buffer时非常有效,如果使用特殊的protocol buffer插件可以直接为您从.proto文件中产生相关的RPC代码。

扩展阅读:

Protobuf2 语法指南

[译]Protobuf 语法指南

Protobuf3 语法指南

[转]Protobuf3 语法指南

几种序列化方式的性能比较

https://www.cjluo.top/2019/02/02/%E5%87%A0%E7%A7%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%B7%A5%E5%85%B7%E7%9A%84%E6%A8%AA%E5%90%91%E6%AF%94%E8%BE%83/

快速使用 gRPC 框架

安装protocbuf

protocbuf官网

https://developers.google.com/protocol-buffers

下载地址:

Release Protocol Buffers v3.19.1 · protocolbuffers/protobuf · GitHub

Windows下的安装很简单,只需到github上下载Windows平台对应的压缩包然后解压即可

下载后解压,会看到该bin文件夹下有一个名为protoc.exe的应用程序,这个就是本文要使用的编译器。

配置环境变量

变量名:PROTOCBUF_HOME

变量值:D:\protoc-3.19.1-win64

找到系统变量中的path变量,选中后点击编辑,在新建的内容中输入:%PROTOBUF_HOME%\bin

验证是否安装成功

出现如图所示

 

安装protocbuf插件

idea 建议下载一个 protobuf的插件, 可以有代码提示. 这里直接去pluging里搜就行了.

在idea的plugins中搜索proto,然后下载如下的插件就行(最多下载的那个),点击install,然后重启就可以。

gRPC项目构建

新建Maven项目并修改pom.xml,首先用IDEA新建一个maven项目

修改pom.xml,注意这个build标签和properties标签都是最顶级标签的直接子标签。 

<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.17.3:exe:${os.detected.classifier}</protocArtifact>
        <pluginId>grpc-java</pluginId>
        <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.42.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中添加一些,这些依赖是构造gRPC-java项目必须用到的(来自官方文档)

<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> <!-- necessary for Java 9+ -->
  <groupId>org.apache.tomcat</groupId>
  <artifactId>annotations-api</artifactId>
  <version>6.0.53</version>
  <scope>provided</scope>
</dependency>

添加.proto文件

proto文件用来描述rpc请求体、响应体、以及rpc提供的服务。通过插件可以根据.proto文件生成Java类。

这里面有个非常重要的点要注意,就是proto文件存放的位置。一定要在和src/main/java源文件目录同级的proto源文件目录才可以。如下图所示:

我们添加一个proto文件:helloworld.proto

syntax = "proto3"; // 协议版本

// 选项配置
option java_package = "com.chenj.protobuf";
option java_outer_classname = "RPCDateServiceApi";
option java_multiple_files = true;

// 定义包名
package com.chenj.protobuf;

// 服务接口.定义请求参数和相应结果	
service RPCDateService {
    rpc getDate (RPCDateRequest) returns (RPCDateResponse) {
    }
}

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

// 定义响应内容
message RPCDateResponse {
    string serverDate = 1;
}

根据.proto文件生成消息体类文件和XXXGrpc类文件

使用maven命令.

在第一步修改的pom.xml的路径下,首先执行

mvn protobuf:compile 生成消息体类文件

接着执行:

mvn protobuf:compile-custom 生成XXXGrpc类文件

使用maven插件, 编译.

第一个命令执行完. 在 target目录里找就行了. 第二个命令也是找就行了. 然后将生成的Java文件拷贝到你的目录里.就可以了 

编写接口实现类

package com.chenj;

import com.chenj.grpc.api.RPCDateRequest;
import com.chenj.grpc.api.RPCDateResponse;
import com.chenj.grpc.api.RPCDateServiceGrpc;
import io.grpc.stub.StreamObserver;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;

// RPCDateServiceGrpc.RPCDateServiceImplBase 这个就是接口.
// RPCDateServiceImpl 我们需要继承他的,实现方法回调
public class RPCDateServiceImpl extends RPCDateServiceGrpc.RPCDateServiceImplBase {
    @Override
    public void getDate(RPCDateRequest request, StreamObserver<RPCDateResponse> responseObserver) {
        //请求结果,我们定义的
        RPCDateResponse rpcDateResponse = null;
        //
        String userName = request.getUserName();
        String response = String.format("你好:%s,今天是%s.", userName,LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
        try {
            // 定义响应,是一个builder构造器.
            rpcDateResponse = RPCDateResponse.newBuilder()
                    .setServerDate(response)
                    .build();
            //int i = 10/0;
        } catch (Exception e) {
            responseObserver.onError(e);
        } finally {
            
            responseObserver.onNext(rpcDateResponse);
        }

        responseObserver.onCompleted();

    }
}

定义服务端

package com.chenj;

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

import java.io.IOException;

public class GRPCServer {
    private static final int port = 9999;

    public static void main(String[] args) throws IOException, InterruptedException {
        //设置service端口
        Server server = ServerBuilder.forPort(port)
                .addService(new RPCDateServiceImpl())
                .build().start();
        System.out.println(String.format("GRpc服务端启动成功, 端口号: %d.", port));

        server.awaitTermination();


    }
}

定义客户端

package com.chenj;

import com.chenj.grpc.api.RPCDateRequest;
import com.chenj.grpc.api.RPCDateResponse;
import com.chenj.grpc.api.RPCDateServiceGrpc;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

public class GRPCClient {
    private static final String host = "localhost";
    private static final int serverPort = 9999;
    public static void main(String[] args) {
        //1,拿到一个通信channel
        ManagedChannel channel = ManagedChannelBuilder.forAddress(host, serverPort).
                usePlaintext()//无需加密或认证
                .build();
        try {
            //2.拿到stub对象
            RPCDateServiceGrpc.RPCDateServiceBlockingStub rpcDateService  = RPCDateServiceGrpc.newBlockingStub(channel);
            RPCDateRequest rpcDateRequest = RPCDateRequest.newBuilder()
                    .setUserName("JACK")
                    .build();
            //3,请求
            RPCDateResponse rpcDateResponse = rpcDateService.getDate(rpcDateRequest);
            //4,输出结果
            System.out.println(rpcDateResponse.getServerDate());
        } finally {
            // 5.关闭channel, 释放资源.
            channel.shutdown();
        }

    }
}

然后先启动Server:

再启动Client:

可以看到执行成功。一个简单的gRPC helloworld工程就搭建好了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值