grpc(一)-简介与protobuf

时间:2021-7-3 作者:qvyue

1.简介

1.1 问题

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

grpc(一)-简介与protobuf
简单架构

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

1.2 方案

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

grpc(一)-简介与protobuf
image

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

  • nta-ip-service采用的是非java语言编写,而nta-log-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`,这种数据交换格式都是基于文本,因此在序列化和反序列化时,并不像二进制序列化那么快

    grpc(一)-简介与protobuf
    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

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

grpc(一)-简介与protobuf
image-20210531165018938

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


  1. 对比

    关于RESTProtoBuf对比如下:

    grpc(一)-简介与protobuf
    image-20210531211721372

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

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

grpc(一)-简介与protobuf
image-20210531173031159

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

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

grpc(一)-简介与protobuf
preview

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

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

grpc(一)-简介与protobuf
网上贴图,侵删

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

grpc(一)-简介与protobuf
网上贴图,侵删

==了解结束==

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如下:

4.0.0org.springframework.bootspring-boot-starter-parent2.5.0com.wangzhrpc-server0.0.1-SNAPSHOTrpc-serverDemo project for Spring Boot1.8org.springframework.bootspring-boot-starterorg.projectlomboklomboktrueorg.springframework.bootspring-boot-starter-testtestorg.springframework.bootspring-boot-maven-pluginorg.projectlomboklombok

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

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

2.2.2 语法

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

grpc(一)-简介与protobuf
image-20210601093810036

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

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


// 定义person 消息结构
message  {
   field_name_1 = tag_1;
   field_name_2 = tag_2;
   field_name_3 = tag_3;
   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如下

4.0.0org.springframework.bootspring-boot-starter-parent2.5.0com.wangzhrpc-server0.0.1-SNAPSHOTrpc-serverDemo project for Spring Boot1.81.6.13.3.0org.springframework.bootspring-boot-starterorg.projectlomboklomboktrueorg.springframework.bootspring-boot-starter-testtestio.grpcgrpc-netty${grpc.version}providedio.grpcgrpc-protobuf${grpc.version}providedio.grpcgrpc-stub${grpc.version}providedcom.google.protobufprotobuf-java${protobuf.version}kr.motd.mavenos-maven-plugin1.5.0.Finalorg.springframework.bootspring-boot-maven-pluginorg.projectlomboklombokorg.xolstice.maven.pluginsprotobuf-maven-plugin0.5.0com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}grpc-javaio.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}src/main/resources/protocompilecompile-custom

网上有很多通过安装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方式即可生成代码,如下:

grpc(一)-简介与protobuf
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);

}

测试结果如下:

grpc(一)-简介与protobuf
image-20210601163730338

自此protobuf就简单的了解完成

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:qvyue@qq.com 进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。