框架篇-grpc(一)-grpc简介与protobuf使用

1.简介

1.1 问题

目前程序开发中,一个程序基本上是以各个服务组成,例如一个简单的系统,用户发起rest请求,经过Nginx反向代理,最终请求到达具体服务上,架构图如下:

简单架构

但有时候服务间内部也需要通信,如上图所示,student-service需要调用teacher-service的IP库查询功能,此时就需要远程调用

1.2 方案

服务间的调用有很多种方案去解决,最暴力的一种就是将需要调用的代码直接拿过来粘贴到当前服务种即可,如下图所示:

image-20210611150034752

这种方案虽然简单但是存在非常多的缺陷,如下:

  • teacher-service采用的是非java语言编写,而student-service采用的是java语言编写,这样代码直接复制过来是用不了的
  • 假如两个服务非别采用不同类型的数据库,这样即使代码复制过来能用,也需要额外增加数据库的配置
  • ...

综上所述在服务间的调用,代码直接拷贝过来这种方式在开发中并不可取,因此需要其他的方案:

  • 通过REST方式或者REST框架进行通信
  • 借助其他的RPC框架通信,例如Spring CloudDubbogRpc

1.3 对比

1.3.1 cloud & dubbo

cloud

spring cloud是一整套服务通信方案,包括注册中心,服务发现,服务容灾

利用spring cloud方式进行服务间通信,需要搭建额外的注册中心,例如zookeeper,

nacos,eureka,consule等,但如果只是单纯的是服务间的通信,就没有必要去采用这一整套方案

dubbo

dubbo原理与spring cloud原理差不多,也是需要依赖于注册中心zookeeper,同样的,对于开发好的服务来说,也没有必要去采用这种一整套方案

1.3.2 rest & grpc

  1. rest

    rest 数据交换格式采用``xml或者json`,这种数据交换格式都是基于文本,因此在序列化和反序列化时,并不像二进制序列化那么快

    image-20210531203848055

    rest传输协议采用的HTTP 1.1,在传输上HTTP 2.0传输快,数据加密使用的是SSL/TLS

    注意:REST也可以采用HTTP 2.0,只不过一般都是采用HTTP 1.1

    所有的浏览器都支持REST


  1. gRPC

gRPC则是google于2015年开源的一个RPC框架。它是基于protoBufHTTP/2实现,

相比较REST,gRPC有四种通信模型:

  • Unary

    客户端发送单一请求消息,服务端回复一个单一响应

  • Client Streaming

    客户端发送多个消息流,服务端回复一个单一响应

  • Server Streaming

    客户端仅发送1条请求消息,并且服务器以多个重播流进行响应

  • Bidirectional Streaming

    客户端和服务器将继续以任意顺序并行发送和接收多个消息。它非常灵活且无阻塞,这意味着在发送下一条消息之前,任何一方都无需等待响应

image-20210531165018938

注意:浏览器不支持gRPC,如果想要支持gRPC 那么就需要借助 grpc-web


  1. 对比

    关于RESTProtoBuf对比如下:

    image-20210531211721372

==额外了解 HTTP2 与 HTTP1.x区别 (start)==

HTTP 2 毋庸置疑 是比 HTTP 1.1 要快的,如下,加载同一张图片 对比

image-20210531173031159

HTTP 2.0 的协议解析采用的是二进制,HTTP 1.X 的解析是基于文本,在速度上略胜一筹

HTTP 2.0 使用了请求头压缩,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小

preview

HTTP1.x的header带有大量信息,而且每次都要重复发送

多路复用,一个request对应一个id,这样一个连接上可以有多个request,每个连接的request可以随机的混杂在一起,接收方可以根据request的 id将request再归属到各自不同的服务端请求里面,从而达到复用

网上贴图,侵删

服务端推送,服务器可以对客户端的一个请求发送多个响应

网上贴图,侵删

==了解结束==

2. ProtoBuf

从上文知道,grpc数据交换格式或者说数据载荷采用的是protobuf,因此在学习grpc之前先学习一下protobuf

2.1 介绍

protobuf(Protocol Buffers)是谷歌推出的一个与语言,平台无关的,高效,可扩展的序列化结构数据的方法,类似于json,一般用于通信协议,数据存储等

在其官网上对该东西有着详细的说明,大体如下:

  • 与平台,语言无关,支持多种语言,例如java,c++,c#python,go等多种语言

  • 高效,简单类比xml,json如下:

    对比 xml json protobuf
    数据结构 较为复杂 比较简单 比较复杂
    数据存储方式 文本 文本 二进制
    数据存储大小 一般 小(比xml2~3倍)
    解析效率 一般 快(比 xml20~100倍)
    学习成本 简单 简单 简单
  • 扩展性,兼容性好,更新数据格式,不会影响和破坏原有的程序

  • 当然protobuf更加关注数据的序列化,关注效率,空间,速度,因此在数据的可读性上和语义表达能力上并不很突出

基于上述原因所以在grpc中会选择protobuf作为数据载荷,而不是json或者xml

2.2 使用

2.2.1 准备

使用idea创建一个springboot项目,名字为rpc-server,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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.wangzh</groupId>
    <artifactId>rpc-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>rpc-server</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

src/main/resources下新建proto/hello.proto

注意:protobuf 文件后缀名都是.protobuf

2.2.2 语法

如果想要在编写时,进行语法提示或者高亮,可以在idea中安装protobuf插件,如下图

image-20210601093810036

插件安装好了以后,就可以在hello.proto里面去撰写protobuf代码,语法如下

// 语法版本 protobuf 编译器默认时 prot
// 如果想要使用proto3 在第一行声明该语法版本
// 如果第一次学直接抛弃 proto2 使用proto3
syntax="proto3";


// 定义person 消息结构
message <MessageName> {
  <data_type> field_name_1 = tag_1;
  <data_type> field_name_2 = tag_2;
  <data_type> field_name_3 = tag_3;
  <data_type> field_name_4 = tag_4;
}

上述代码中,具体解释如下:

  • message 关键字用来定义一个消息,消息名字需要满足驼峰命名规则

    messageprotobuf中最基本得数据单元,类似于java中的类

    message里面还可以嵌套message

  • data_type 用来定义属性的数据类型,在protobuf中数据类型如下:

    data_type 解释 java类型
    string 字符串,符串必须是UTF-8编码或者7-bit ASCII编码 String
    bool 布尔类型 boolean
    bytes 可能包含任意顺序的字节数据 ByteString
    float 单精度浮点型 float
    double 双精度浮点型 double
    int32 使用可变长编码方式。编码负数时不够高效——如果你的字段可能含有负数,那么请使用sint32 int
    sint32 使用可变长编码方式。有符号的整型值,编码时比通常的int32高效 int
    int64 使用可变长编码方式。编码负数时不够高效—如果你的字段可能含有负数,那么请使用sint64 long
    sint64 使用可变长编码方式。有符号的整型值, 编码时比通常的int64高效 long
    uint32 使用可变长编码 不带符号 int
    uint64 使用可变长编码 不带符号 long
    fixed32 总是4个字节。如果数值总是比总是比2^28大的话,这个类型会比uint32高效 int
    fixed64 总是8个字节。如果数值总是比总是比2^56大的话,这个类型会比uint32高效 long
    sfixed32 总是4个字节 int
    sfixed64 总是8个字节 long

    除了这些数据类型以外,还有其他的数据类型,例如枚举,消息等类型,后面会去再探讨

  • field_name 属性名 多个单词之间使用下划线隔开

  • tag 标签,每个属性的标签都是唯一的,到时候protobuf会根据标签去进行序列化

    标签是一个任意整数,不能重复,且数值范围在 1 ~ 2^29 - 1

    且不能使用[19000 - 19999]之间的数字,这些数字保留给了protobuf内部实现

    注意: 1-15只占了一个字节,16-2047占用了两个字节

2.2.3 案例

案例中主要分为以下几大类:

  • 基础案例
  • 枚举案例
  • 消息案例(同文件)
  • 消息案例(不同文件)
  • 嵌套案例
  • 补充案例

下面是其具体详情


基础案例

有了上述的例子,接下来我们来撰写一个Person消息,代码如下:

/*
 * 语法版本 protobuf 编译器默认时 proto2
 * 如果想要使用proto3 在第一行声明该语法版本
 * 如果第一次学直接抛弃 proto2 使用proto3
 */
syntax="proto3";

/*
 * 定义person 消息结构
 */
message Person {
  uint32 id = 1;
  string name = 2;
  uint32 age = 3;
  double salary = 4;
}

当然也可以将多个消息定义在同一个.proto文件中,如下:

/*
 * 语法版本 protobuf 编译器默认时 proto2
 * 如果想要使用proto3 在第一行声明该语法版本
 * 如果第一次学直接抛弃 proto2 使用proto3
 */
syntax="proto3";

/*
 * 定义person 消息结构
 */
message Person {
  uint32 id = 1;
  string name = 2;
  uint32 age = 3;
  double salary = 4;
}

message Car {
  string name = 1;
  string color = 2;
  double price = 3;
}

枚举案例

除了上述描述的数据类型,还可以定义枚举类型,新建enums.proto如下:

syntax="proto3";

message Person {
  /*
   * id
   */
  sint32 id = 1;
  string name = 2;

  /*
   * 定义枚举类型,枚举第一个值必须为 0,而且为 0 的元素一定是第一个元素
   */
  enum Gender {
    MALE = 0;
    FEMALE = 1;
  }
  Gender gender = 3;
}

上述案例中枚举定义在message内部,当然也可以定义在外部,被不同的message所使用,如下:

syntax="proto3";

message Student {
  /*
   * id
   */
  sint32 id = 1;
  string name = 2;

  /*
   * 定义枚举类型,枚举第一个值必须为 0,而且为 0 的元素一定是第一个元素
   */
  enum Gender {
    MALE = 0;
    FEMALE = 1;
  }

  Gender gender = 3;
  Pet pet = 4;
}

enum Pet {
  CAT = 0;
  DOG = 1;
}

message Teacher {
  Pet pet = 1;
}

消息案例(同文本)

数据类型除了枚举以外,还可以是消息类型,如下:

syntax="proto3";

message Student {
  /*
   * id
   */
  sint32 id = 1;
  string name = 2;

  /*
   * 定义枚举类型,枚举第一个值必须为 0,而且为 0 的元素一定是第一个元素
   */
  enum Gender {
    MALE = 0;
    FEMALE = 1;
  }

  Gender gender = 3;
  Pet pet = 4;
}

enum Pet {
  CAT = 0;
  DOG = 1;
}

message Teacher {
  Pet pet = 1;
  Student student = 2;
}

消息案例(不同文件)

如果是在不同文件中,则需要导入进来,才能定义,如下:

/*
 * 语法版本 protobuf 编译器默认时 proto2
 * 如果想要使用proto3 在第一行声明该语法版本
 * 如果第一次学直接抛弃 proto2 使用proto3
 */
syntax="proto3";

import "proto/enums.proto";

/*
 * 定义person 消息结构
 */
message Person {
  uint32 id = 1;
  string name = 2;
  uint32 age = 3;
  double salary = 4;
  Student student = 5;
}

message Car {
  string name = 1;
  string color = 2;
  double price = 3;
}

当然如果是两个文件中消息类型一样,则会报错,就好比java中类名一摸一样,会报错道理是一样的,因此为了区分可以给每个.proto文件增加包,如下:

syntax="proto3";

package com.wangzh;

// 导入其他的message
import "proto/enums.proto";


/*
 * 定义person 消息结构
 */
message Person {
  uint32 id = 1;
  string name = 2;
  uint32 age = 3;
  double salary = 4;
  Student student = 5;
}

message Car {
  string name = 1;
  string color = 2;
  double price = 3;
  message Engine {
    string brand = 1;
  }
}

建议以后每次都把包携带上


嵌套案例

消息之前还可以相互嵌套,如下:

message Car {
  string name = 1;
  string color = 2;
  double price = 3;
  message Engine {
    string brand = 1;
  }
}

补充案例

经过上述操作后,基本上明白了protobuf的基本写法,除了上面写法以外,protobuf还有限定符,如下:

  • required

    必须的,即客户端和发送端都必须处理这个字段,即数据发送之前需要设置该字段,数据接收之后也需要处理该字段

    注意: proto3 已经移除了这个字段

  • optional

    这是一个可选字段,对于发送者来说,可以选择设置或者不设置该字段的值。

    对于接收方来说,如果能够识别可选字段,那就处理,无法识别则不处理。

    message Person {
      uint32 id = 1;
      string name = 2;
      optional uint32 age = 3;
      Student student = 5;
    }
    
  • repeated

    表示字段可以包含0~N个元素,特性与Optional一样,但是一次可以包含多个值,类似于数组

    message Person {
      uint32 id = 1;
      string name = 2;
      repeated double salary = 4;
      Student student = 5;
    }
    

2.3 生成

上述基本上了解了protobuf文件的基本写法,接下来了解其代码生成,生成的代码会去序列化和反序列化protobuf

2.3.1 依赖

修改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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.wangzh</groupId>
    <artifactId>rpc-server</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>rpc-server</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <grpc.version>1.6.1</grpc.version>
        <protobuf.version>3.3.0</protobuf.version>
    </properties>


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

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-netty</artifactId>
            <version>${grpc.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-protobuf</artifactId>
            <version>${grpc.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-stub</artifactId>
            <version>${grpc.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>${protobuf.version}</version>
        </dependency>

    </dependencies>


    <build>
        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.5.0.Final</version>
            </extension>
        </extensions>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
            <!--代码生成插件-->
                <plugin>
                    <groupId>org.xolstice.maven.plugins</groupId>
                    <artifactId>protobuf-maven-plugin</artifactId>
                    <version>0.5.0</version>
                    <configuration>
                        <protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
                        <pluginId>grpc-java</pluginId>
                        <pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
                       
 <!-- protobuf 文件位置 -->                       <protoSourceRoot>src/main/resources/proto</protoSourceRoot>
                    </configuration>
                    <executions>
                        <execution>
                            <goals>
                                <goal>compile</goal>
                                <goal>compile-custom</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
        </plugins>
    </build>

</project>

网上有很多通过安装protobuf环境方式来生成代码,但是grpc官方提供了一种更加优雅的方式生成代码,如上

2.3.2 生成

删除之前写的文件,新建一个新的文件,hello.proto,内容如下:

syntax="proto3";

package com.wangzh;
// 生成java代码的包名
option java_package = "com.wangzh.rpcserver.proto";

// 是用一个class文件来定义所有的message对应的java类
option java_outer_classname = "PersonModel";

// 是否如果是true,那么每一个message文件都会有一个单独的class文件 否则,message全部定义在outerclass文件里
// option java_multiple_files = true;

message Person {
  uint32 id = 1;
  string name = 2;
  uint32 age = 3;
}

输入mvn protobuf:compile方式即可生成代码,如下:

image-20210601162801298

生成的代码存在target目录中

2.4 测试

在测试类中测试生成的代码,测试代码如下:

@Test
void contextLoads() throws InvalidProtocolBufferException {
    // 构建build对象
    PersonModel.Person.Builder builder = PersonModel.Person.newBuilder();
    builder.setId(1);
    builder.setAge(15);
    builder.setName("lisi");

    // 构建person对象
    PersonModel.Person person = builder.build();
    System.out.println(person);

    // 序列化
    byte[] bytes = person.toByteArray();
    System.out.println(String.format("字节序列:%s",Arrays.toString(bytes)));


    // 反序列化
    person = PersonModel.Person.parseFrom(bytes);
    System.out.println(person);

}

测试结果如下:

image-20210601163730338

自此protobuf就简单的了解完成

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

推荐阅读更多精彩内容