微服务设计的原则是单一职责、轻量级通信、服务粒度适当,而说到服务通信,我们熟知的有MQ通信,还有REST、Dubbo和Thrift等,这次我来说说gRPC,

谷歌开发的一种数据交换格式,说不定哪天就需要上了呢,多学习总是件好事。

作者原创文章,谢绝一切转载,违者必究。

准备:

Idea2019.03/Gradle6.0.1/Maven3.6.3/JDK11.0.4/Proto3.0/gRPC1.29.0

难度: 新手--战士--老兵--大师

目标:

  1. 实现四种模式下的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相关参数:

  1. java_multiple_files 是否单独生成在.proto文件中定义的顶级message、enum、和service,否则只生成一个包装了内部类的外部类; java_package 编译生成的java类文件的包位置;java_outer_classname 外部类名称;java_generic_services是否生成各语言版本的基类(已过时);

  2. service Greeter {...} 定义服务和包含的方法;

  3. 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')
}
}

以上代码解析:

  1. Moduleapply plugin: 'com.google.protobuf' 引入protobuf-gradle-plugin,作为protobuf 编译插件,会在右侧gradle--other中,自动添加proto相关的任务(共6个)
  2. compile fileTree(dir: "lib", include: "*.jar") 这里必须引入lib目录的j2ee相关jar,否则即使每次手动加入jar依赖,但启动应用时gradle会reimport,导致一直提示因少依赖而无法解析,这也是gradle引入第三方jar的正确方式
  3. implementation 'io.grpc:grpc-protobuf:1.29.0',gradle新版语法,implementation 仅仅对当前的Module提供接口,对外隐藏不必要的接口,而compile(新版升级为 api )依赖的库将会完全参与编译和打包,
  4. 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.GreeterGrpcgetSayHelloMethod 方法即约定了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=./

全文完!


我的其他文章:

只写原创,敬请关注

微服务通信方式——gRPC的更多相关文章

  1. .NET Core 微服务之grpc 初体验(干货)

    Grpc介绍 GitHub: https://github.com/grpc/grpc gRPC是一个高性能.通用的开源RPC框架,其由Google主要面向移动应用开发并基于HTTP/2协议标准而设计 ...

  2. .NET Core微服务开发服务间调用篇-GRPC

    在单体应用中,相互调用都是在一个进程内部调用,也就是说调用发生在本机内部,因此也被叫做本地方法调用:在微服务中,服务之间调用就变得比较复杂,需要跨网络调用,他们之间的调用相对于与本地方法调用,可称为远 ...

  3. go微服务系列(四) - gRPC入门

    1. 前言 2. gRPC与Protobuf简介 3. 安装 4. 中间文件演示 4.1 编写中间文件 4.2 运行protoc命令编译成go中间文件 5. 创建gRPC服务端 5.1 新建Produ ...

  4. 如何基于gRPC沟通微服务框架

    本文我们来讲解一下如何使用 gRPC构建微服务,gRPC是一个开源框架,可用于构建可扩展且高性能的微服务并创建服务之间的通信. 背景 随着企业越来越多地转向微服务,对构建这些微服务的低延迟和可扩展框架 ...

  5. 网易蜂巢微服务架构:用RabbitMQ实现轻量级通信

    本次分享内容由三个部分组成: 微服务架构与MQ RabbitMQ场景分析与优化 RabbitMQ在网易蜂巢中的应用和案例分享 1微服务架构与MQ 微服务架构是一种架构模式,它将单体应用划分成一组微小的 ...

  6. Go微服务全链路跟踪详解

    在微服务架构中,调用链是漫长而复杂的,要了解其中的每个环节及其性能,你需要全链路跟踪. 它的原理很简单,你可以在每个请求开始时生成一个唯一的ID,并将其传递到整个调用链. 该ID称为Correlati ...

  7. Golang微服务实践

    背景 在之前的文章<漫谈微服务>我已经简单的介绍过微服务,微服务特性是轻量级跨平台和跨语言的服务,也列举了比较了集中微服务通信的手段的利弊,本文将通过RPC通信的方式实现一个增删查Redi ...

  8. 【微服务落地】服务间通信方式: gRPC的入门

    gRPC是什么 官方介绍: https://grpc.io/docs/what-is-grpc/introduction/ "A high-performance, open-source ...

  9. Grpc微服务从零入门

    快速入门 安装 JDK 毫无疑问,要想玩Java,就必须得先装Java JDK,目前公司主要使用的是Oracle JDK 8,安装完成后要配置环境才能正常使用,真蠢,不过也就那么一下下,认了吧.配置方 ...

随机推荐

  1. iOS mmap

    一.常规文件操作 常规文件操作(read/write)有那几个重要步骤: 进程发起读文件请求 内核通过查找进程文件符表,定位到内核已打开文件集上的文件信息,从而找到此文件的 inode inode 在 ...

  2. Mac OSX安装 Django MySQL mysqlclient

    Python3.6 $ brew install mysql-connector-c # 如果没有安装brew,先安装:# 安装可以查看:https://www.cnblogs.com/Jokergu ...

  3. 4.用IntelliJ IDEA 创建Maven Web

    一.File→New→ Project 二.Maven→org.apache.maven.archetypes:maven-archetype-webapp→Next(记得打钩) 三.填入后Next→ ...

  4. Html,css构建一个对话框,练习201911281028

    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/stri ...

  5. 技术再深入一点又何妨?一脸懵B的聊Actor

    记得上次深入 Resin 源码时,见到了Actor 字眼,当时主要从 Resin 中抽取关键架构,就屏蔽了 Actor 相关代码.未曾想这两天研究 flink 的运行架构以及源码,再次与 Actor ...

  6. java中eclipse的安装和JDK的环境变量的配置以及记事本的使用

    2020-04-09 23:26:15 学习java的第一步当然就是环境配置了,java中的配置作为小白刚刚开始肯定会有点一点晕头转向的,开没等开始入门呢!就要准备放弃了.哈哈哈哈,没关系的,都是这么 ...

  7. cls

    class : python中cls代表的是类的本身,相对应的self则是类的一个实例对象. class Person(object): def __init__(self, name, age): ...

  8. C++模板心得

    C++模板心得 我开始学模板的时候一脸懵逼,真的看不懂模板是怎么作用的.因为大多数人的代码把模板声明和函数.类的声明分行写,让我以为模板的作用是全局的,实际上应该像如下理解. 函数模板 templat ...

  9. 曹工说Redis源码(4)-- 通过redis server源码来理解 listen 函数中的 backlog 参数

    文章导航 Redis源码系列的初衷,是帮助我们更好地理解Redis,更懂Redis,而怎么才能懂,光看是不够的,建议跟着下面的这一篇,把环境搭建起来,后续可以自己阅读源码,或者跟着我这边一起阅读.由于 ...

  10. .NET MVC中登录过滤器拦截的两种方法

    今天给大家介绍两种ASP中过滤器拦截的两种方法. 一种是EF 的HtppModule,另一种则是灵活很多针对MVC的特性类 Attribute 具体什么是特性类可以参考着篇文章:https://www ...