微服务通信方式——gRPC
微服务设计的原则是单一职责、轻量级通信、服务粒度适当,而说到服务通信,我们熟知的有MQ通信,还有REST、Dubbo和Thrift等,这次我来说说gRPC,
谷歌开发的一种数据交换格式,说不定哪天就需要上了呢,多学习总是件好事。
作者原创文章,谢绝一切转载,违者必究。
准备:
Idea2019.03/Gradle6.0.1/Maven3.6.3/JDK11.0.4/Proto3.0/gRPC1.29.0
难度: 新手--战士--老兵--大师
目标:
- 实现四种模式下的gRPC通信
说明:
为了遇见各种问题,同时保持时效性,我尽量使用最新的软件版本。源码地址,其中的day28:https://github.com/xiexiaobiao/dubbo-project
1 介绍
先引用一小段官方介绍:
Protocol Buffers - Google's data interchange format. Protocol Buffers (a.k.a., protobuf) are Google's language-neutral, platform-neutral, extensible
mechanism for serializing structured data.
Protocol Buffers,即protobuf,类比如JSON,XML等数据格式,实现了对结构化数据序列化时跨语言,跨平台,可扩展的机制。通过预定义.proto
文件,
来协商通信双方的数据交换格式、接口方法,这即是“Protocol”的原译。.proto
文件能编译为多种语言对应的代码,从而实现跨平台。
grpc使用http2.0作为通信方式,先说http2.0,它是对1.0的增强,而不是替代,特点:
- 二进制传输:相比1.0版的文本,2.0使用二进制帧传输数据;
- 多路复用:一个TCP连接中包含多个帧组成的流,再解析为不同的请求,从而可同时发送多个请求,避免1.0版的队头阻塞;
- 首部压缩:1.0版的Header部分信息很多,2.0使用HPACK压缩格式减少数据量;
- 服务端推送:服务端预先主动推送客户端必要的信息,从而减少延迟;
适用场景:定制化接口及数据交换格式,追求高性能,通信对宽带敏感
缺点:大多数HTTP Server尚不支持http2.0,Nginx目前只能将其降级为1.0处理;没有连接池、服务发现和负载均衡的实现;
2 实战步骤
A 普通模式
2.1 建立gradle类型项目,命名为GPRC-project
,我上传的Git代码中还包含了maven类型的项目,按照官方说明制作,运行方法略异,见其中.txt文件说明。
2.2 编写.proto文件,GPRC-project\src\main\proto\helloworld.proto
syntax = "proto3"; option java_multiple_files = true;
option java_package = "com.biao.grpc.helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW"; // The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
} // The request message containing the user's name.
message HelloRequest {
string name = 1;
} // The response message containing the greetings
message HelloReply {
string message = 1;
}
以上代码解析,几个java相关参数:
java_multiple_files
是否单独生成在.proto文件中定义的顶级message、enum、和service,否则只生成一个包装了内部类的外部类;java_package
编译生成的java类文件的包位置;java_outer_classname
外部类名称;java_generic_services是否生成各语言版本的基类(已过时);service Greeter {...}
定义服务和包含的方法;message
定义消息体结构,这里定义了一个 String类型,且只有一个字符串类型的value成员,该成员编号为1来代替名字,这也是protobuf体积小的原因之一,别的数据描述语言(json、xml)都是通过成员名字标识,而Portobuf通过唯一编号,只是不便于查阅。
2.3 编写GPRC-project\build.gradle
,包含依赖引入和gradle编译配置:
buildscript {
repositories {
mavenCentral()
maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/'}
}
dependencies {
// protobuf 编译插件,会在右侧gradle--other中,添加Proto相关的任务(共6个)
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.12'
}
} //plugins {
// id 'java'
// id 'com.google.protobuf'
// id 'com.google.protobuf' version '0.8.8'
//} apply plugin: 'java'
apply plugin: 'com.google.protobuf' group 'com.biao.grpc'
version '1.0-SNAPSHOT' sourceCompatibility = 1.8 repositories {
mavenCentral()
maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/'}
} dependencies { //这里必须引入lib目录的j2ee相关jar,否则即使每次手动加入jar依赖,但启动应用时gradle会reimport,
// 导致一直提示因少依赖而无法解析,这也是gradle引入第三方jar的方式
compile fileTree(dir: "lib", include: "*.jar") testCompile group: 'junit', name: 'junit', version: '4.12'
implementation 'io.grpc:grpc-netty-shaded:1.29.0'
implementation 'io.grpc:grpc-protobuf:1.29.0'
implementation 'io.grpc:grpc-stub:1.29.0'
} sourceSets {
main {
proto {
// .proto文件目录
srcDir 'src/main/proto'
}
java {
// include self written and generated code, 源代码生成到一个单独的目录
srcDirs 'src/main/java','generated-sources/main/java'
}
}
// remove the test configuration - at least in your example you don't have a special test proto file
/* test {
proto {
srcDir 'src/test/proto'
}
proto {
srcDir 'src/test/java'
}
}*/
} protobuf {
// Configure the protoc executable
protoc {
// Download from repositories ,从仓库下载,
artifact = "com.google.protobuf:protoc:3.11.0"
}
plugins {
grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.29.0'
}
}
//'src' 改为'generated-sources',则会变更.proto文件对应的java类文件生成目录
generateProtoTasks.generatedFilesBaseDir = 'src/main/java' generateProtoTasks {
// all() returns the collection of all protoc tasks
all()*.plugins {
grpc {}
} // In addition to all(),you may get the task collection by various
// criteria: // (Java only) returns tasks for a sourceSet
ofSourceSet('main')
}
}
以上代码解析:
- Moduleapply plugin: 'com.google.protobuf' 引入protobuf-gradle-plugin,作为protobuf 编译插件,会在右侧gradle--other中,自动添加proto相关的任务(共6个)
- compile fileTree(dir: "lib", include: "*.jar") 这里必须引入lib目录的j2ee相关jar,否则即使每次手动加入jar依赖,但启动应用时gradle会reimport,导致一直提示因少依赖而无法解析,这也是gradle引入第三方jar的正确方式
- implementation 'io.grpc:grpc-protobuf:1.29.0',gradle新版语法,implementation 仅仅对当前的Module提供接口,对外隐藏不必要的接口,而compile(新版升级为 api )依赖的库将会完全参与编译和打包,
- protobuf {...}中则声明protoc-gen-grpc-java插件来源的.proto文件源目录及生成目标目录,
2.4 运行右侧 task :gradle --> other --> generateProto
,则自动生成类文件和接口文件(XXXGrpc
),并且很贴心的是,如果原.proto
文件有注释,
生成的文件中会自动带上原注释内容。可以看到,helloworld.proto 和 stream.proto 生成的对应的文件,前者为 6 个,后者为 1 个,因java_multiple_files参数不同。
生成文件后,将文件移动到src/main/java对应的包下面,并将build.gradle与自动生成文件的部分注释掉,否则启动应用时,又会自动生成,导致IDE提示类重复。
2.5 看下源码,包com.biao.grpc.helloworld
下面生成了对应于helloworld.proto
的类和接口,包括服务、请求消息结构体和响应消息结构体。
com.biao.grpc.helloworld.GreeterGrpc
中 getSayHelloMethod
方法即约定了RPC的方法、请求/响应数据类型,并获取方法全名:
@io.grpc.stub.annotations.RpcMethod(
fullMethodName = SERVICE_NAME + '/' + "SayHello",
requestType = com.biao.grpc.helloworld.HelloRequest.class,
responseType = com.biao.grpc.helloworld.HelloReply.class,
methodType = io.grpc.MethodDescriptor.MethodType.UNARY)
public static io.grpc.MethodDescriptor<com.biao.grpc.helloworld.HelloRequest,
com.biao.grpc.helloworld.HelloReply> getSayHelloMethod() {
io.grpc.MethodDescriptor<com.biao.grpc.helloworld.HelloRequest, com.biao.grpc.helloworld.HelloReply> getSayHelloMethod;
if ((getSayHelloMethod = GreeterGrpc.getSayHelloMethod) == null) {
synchronized (GreeterGrpc.class) {
if ((getSayHelloMethod = GreeterGrpc.getSayHelloMethod) == null) {
GreeterGrpc.getSayHelloMethod = getSayHelloMethod =
io.grpc.MethodDescriptor.<com.biao.grpc.helloworld.HelloRequest, com.biao.grpc.helloworld.HelloReply>newBuilder()
.setType(io.grpc.MethodDescriptor.MethodType.UNARY)
.setFullMethodName(generateFullMethodName(SERVICE_NAME, "SayHello"))
.setSampledToLocalTracing(true)
.setRequestMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
com.biao.grpc.helloworld.HelloRequest.getDefaultInstance()))
.setResponseMarshaller(io.grpc.protobuf.ProtoUtils.marshaller(
com.biao.grpc.helloworld.HelloReply.getDefaultInstance()))
.setSchemaDescriptor(new GreeterMethodDescriptorSupplier("SayHello"))
.build();
}
}
}
return getSayHelloMethod;
}
2.6 建立server端:
public class HelloWorld_Server {
private static final Logger logger = Logger.getLogger(HelloWorld_Server.class.getName()); private int port = 50051;
private Server server; private void start() throws IOException{
server = ServerBuilder.forPort(port)
.addService(new GreeterImpl())
.build()
.start();
logger.info("Server started, listening on "+ port); Runtime.getRuntime().addShutdownHook(new Thread(){ @Override
public void run(){ System.err.println("*** shutting down gRPC server since JVM is shutting down");
HelloWorld_Server.this.stop();
System.err.println("*** server shut down");
}
});
} private void stop(){
if (server != null){
server.shutdown();
}
} // block 一直到退出程序
private void blockUntilShutdown() throws InterruptedException {
if (server != null){
server.awaitTermination();
}
} public static void main(String[] args) throws IOException, InterruptedException { final HelloWorld_Server server = new HelloWorld_Server();
server.start();
server.blockUntilShutdown();
} // 实现 定义一个实现服务接口的类
private class GreeterImpl extends com.biao.grpc.helloworld.GreeterGrpc.GreeterImplBase { @Override
public void sayHello(com.biao.grpc.helloworld.HelloRequest req,
StreamObserver<com.biao.grpc.helloworld.HelloReply> responseObserver){
com.biao.grpc.helloworld.HelloReply reply = com.biao.grpc.helloworld.HelloReply.
newBuilder()
.setMessage(("Hello "+req.getName()))
.build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
System.out.println("Message from gRPC-Client:" + req.getName());
}
}
}
以上代码解析:通过GreeterImpl扩展GreeterGrpc.GreeterImplBase具体实现了gRPC服务的方法,作为服务端响应请求的业务逻辑。
2.7 建立client端:
public class HelloWorld_Client {
private final ManagedChannel channel;
private final com.biao.grpc.helloworld.GreeterGrpc.GreeterBlockingStub blockingStub;
private static final Logger logger = Logger.getLogger(HelloWorld_Client.class.getName()); public HelloWorld_Client(String host,int port){
channel = ManagedChannelBuilder.forAddress(host,port)
.usePlaintext()
.build(); blockingStub = com.biao.grpc.helloworld.GreeterGrpc.newBlockingStub(channel);
} public void shutdown() throws InterruptedException {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
} public void greet(String name){
com.biao.grpc.helloworld.HelloRequest request = com.biao.grpc.helloworld.HelloRequest
.newBuilder()
.setName(name)
.build();
com.biao.grpc.helloworld.HelloReply response;
try{
response = blockingStub.sayHello(request);
} catch (StatusRuntimeException e)
{
logger.log(Level.WARNING, "RPC failed: {0}", e.getStatus());
return;
}
logger.info("Message from gRPC-Server: "+response.getMessage());
} public static void main(String[] args) throws InterruptedException {
HelloWorld_Client client = new HelloWorld_Client("127.0.0.1",50051);
try{
String user = "world";
if (args.length > 0){
user = args[0];
}
client.greet(user);
}finally {
client.shutdown();
}
}
}
以上代码解析:在greet方法中,引用HelloRequest和HelloReply,并发起gRPC业务请求。
2.8 先运行com.biao.grpc.helloworld.HelloWorld_Server
,再运行com.biao.grpc.helloworld.HelloWorld_Client
, 输出以下为成功:
B 流模式
gRPC有四种通信模式:
- 普通模式:一次请求对应一次响应,和普通方法请求一样;
- 客户端流模式:客户端使用流模式传入多个请求对象,服务端返回一个响应结果;
- 服务端流模式:一个请求对象,服务端使用流模式传回多个结果对象;
- 双向流模式:客户端流式和服务端流式组合;
前面有说过,gRPC使用http/2通信,数据传输使用二进制帧,帧是HTTP2.0通信的最小单位,而消息由一或多个帧组成,流是比消息更大的通讯单位,
是TCP连接中的一个虚拟通道。每个数据流以消息的形式发送,消息中的帧可以乱序发送,然后再根据每个帧首部的流标识符重新组装为流。
前面的helloworld算第1种,我这里写了后 3 种模式,根据stream.proto开发,使用同一个server端:com.biao.grpc.stream.StreamServer
,其中实现
了客户端流模式、服务端流模式和双向流模式3种通信模式的具体方法实现,
public class StreamServer { private static int port = 8883;
private static io.grpc.Server server; public void run() {
ServiceImpl serviceImpl = new ServiceImpl();
server = io.grpc.ServerBuilder.forPort(port).addService(serviceImpl).build();
try {
server.start();
System.out.println("Server start success on port:" + port);
server.awaitTermination();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
} // 实现 定义一个实现服务接口的类
private static class ServiceImpl extends StreamServiceGrpc.StreamServiceImplBase { @Override
public void serverSideStreamFun(Stream.RequestData request, StreamObserver<Stream.ResponseData> responseObserver) {
// TODO Auto-generated method stub
System.out.println("请求参数:" + request.getText());
for (int i = 0; i < 10; i++) {
responseObserver.onNext(Stream.ResponseData.newBuilder()
.setText("你好" + i)
.build());
}
responseObserver.onCompleted();
} @Override
public StreamObserver<Stream.RequestData> clientSideStreamFun(StreamObserver<Stream.ResponseData> responseObserver) {
// TODO Auto-generated method stub
return new StreamObserver<Stream.RequestData>() {
private Stream.ResponseData.Builder builder = Stream.ResponseData.newBuilder(); @Override
public void onNext(Stream.RequestData value) {
// TODO Auto-generated method stub
System.out.println("请求参数:" + value.getText()); } @Override
public void onError(Throwable t) {
// TODO Auto-generated method stub } @Override
public void onCompleted() {
// TODO Auto-generated method stub
builder.setText("数据接收完成");
responseObserver.onNext(builder.build());
responseObserver.onCompleted();
}
};
} @Override
public StreamObserver<Stream.RequestData> twoWayStreamFun(StreamObserver<Stream.ResponseData> responseObserver) {
// TODO Auto-generated method stub
return new StreamObserver<Stream.RequestData>() { @Override
public void onNext(Stream.RequestData value) {
// TODO Auto-generated method stub
System.out.println("请求参数:" + value.getText());
responseObserver.onNext(Stream.ResponseData.newBuilder()
.setText(value.getText() + ",欢迎你的加入")
.build());
} @Override
public void onError(Throwable t) {
// TODO Auto-generated method stub
t.printStackTrace();
} @Override
public void onCompleted() {
// TODO Auto-generated method stub
responseObserver.onCompleted();
}
};
}
} public static void main(String[] args) {
StreamServer server = new StreamServer();
server.run();
} }
另外我写了3个client端,代码分析,略!运行后即可看到效果,我这里给个双向流的结果例子:
附:手动编译
为了加强动手能力,我这里也做个手动编译生成java代码的步骤:
Java代码生成编译器下载: https://repo.maven.apache.org/maven2/io/grpc/protoc-gen-grpc-java/
下载安装protobuf,https://github.com/protocolbuffers/protobuf/releases?after=v3.0.0-alpha-4
To install protobuf, you need to install the protocol compiler (used to compile .proto files) and the protobuf runtime for your chosen programming language.
要使用protobuf,需先安装协议编译器(protocol compiler),用于编译.proto文件,并且作为protobuf的运行时环境。其实安装protobuf等价于安装其编译环境protoc。
环境变量设置,将protoc的解压目录添加到Path下:
CMD下使用protoc --version
命令输出如下即为成功:
手动编译:进入 .proto
文件所在目录, 使用protoc.exe生成消息结构体,下图中标号 2 :
protoc <待编译文件> --java_out=<输出文件保存路径>
使用protoc-gen-grpc-java生成服务接口:下图中标号 3 :
protoc *.proto --plugin=protoc-gen-grpc-java=C:\protobuf-3.0-beta\protoc-gen-grpc-java-1.9.1-windows-x86_64.exe --grpc-java_out=./
全文完!
我的其他文章:
- 1 分布式任务调度系统
- 2 Dubbo学习系列之十八(Skywalking服务跟踪)
- 3 Spring优雅整合Redis缓存
- 4 SOFARPC模式下的Consul注册中心
- 5 八种控制线程顺序的方法
只写原创,敬请关注
微服务通信方式——gRPC的更多相关文章
- .NET Core 微服务之grpc 初体验(干货)
Grpc介绍 GitHub: https://github.com/grpc/grpc gRPC是一个高性能.通用的开源RPC框架,其由Google主要面向移动应用开发并基于HTTP/2协议标准而设计 ...
- .NET Core微服务开发服务间调用篇-GRPC
在单体应用中,相互调用都是在一个进程内部调用,也就是说调用发生在本机内部,因此也被叫做本地方法调用:在微服务中,服务之间调用就变得比较复杂,需要跨网络调用,他们之间的调用相对于与本地方法调用,可称为远 ...
- go微服务系列(四) - gRPC入门
1. 前言 2. gRPC与Protobuf简介 3. 安装 4. 中间文件演示 4.1 编写中间文件 4.2 运行protoc命令编译成go中间文件 5. 创建gRPC服务端 5.1 新建Produ ...
- 如何基于gRPC沟通微服务框架
本文我们来讲解一下如何使用 gRPC构建微服务,gRPC是一个开源框架,可用于构建可扩展且高性能的微服务并创建服务之间的通信. 背景 随着企业越来越多地转向微服务,对构建这些微服务的低延迟和可扩展框架 ...
- 网易蜂巢微服务架构:用RabbitMQ实现轻量级通信
本次分享内容由三个部分组成: 微服务架构与MQ RabbitMQ场景分析与优化 RabbitMQ在网易蜂巢中的应用和案例分享 1微服务架构与MQ 微服务架构是一种架构模式,它将单体应用划分成一组微小的 ...
- Go微服务全链路跟踪详解
在微服务架构中,调用链是漫长而复杂的,要了解其中的每个环节及其性能,你需要全链路跟踪. 它的原理很简单,你可以在每个请求开始时生成一个唯一的ID,并将其传递到整个调用链. 该ID称为Correlati ...
- Golang微服务实践
背景 在之前的文章<漫谈微服务>我已经简单的介绍过微服务,微服务特性是轻量级跨平台和跨语言的服务,也列举了比较了集中微服务通信的手段的利弊,本文将通过RPC通信的方式实现一个增删查Redi ...
- 【微服务落地】服务间通信方式: gRPC的入门
gRPC是什么 官方介绍: https://grpc.io/docs/what-is-grpc/introduction/ "A high-performance, open-source ...
- Grpc微服务从零入门
快速入门 安装 JDK 毫无疑问,要想玩Java,就必须得先装Java JDK,目前公司主要使用的是Oracle JDK 8,安装完成后要配置环境才能正常使用,真蠢,不过也就那么一下下,认了吧.配置方 ...
随机推荐
- 针对Kafka的centos系统参数优化
TCP网络优化 sudo vim /etc/sysctl.conf vm.max_map_count=655360net.core.rmem_default=262144net.core.rmem_m ...
- P1004 方格取数(四维dp)
P1004 方格取数 思路如下 这题是看洛谷大佬的思路才写出来的,所以我会把大佬的思路展示如下: 1⃣️:我们可以找到一个叫思维dp的东西,dp[i][j][k][l],其中前两维表示一个人从原点出发 ...
- STS——BootDash的报错及解决
首先先吐槽下自己,昨晚查了两小时bug,原因在于模板引擎thymeleaf编写时, 调用th:src属性时,其中的@{}表达式,花括号忘记括回来了. 这里给我的教训就是,写代码细致一点,检查的时候优 ...
- Hadoop(十):本地IDEA链接远程Hadoop
本文使用的Hadoop为2.7.7,版本如果不同要下载相应版本的文件 配置本地的Hadoop库(不需完整安装,但是要有环境支持) 下载文件 https://github.com/speedAngel/ ...
- Vulnhub DC-8靶机渗透
信息搜集 nmap -sP 192.168.146.0/24 #主机发现 nmap -A 192.168.146.146 #Enable OS detection, version detection ...
- Linux强大屏幕截图方法,理论能截取任何图形界面,包括登录界面
众所周知,屏幕截图可以使用“Print Screen”按键,但是,此按键的响应是靠系统的后台服务实现的,Linux在某些场景下,是不响应此按键的. 这里介绍一种更强大的截图方法,它是靠转储X图形环境的 ...
- javascript入门 之 zTree(十四 增删查改)(一)
<!DOCTYPE html> <HTML> <HEAD> <TITLE> ZTREE DEMO - beforeEditName / beforeRe ...
- 写日志 使用nnlog
import nnlog log=nnlog.Logger(r"/学习/接口自动化/BestTest/10.26/book_server.log",backCount=5,leve ...
- scp 使用方法
scp就是secure copy,一个在linux下用来进行远程拷贝文件的命令.有时我们需要获得远程服务器上的某个文件,该服务器既没有配置ftp服务器,也没有做共享,无法通过常规途径获得文件时,只需要 ...
- 事务的传播属性及隔离级别 Spring
事务的传播属性(Propagation) REQUIRED ,这个是默认的属性 Support a current transaction, create a new one if none exis ...