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协议”就指明了程序如何进行网络传输和序列化。
image-1657245019731

图1:完整RPC架构图

RPC核心功能

RPC的核心功能是指实现一个RPC最重要的功能模块,就是上图中的“RPC协议”部分:
image-1657245165707
一个RPC的核心功能主要有5个部分组成,分别是:客户端、客户端Stub、网络传输模块、服务端Stub、服务端等。

image-1657245254888

图2:RPC核心功能图

核心RPC框架的重要组成:
1.客户端(Client):服务调用方。
2.客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数打包成网络消息,再通过网路传输发送给服务端。
3.服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理。
4.服务端(Server):服务真正的提供者。
5.Network Service:底层传输,可以是TCO或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.服务消费方得到最终结果。

gRPC简介

官网
https://www.grpc.io/
gRPC 官方文档中文版
http://doc.oschina.net/grpc

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服务端来处理客户端调用。在客户端拥有一个存根能够像服务端一样的方法。

image-1657246731489

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

gRPC的功能优点:

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

gRPC的组成部分:

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

image-1657247174860

使用protobuf序列化协议

什么是ProtoBuf

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

在序列化数据的机制中,ProtoBuf是灵活、高效、自动化的,相对常见的XML、JSON,描述同样的信息,ProtoBuf序列化后数据量更小(在网络传输消耗的网络流俩更少)、序列化/反序列化速度更快、更简单。一旦定义了要处理的数据的数据结构之后,就可以利用ProtoBuf的代码生成工具生成相关代码。只需要使用Protbuf对数据结构进行一次描述,即可利用各种不同语言(proto3支持C++, Java, Python, Go, Ruby, Objective-C, C#)或从各种不同流中对你的结构化数据轻松读写。

使用ProtoBuf

ProtoBuf 协议的工作流程

image-1657247708058

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

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

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

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

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

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

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

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

ProtoBuf消息定义

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

// Java类

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

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

package protocobuff_Demo;
// 关注1:包名,防止不同 .proto 项目间命名 发生冲突

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

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

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

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phone = 4;
}

message AddressBook {
  repeated Person person = 1;
}

Proto3版本

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

option java_package = "com.giles.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;
}

1.消息对象
在ProtocolBuffer中:

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

image-1657250412309

2.字段

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

字段修饰符
作用:设置该字段解析时的规则
image-1657250533400

字段类型
字段类型主要有三类:

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

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

枚举类型
作用:为字段指定一个可能取值的字段集合,该字段只能从该指定的字段集合里取值
下面例子,电话号码可能是手机号、家庭座机号或工作电话号的其中一个,那么九江PhoneType定义为枚举类型,并将加入电话的集合(MOBILE、HOME、WORK)

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

// 枚举类型 定义
 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.giles.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

示意图
image-1657257628794

优点从上图可知,T - L - V存储方式的优点是
a. 不需要分隔符 就能 分隔开字段,减少了 分隔符 的使用
b. 各字段 存储得非常紧凑,存储空间利用率非常高
c. 若 字段没有被设置字段值,那么该字段在序列化时的数据中是完全不存在的,即不需要编码

  • 结论2:Protocol Buffer对于不同数据类型 采用不同的 序列化方式(编码方式 & 数据存储方式),如下图:

image-1657257719446

从上表可以看出:

  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 语法指南
https://colobu.com/2015/01/07/Protobuf-language-guide/
Protobuf3 语法指南
https://colobu.com/2017/03/16/Protobuf3-language-guide/
几种序列化方式的性能比较
https://www.cjluo.top/2019/02/02/几种序列化工具的横向比较/

快速使用gRPC框架

环境准备

安装protocbuf

protocbuf官网
https://developers.google.com/protocol-buffers
下载地址:
https://github.com/protocolbuffers/protobuf/releases/tag/v3.19.1
Windows下的安装很简单,只需到github上下载Windows平台对应的压缩包然后解压即可

image-1657258376314

下载后解压,会看到该bin文件夹下有一个名为protoc.exe的应用程序,这个就是本文要使用的编译器。
配置环境变量
变量名:PROTOCBUF_HOME
变量值:D:\protoc-3.19.1-win64

找到系统变量中的path变量,选中后点击编辑,在新建的内容中输入:%PROTOBUF_HOME%\bin
验证是否安装成功
出现如图所示

image

安装成功

安装protocbuf插件

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

image-1657258435989

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

gRPC项目构建

新建Maven项目并修改pom.xml

image-1657258506197
修改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源文件目录才可以。如下图所示:

image-1657258610934

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

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



// 选项配置

option java_package = "com.giles.protobuf";

option java_outer_classname = "RPCDateServiceApi";

option java_multiple_files = true;



// 定义包名

package com.giles.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 生成消息体类文件

image-1659582489503

接着执行:
mvn protobuf:compile-custom 生成XXXGrpc类文件

image-1659582522112

使用maven插件, 编译.

image-1659582550934

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

编写接口实现类
package com.giles.bean;

import com.giles.protobuf.RPCDateRequest;
import com.giles.protobuf.RPCDateResponse;
import com.giles.protobuf.RPCDateServiceGrpc;
import io.grpc.stub.StreamObserver;

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

/**
 * @author CYC
 * @date 2022/7/8
 * @time 14:26
 */

// 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.giles.server;

import com.giles.bean.RPCDateServiceImpl;
import io.grpc.Server;
import io.grpc.ServerBuilder;

import java.io.IOException;

/**
 * @author CYC
 * @date 2022/7/8
 * @time 14:28
 */
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.giles.cilent;

import com.giles.protobuf.RPCDateRequest;
import com.giles.protobuf.RPCDateResponse;
import com.giles.protobuf.RPCDateServiceGrpc;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;

/**
 * @author CYC
 * @date 2022/7/8
 * @time 14:30
 */
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("GILES")
                    .build();
            //3,请求
            RPCDateResponse rpcDateResponse = rpcDateService.getDate(rpcDateRequest);
            //4,输出结果
            System.out.println(rpcDateResponse.getServerDate());
        } finally {
            // 5.关闭channel, 释放资源.
            channel.shutdown();
        }

    }
}


然后先启动Server:

再启动Client:

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

未完待续~~~~

上一篇 下一篇