分布式RPC框架之gRPC
前言
关于本文研究的gRPC 相关的使用示例代码已开源,并整理成文章以方便大家快速学习。
代码:https://gitee.com/Jackpotsss/grpc-demo
RPC
基本概念
RPC (Remote Procedure Call,RPC)远程过程调用,是一种计算机通信协议,该协议允许一台计算机的程序调用另一台计算机中的程序,是一种进程间的通讯模式。而程序员就像调用本地程序一样,无需额外的为这个交互编程。
如果程序是面向对象编程,远程过程调用又叫做远程方法调用。
HTTP与RPC的区别
了解RPC是什么以及什么作用后,不难心生疑问,既然有 HTTP 请求,为什么还要用 RPC 调用?服务与服务之间直接使用HTTP接口调用不就行了吗,非得用RPC吗?问得好!
首先,这里面存在一个理解误区,http 和 rpc 并不是一个并行概念,rpc 是远程过程调用,其调用协议包含传输协议和序列化协议。传输协议包含:如 gRPC使用的是 http2.0 协议,dubbo基于TCP协议自定义报文。序列化协议包含:如基于文本编码的XML、JSON,基于二进制编码的protobuf、hessian 等。
如果单纯作为服务间调用使用的话,http1.1协议的报文中包含太多无用的信息,一个POST协议的格式大致如下:
HTTP/1.0 200 OK
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84
<html>
<body>Hello World</body>
</html>
即使编码协议也就是body 使用二进制编码协议,报文元数据也就是header头的键值对却用了文本编码,非常占字节数。如上面的报文所使用的有效字节数仅占约 30%,也就是70%的时间用于传输元数据无用编码。当然实际情况下报文内容可能会比这个长,但是报头所占的比例也是非常多的。
那么假如我们自定义tcp协议的报文,使元数据编码尽可能的减少,那么整体效率就会提高,Dubbo2.x 中就是这样实现的。而gRPC 则直接采用http2.0 协议,http2.0协议已经优化编码效率问题。
上面说的是传输效率方面,其他方面,比如服务端的业务逻辑发生异常,该如何通知客户端;在分布式系统中,服务往往是集群部署,那么自然而然对服务的调用应当是负载均衡调用的,分摊单个服务器的压力;当某个服务器宕机时,也应该进行失败重试,调用正常的服务。总之,异常处理、服务发现、负载均衡、熔断降级、安全机制等这些方面都是成熟的RPC框架应该具有的特性,相对于http来说,rpc 更多的是封装了这些特性,是面向服务的更高级的封装。如果在 http调用之上封装这一系列特性,那么它就是一个rpc框架了。
解决方案
工业界都有自己的RPC框架,常见的有:
- Java RMI
- Google gRPC
- Alibaba Dubbo(Apache)
- Facebook Thrift(Apache)
RMI
Java RMI 是Java 原生提供的远程过程调用协议。
gRPC
gRPC 是Google 开源的高性能、语言中立的RPC框架,本文主要讲解的对象。
Dubbo
Apache Dubbo 是一款微服务框架,最初由阿里开发,为大规模微服务实践提供高性能 RPC 通信、流量治理、可观测性等解决方案,涵盖 Java、Golang 等多种语言 SDK 实现。
Thrift
Thrift是一个轻量级、跨语言的远程服务调用框架,最初由Facebook开发,后进入Apache开源项目。它通过自身的IDL中间语言, 并借助代码生成引擎生成各种主流语言的RPC服务端/客户端模板代码。
Thrift支持多种不同的编程语言,包括C++、Java、Python、PHP、Ruby等。
工作流程
流程:
- 客户端调用客户端stub(client stub),这个调用是在本地,并将调用参数push到栈中。
- 客户端stub将这些参数包装 marshalling,并通过系统调用发送到服务端机器(常见方式:XML、JSON、二进制编码)
- 客户端本地操作系统发送信息至服务器。(可通过自定义TCP协议或HTTP传输)
- 服务器系统将信息传送至服务端stub(server stub)。
- 服务端stub解析信息。该过程叫 unmarshalling。
- 服务端stub调用程序,并通过类似的方式返回给客户端。
gRPC
gRPC 是一款高性能、开源的通用 RPC 框架,由Google 开源。它语言中立,平台中立,gRPC 的客户端和服务端可以在不同的环境中运行,例如使用java 语言写一个服务端,可以用go 语言写客户端调用。
数据在进行网络传输的时候,需要进行序列化,gRPC 默认使用 protocol-buffers ,这是 Google 开源的一套成熟的结构数据的序列化机制。本文基于gRPC-java 版本进行研究和学习。
protobuf 协议
gRPC 默认使用 protocol-buffers
作为其接口定义语言 (IDL) 和底层消息交换格式。
在 gRPC 中,客户端应用程序可以像在本地一样直接调用不同机器上的服务器应用程序上的方法,使您可以更轻松地创建分布式应用程序和服务。 与许多 RPC 系统一样,gRPC 是基于定义服务的想法,指定可以使用其参数和返回类型远程调用的方法。 在服务端,服务端实现了这个接口,运行一个gRPC服务端来处理客户端调用。 在客户端,客户端有一个 stub,它提供与服务器相同的方法。
gRPC 客户端和服务器可以在各种环境中运行和相互通信。例如,您可以使用 Go、Python 或 Ruby 的客户端与使用 Java 创建的 gRPC 服务器进行通信。
IDL(Interface description language)是指接口描述语言,是用来描述软件组件接口的一种计算机语言,是跨平台开发的基础。IDL通过一种中立的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信交流;比如,一个组件用C++写成,另一个组件用Go写成。
运行原理
默认情况下,gRPC 使用 Protocol Buffers,这是 Google 用于序列化结构化数据的成熟开源机制。
使用 protocol buffers 的第一步是在 proto 文件中定义要序列化的数据的结构,这是一个扩展名为 .proto 的普通文本文件。 下面是一个简单的例子:
// 定义一个叫HelloService的服务
service HelloService {
// 定义一个叫SayHello的方法,这个方法接受HelloRequest消息作为参数,返回HelloResponse消息
rpc SayHello (HelloRequest) returns (HelloResponse);
}
// 定义HelloRequest消息
message HelloRequest {
required string greeting = 1;
}
// 定义HelloResponse消息
message HelloResponse {
required string reply = 1;
}
一旦指定了数据结构,就可以使用 protocol buffer 编译器 protoc
从 proto 定义中以您的首选语言生成数据访问类。
基本使用
使用步骤:
- 添加相关依赖及插件
- 使用IDL编写proto文件
- 编译自动生成代码
- 编写业务代码
在Maven Pom文件中添加相关依赖及插件的细节这里就不赘述了,详情请查看官方文档。
proto 文件
在proto 文件中编写接口描述语言IDL
定义消息
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
syntax关键词定义使用的是proto3语法版本
message关键词,标记开始定义一个消息,消息体,用于定义各种字段类型。protobuf消息定义的语法结构,跟我们平时接触的各种语言的类定义,非常相似。
字段类型
支持多种数据类型,例如:string、int32、double、float等等,下面会有详细的讲解。
分配标识符
通过前面的例子,在消息定义中,每个字段后面都有一个唯一的数字,这个就是标识号。
这些标识号是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变,每个消息内唯一即可,不同的消息定义可以拥有相同的标识号。
注: [1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的字段预留一些标识号。
基本字段类型
proto 中的字段类型以及与各语言的映射关系:
.proto Type | Notes | C++ Type | Java Type | Go Type |
---|---|---|---|---|
double | double | double | float64 | |
float | float | float | float32 | |
int32 | 使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代 | int32 | int | int32 |
uint32 | 使用变长编码 | uint32 | int | uint32 |
uint64 | 使用变长编码 | uint64 | long | uint64 |
sint32 | 使用变长编码,这些编码在负值时比int32高效的多 | int32 | int | int32 |
sint64 | 使用变长编码,有符号的整型值。编码时比通常的int64高效。 | int64 | long | int64 |
fixed32 | 总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。 | uint32 | int | uint32 |
fixed64 | 总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。 | uint64 | long | uint64 |
sfixed32 | 总是4个字节 | int32 | int | int32 |
sfixed64 | 总是8个字节 | int64 | long | int64 |
bool | bool | boolean | bool | |
string | 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。 | string | String | string |
bytes | 可能包含任意顺序的字节数据。 | string | ByteString | []byte |
数组类型
在protobuf消息中定义数组类型,是通过在字段前面增加repeated关键词实现,标记当前字段是一个数组。
message Msg {
repeated int32 arrays = 1; // 整型数组
}
message Msg {
repeated string names = 1; //字符串数组
}
消息嵌套
我们在各种语言开发中类型的定义是可以互相嵌套的,可以使用其他类作为自己的成员属性类型。在protobuf中同样支持消息嵌套,可以在一个消息中嵌套另外一个消息,字段类型可以是另外一个消息类型。
// 定义Result消息
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3; // 字符串数组
}
// 定义SearchResponse消息
message SearchResponse {
// 引用上面定义的Result消息类型,作为results字段的类型
repeated Result results = 1; // repeated关键词标记,说明results字段是一个数组
}
类似类嵌套一样,消息也可以嵌套
message SearchResponse {
// 嵌套消息定义
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
// 引用嵌套的消息定义
repeated Result results = 1;
}
import导入
我们在开发一个项目的时候通常有很多消息定义,都写在一个proto文件,不方便维护,通常会将消息定义写在不同的proto文件中,在需要的时候可以通过import导入其他proto文件定义的消息。
文件: result.proto
syntax = "proto3";
message Result {
string url = 1;
string title = 2;
}
文件: search_response.proto
syntax = "proto3";
import "result.proto"; // 导入Result消息定义
message SearchResponse {
repeated Result results = 1;
}
枚举类型
syntax = "proto3";//指定版本信息,不指定会报错
enum PhoneType { //枚举消息类型,使用enum关键词定义,一个电话类型的枚举类型
MOBILE = 0; //proto3版本中,首成员必须为0,成员不应有相同的值
HOME = 1;
WORK = 2;
}
// 定义一个电话消息
message PhoneNumber{
string number = 1; // 电话号码字段
PhoneType type = 2; // 电话类型字段,电话类型使用PhoneType枚举类型
}
特别注意:
必须有有一个0值,我们可以用这个0值作为默认值。 这个零值必须为第一个元素,为了兼容proto2语义,枚举类的第一个值总是默认值。(这个默认值还是比较坑的,开发的时候注意规避)
map类型
语法:
map<key_type, value_type> map_field = N;
key_type
可以是字符串类型或任意整型,不能是浮点类型、字节类型、枚举类型。
syntax = "proto3";
message Product{
string name = 1;
map<string, string> attrs = 2; // 键值对
}
注:Map 字段不能使用repeated
关键字修饰。
注意事项:
1、如果 执行过程中发现 import依赖的文件找不到,需要多指定几个 proto_path(如–proto_path:. –proto_path:/common/base)
2、如果 需要生产多个类需要在 proto文件中加入
option java_multiple_files = true;
3、如果 需要指定生成的类路径需要在 proto文件中加入
option java_package="com.xxxx.xxx.xxx";
自动生成代码
这里额外补充另外一种编译自动生成代码的方法,就是直接下载Protobuf 编译器 protoc
及插件:
下载 Protobuf 编译器 protoc
:https://github.com/protocolbuffers/protobuf/releases
下载protoc-gen-grpc 插件 ,选择版本下载 , 下载地址: https://repo.maven.apache.org/maven2/io/grpc/protoc-gen-grpc-java/
安装完成后,将 /bin 路径添加到PATH环境变量中。打开cmd,命令窗口执行protoc 命令验证是否安装成功。
生成java 源码:
protoc --java_out=. xxx.proto
使用插件生成Grpc类:
protoc --plugin=protoc-gen-grpc-java=D:\rpc\protoc-gen-grpc-java\protoc-gen-grpc-java-1.52.0.exe --grpc-java_out=. --proto_path=./ xxx.proto
注意:
--proto_path
接收两个参数,分别是路径 和 文件名。
两种调用方式
客户端对服务器的rpc请求,gRPC 支持同步调用和异步调用两种方式。
同步调用
使用Grpc 工具类的 newBlockingStub()
方法获取一个同步阻塞的客户端Stub:
public void sendMsgSimple(){
MyServiceGrpc.MyServiceBlockingStub blockingStub = MyServiceGrpc.newBlockingStub(channel);
MyMemberRequest request = MyMemberRequest.newBuilder().setName("jackpot").build();
MyResponse response = blockingStub.sendMessageSingle(request);
}
异步调用
使用Grpc 工具类的 newFutureStub()
方法获取一个异步调用的客户端Stub:
异步请求后立马返回一个Future对象,我们可以调用 get()
方法阻塞等待获取响应结果,或者添加监听器,等响应结果到达时执行回调函数。
public void sendMsgSimpleAsync(){
MyServiceGrpc.MyServiceFutureStub myServiceFutureStub = MyServiceGrpc.newFutureStub(channel);
MyMemberRequest request = MyMemberRequest.newBuilder().setName("jackpot").build();
ListenableFuture<MyResponse> responseListenableFuture = myServiceFutureStub.sendMessageSingle(request);
responseListenableFuture.addListener(()->{
try {
MyResponse response = responseListenableFuture.get();
logger.info("客户端接收到服务端的响应信息: {}", response.getMessage());
}catch (InterruptedException i) {
}catch ( ExecutionException e) {
}
},Executors.newFixedThreadPool(3));
// MyResponse response = myResponseListenableFuture.get();
//MyResponse response = myResponseListenableFuture.get(5, TimeUnit.SECONDS); //设定阻塞超时时间
}
四种交互
gRPC 允许你定义四类服务方法,下面分别介绍如何定义,以及客户端和服务端的交互方式。
简单RPC
即客户端发送一个请求给服务端,从服务端获取一个应答,就像一次普通的函数调用。
rpc SayHello (HelloRequest) returns (HelloReply) {}
在简单RPC中,服务端不能响应多条数据,只能响应一次,否则按异常处理。如果期望服务端像流一样响应多条数据,应该使用服务端流式RPC。
服务端流式 RPC
即客户端发送一个请求给服务端,可获取一个数据流用来读取一系列消息。客户端从返回的数据流里一直读取直到没有更多消息为止。
通俗的讲就是客户端请求一次,服务端就可以源源不断的给客户端发送消息。
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){}
注意stream关键词声明在什么地方
客户端流式 RPC
即客户端用提供的一个数据流写入并发送一系列消息给服务端。一旦客户端完成消息写入,就等待服务端读取这些消息并返回应答。客户端通过流发送多次rpc请求给服务端,服务端接收数据后返回一个响应。
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse) {}
例如当客户端需要上传一个文件到服务端时就可以采用这种方式,客户端将文件拆分成多个小的文件,以流的方式顺序发送给服务端,服务端将这些文件接收到合并,这样比直接上传一个文件,效率要高。
双向流式 RPC
即两边都可以分别通过一个读写数据流来发送一系列消息。这两个数据流操作是相互独立的,所以客户端和服务端能按其希望的任意顺序读写,例如:服务端可以在写应答前等待所有的客户端消息,或者它可以先读一个消息再写一个消息,或者是读写相结合的其他方式。每个数据流里消息的顺序会被保持。
类似tcp通信,客户端和服务端可以互相发消息。
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){}
异常处理
方案一:
直接调用OnError方法,传递Status包装异常后返回
try {
}catch (Throwable t) {
responseObserver.onError(Status.UNKNOWN
.withDescription(t.getMessage())
.withCause(t)
.asRuntimeException());
}
这个方式客户端可以感知到,但是可能能够放入的信息有限,只能是一个字符串,只能在withDescription
这个参数里,如果要多个参数,可能借助一些序列化框架转化为字符串进行转换。
方案二:
protobuf文件:
syntax = "proto3";
package credit ;
option java_package = "com.maycur.grpc.credit";
// 通用异常处理信息
message ErrorInfo {
string errorCode = 1; // 错误的业务编码
string defaultMsg = 2;// 默认提示信息
}
java代码:

注:
业务服务和RPC服务使用不同的端口,但在同一进程中;同样的,业务客户端和客户端Stub 使用不同的端口,但属于同一进程。
继续优化:包装StreamObserver类,增强其功能。(采用的方案)
整个异常处理的流程,总结起来就是两点:
- 异常的传输是通过 Status/Metadata 来实现
- Status/Metadata 的传输是通过 HTTP Header 来实现的
状态码文档:https://github.com/grpc/grpc/blob/master/doc/statuscodes.md
命名解析
gRPC 默认使用 DNS 作为命名解析,如果想要使用其他的注册中心,如 Consul 等,就需要扩展命名解析的逻辑 使用 Consul 作为注册中心,实现 Consul 命名解析的逻辑。
负载均衡
在微服务架构下,同一种服务往往会部署到多台服务器,以达到高可用的目的,此时服务间的调用就不得不考虑负载均衡调用的问题了。gRPC 的负载均衡策略有多种,主要有以下几种:
- 基于 dns 实现
- 基于注册中心的实现
- 基于 Nginx 的代理负载模式
gRPC 支持 DNS
作为默认 Naming 系统,同时也提供了实现 Naming
系统乃至 LoadBalance
功能的用户接口。所以,第三方注册中心,如 Etcd
、Consul
、Zookeeper
都可以作为非常优秀的 gRPC
负载均衡实现。
gRPC Name Resolution
常用如下格式,scheme 表示要使用的 Naming
方式。目前常用的 scheme 有:
scheme://authority/endpoint_name
dns (例: dns://myAuthority/www.qq.com)
ipv4 (IPv4 地址 例: ipv4:///110.12.92.1:443)
ipv6 (IPv6 地址 例: ipv6:///2607:f8b0:400a:801::1001)
基于注册中心
gRPC官方提供了关于gRPC的负载均衡方案
gRPC中的负载平衡是以每次调用为基础,而不是以每个连接为基础。换句话说,即使所有的请求都来自一个客户端,我们仍希望它们在所有的服务器上实现负载平衡。

目前官方提供了以下几种负载均衡策略:
- pick_first 取第一个地址
- round_robin 轮询
在负载均衡方面不如dubbo的组件那么丰富,但是其提供了服务发现的接口, 可以通过实现其接口,灵活实现负载均衡功能。grpc-java 客户端提供了 NameResolver 、NameResolverProvider 、NameResolverRegistry 等实现服务注册发现的扩展类。结合注册中心 ZooKeeper/Etcd/Consul 等,很容易实现一个基于注册中心的带服务治理的 grpc 。
pick_first
如果服务配置没有指定任何 LB 策略,这是默认的 LB 策略。 它不需要任何配置。pick_first 策略从解析器获取地址列表。 它尝试按顺序一次一个地连接到这些地址,直到找到一个可以访问的地址。 如果所有地址都不可到达,它会在尝试重新连接时将通道的状态设置为 TRANSIENT_FAILURE
。如果它能够连接到其中一个地址,它将通道的状态设置为 READY,然后通道上发送的所有 RPC 将被发送到该地址。 如果到该地址的连接随后断开,pick_first 策略会将通道置于 IDLE 状态,并且它不会尝试重新连接,直到应用程序请求它这样做(通过通道的连接状态 API 或发送 RPC) .
round_robin
轮询策略
服务动态发现
可以基于Zookeeper手动实现
基于nginx 代理
Grpc服务基于nginx 实现负载均衡
自定义负载均衡策略
容错机制
失败重试策略
对冲策略
Hedging 是一种备选重试策略。 Hedging 允许在不等待响应的情况下,主动发送单个 gRPC 调用的多个副本。 Hedged gRPC 调用可以在服务器上执行多次,并使用第一个成功的结果。 重要的是,务必仅针对可安全执行多次且不会造成负面影响的方法启用 hedging。
与重试相比,Hedging 具有以下优缺点:
- Hedging 的优点是,它可能会更快地返回成功的结果。 它允许同时进行多个 gRPC 调用,并在出现第一个成功的结果时完成。
- Hedging 的一个缺点是它可能会造成浪费。 进行了多个调用并且这些调用全部成功。 而仅使用第一个结果,并放弃其余结果。
数据传输
gRPC 有多种传输实现:
基于 Netty 的 HTTP/2 传输是基于 Netty 的主要传输实现。 它在 Android 上不受官方支持。
基于 OkHttp 的 HTTP/2 传输是一种基于 Okio 的轻量级传输,并分叉了 OkHttp 的低级部分。 它主要用于Android。
进程内传输适用于服务器与客户端处于同一进程的情况。 它经常用于测试,同时对于生产使用也是安全的。
Binder 传输用于单个设备上的 Android 跨进程通信。
keepalive ping 是一种通过传输发送 HTTP2 ping 来检查通道当前是否工作的方法。 它是定期发送的,如果 ping 在一定的超时时间内没有被对等方确认,则传输将断开。
GRPC_ARG_KEEPALIVE_TIME_MS
此通道参数控制在传输上发送 keepalive ping 之前的时间段(以毫秒为单位)。
GRPC_ARG_KEEPALIVE_TIMEOUT_MS
此通道参数控制 keepalive ping 发送方等待确认的时间量(以毫秒为单位)。 如果在这段时间内没有收到确认,它将关闭连接。
gRPCurl
gRPCurl 是由 gRPC 社区创建的命令行工具。 其功能包括:
- 调用 gRPC 服务,包括流式服务。
- 使用 gRPC 反射进行服务发现。
- 列出并描述 gRPC 服务。
- 适用于安全 (TLS) 和不安全(纯文本)服务器。
有关下载和安装 grpcurl
的信息,请参阅 gRPCurl GitHub 主页。
# 查看帮助
grpcurl -help
# 使用 describe 来查看服务器定义的服务
grpcurl localhost:<port> describe
调用 gRPC 服务
grpcurl 工具常用命令:
# 查看RPC服务列表
grpcurl -plaintext 127.0.0.1:9980 list
grpc.reflection.v1alpha.ServerReflection
syncuser.BatchSyncUserService
# 查看某个服务的方法列表
grpcurl -plaintext 127.0.0.1:9980 list syncuser.BatchSyncUserService
syncuser.BatchSyncUserService.batchSyncUser
# 查看方法定义
grpcurl -plaintext 127.0.0.1:9980 describe syncuser.BatchSyncUserService.batchSyncUser
syncuser.BatchSyncUserService.batchSyncUser is a method:
rpc batchSyncUser ( .syncuser.BatchSyncUser ) returns ( .syncuser.MyResponse );
# 查看请求参数
grpcurl -plaintext 127.0.0.1:9980 describe syncuser.BatchSyncUser
syncuser.BatchSyncUser is a message:
message BatchSyncUser {
repeated .syncuser.BatchSyncUser.SyncUser syncUsers = 1;
.syncuser.OperationType operationType = 2;
string clientId = 3;
message SyncUser {
string userName = 1;
string mobile = 2;
string accountNo = 3;
.syncuser.PostEnum userType = 4;
bool company_exam_pass = 10;
sint64 company_exam_pass_time = 11;
bool project_exam_pass = 12;
sint64 project_exam_pass_time = 13;
bool group_exam_pass = 14;
sint64 group_exam_pass_time = 15;
bool all_exam_pass = 16;
sint64 all_exam_pass_time = 17;
}
}
# 调用rpc服务,默认请求格式为json
grpcurl -plaintext -d '{"syncUsers":[{"userName ":"test1"}],"operationType":1}' 127.0.0.1:9980 syncuser.BatchSyncUserService.batchSyncUser
{
"message": "用户数据同步完成,共计同步1条用户数据"
}
grpcurl 下载地址:https://github.com/fullstorydev/grpcurl/releases
遇到的使用错误
错误1:
grpcurl 报错tls: first record does not look like a TLS handshake
解决办法:请求时增加参数:-plaintext,参考下面的命令
grpcurl -plaintext localhost:<port> describe
错误2:
grpcurl.exe -plaintext 127.0.0.1:9980 list
Failed to list services: server does not support the reflection API
解决办法:RPC服务要开启反射服务
添加依赖:
compile "io.grpc:grpc-services:${grpcVersion}"
添加反射服务
ServerBuilder.forPort(port)
.addService(batchSyncUserService)
.addService(ProtoReflectionService.newInstance())// 添加反射服务
.build()
README
作者:银法王
记录:
2023-01-30 第一次修订
参考: