1.简介
1.1 问题
目前程序开发中,一个程序基本上是以各个服务组成,例如一个简单的系统,用户发起rest
请求,经过Nginx
反向代理,最终请求到达具体服务上,架构图如下:
但有时候服务间内部也需要通信,如上图所示,student-service
需要调用teacher-service
的IP库查询功能,此时就需要远程调用
1.2 方案
服务间的调用有很多种方案去解决,最暴力的一种就是将需要调用的代码直接拿过来粘贴到当前服务种即可,如下图所示:
这种方案虽然简单但是存在非常多的缺陷,如下:
- 当
teacher-service
采用的是非java
语言编写,而student-service
采用的是java
语言编写,这样代码直接复制过来是用不了的 - 假如两个服务非别采用不同类型的数据库,这样即使代码复制过来能用,也需要额外增加数据库的配置
- ...
综上所述在服务间的调用,代码直接拷贝过来这种方式在开发中并不可取,因此需要其他的方案:
- 通过
REST
方式或者REST
框架进行通信 - 借助其他的
RPC
框架通信,例如Spring Cloud
,Dubbo
,gRpc
等
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
-
rest
rest
数据交换格式采用``xml或者
json`,这种数据交换格式都是基于文本,因此在序列化和反序列化时,并不像二进制序列化那么快rest
传输协议采用的HTTP 1.1
,在传输上HTTP 2.0
传输快,数据加密使用的是SSL/TLS
注意:REST也可以采用
HTTP 2.0
,只不过一般都是采用HTTP 1.1所有的浏览器都支持REST
- gRPC
gRPC
则是google于2015年开源的一个RPC框架。它是基于protoBuf
和HTTP/2
实现,
相比较REST
,gRPC
有四种通信模型:
-
Unary
客户端发送单一请求消息,服务端回复一个单一响应
-
Client Streaming
客户端发送多个消息流,服务端回复一个单一响应
-
Server Streaming
客户端仅发送1条请求消息,并且服务器以多个重播流进行响应
-
Bidirectional Streaming
客户端和服务器将继续以任意顺序并行发送和接收多个消息。它非常灵活且无阻塞,这意味着在发送下一条消息之前,任何一方都无需等待响应
注意:浏览器不支持gRPC,如果想要支持gRPC 那么就需要借助
grpc-web
-
对比
关于
REST
和ProtoBuf
对比如下:
==额外了解 HTTP2 与 HTTP1.x区别 (start)==
HTTP 2 毋庸置疑 是比 HTTP 1.1 要快的,如下,加载同一张图片 对比
HTTP 2.0 的协议解析采用的是二进制,HTTP 1.X 的解析是基于文本,在速度上略胜一筹
HTTP 2.0 使用了请求头压缩,HTTP2.0使用encoder
来减少需要传输的header大小,通讯双方各自cache
一份header fields
表,既避免了重复header的传输,又减小了需要传输的大小
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 数据结构 较为复杂 比较简单 比较复杂 数据存储方式 文本 文本 二进制 数据存储大小 大 一般 小(比 xml
小2~3
倍)解析效率 慢 一般 快(比 xml
快20~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
插件,如下图
插件安装好了以后,就可以在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
关键字用来定义一个消息,消息名字需要满足驼峰命名规则message
是protobuf
中最基本得数据单元,类似于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
方式即可生成代码,如下:
生成的代码存在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);
}
测试结果如下:
自此protobuf就简单的了解完成