首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164…
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca…
一. 前言
上一篇看了 gRPC 的构建方式,这一篇就来学习 gRPC 的使用方式了。
二. 基础使用
2.1 基础使用
参考这个案例 @ zhuanlan.zhihu.com/p/354095075
- 构建基础依赖包,用于创建 API 接口
- 准备Server端,Server 端需要继承相关的 gRPC 接口
- 通过 gRPC 生成的工具发起调用
总结 :使用的流程其实和 Feign 或者 Dubbo 是一样的,核心的逻辑都是面向接口,同时封装内部的具体调用逻辑。最大的区别可能是 gRPC 是通过工具类直接生成对应的 interface
S1 : 通过 Maven 构建 gRPC 访问接口
@ https://juejin.cn/post/7223361873760157753
syntax = "proto3";
package com.example.grpc;
option java_multiple_files = true;
// 接口类
service HelloService {
rpc hello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string name = 1 ;
int32 age = 2;
repeated string hobbies = 3;
map<string, string> tags = 4;
}
message HelloResponse {
string greeting = 1;
}
S2 : Client 和 Server 中引入生成的依赖
- 由于第一步生成的依赖包中包含了自动生成的类 , 这里要引用进去
S3 : 构建 Server 端
@GrpcService
public class UserService extends UserServiceGrpc.UserServiceImplBase {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
public void query(UserRequest request, StreamObserver<UserResponse> responseObserver) {
logger.info(" UserService 接收到的参数,name:" + request.getName());
UserResponse response = UserResponse.newBuilder().setName("test").setAge(0).setAddress("test").build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
- 这里的 @GrpcService 是自定义的,目的主要是用于扫描 Service 类 ,汇总 config 中进行加载
@Component
public class ServiceManager {
// 用于管理整个客户端的 Server
private Server server;
// 用于定于客户端访问的端口号
private int grpcServerPort = 9091;
public void loadService(Map<String, Object> grpcServiceBeanMap) throws IOException, InterruptedException {
// 构建一个 ServerBuilder,为其匹配一个端口
ServerBuilder serverBuilder = ServerBuilder.forPort(grpcServerPort);
// 添加可以处理的 Server 接口
for (Object bean : grpcServiceBeanMap.values()) {
serverBuilder.addService((BindableService) bean);
}
// 启动 Server
server = serverBuilder.build().start();
// 配置前后置处理器
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
if (server != null) {
server.shutdown();
}
}
});
server.awaitTermination();
}
}
S4 : 发起调用
@Resource
UserServiceGrpc.UserServiceBlockingStub userService;
// 通过 Stub 发起 Server 请求
public String query() {
UserRequest userRequest = UserRequest.newBuilder().setName("test").build();
UserResponse user = userService.query(userRequest);
return "ok";
}
这一块主要涉及到的是几个组件的创建,主要分为2步 :
- @Bean 构建 ManagedChannel
- 通过 ManagedChannel 构建对应的 ServiceGrpc 和 Stub
@Configuration
public class GrpcServiceConfig {
@Bean
public ManagedChannel getChannel() {
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 9091)
.usePlaintext()
.build();
return channel;
}
@Bean
public HelloServiceGrpc.HelloServiceBlockingStub getStub1(ManagedChannel channel) {
return HelloServiceGrpc.newBlockingStub(channel);
}
@Bean
public UserServiceGrpc.UserServiceBlockingStub getStub2(ManagedChannel channel) {
return UserServiceGrpc.newBlockingStub(channel);
}
}
三. 核心要点
了解其原理的第一步可以从了解其流程开始,这里主要来看看其中几个暴露出来的流程类 :
S1 : 服务端的建立 - Server
ServerBuilder serverBuilder = ServerBuilder.forPort(8089);
Server server = serverBuilder.build().start();
// 核心就是那一句 start()
- 在启动start后,最主要的过程就是如下几句
public ServerImpl start() throws IOException {
ServerListenerImpl listener = new ServerListenerImpl();
for (InternalServer ts : transportServers) {
// 这里会调用具体的实现类,例如 NettyServer
ts.start(listener);
activeTransportServers++;
}
}
// C- NettyServer
- 在 Netty Server 中就会为其构建一个 Netty 连接,这里就不详细看了,主要几个核心的要点
ServerBootstrap b = new ServerBootstrap();
NettyServerTransport transport = new NettyServerTransport(...)
ransportListener = listener.transportCreated(transport);
S2 :客户端的建立 - ManagedChannel
从上面可以看到一个 Netty Server 的建立,那么下面对应的应该就是 Netty Client 的相关配置了 :
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 8089)
.usePlaintext()
.build();
这个类很大,一篇都讲不完,核心的方法就是 :
newCall(MethodDescriptor<ReqT,RespT> methodDescriptor, CallOptions callOptions):创建一个新的 Call 对象,用于发起请求
S3 : 连接和请求 - Channel
ManagedChannel 的基础接口,定义了一些连接和请求的基本操作
S4 : 真实的调用 - Call
call 系列包含 ClientCall 和 ServerCall 两种,通过这2种类让客户端和服务端对 一个 gRPC 调用发起管理
ClientCall 主要体现在发起具体调用的时候,在对应的 BlockingStub 类中,有使用 ClientCall 相关的处理 :
public com.example.grpc.UserResponse query(com.example.grpc.UserRequest request) {
return blockingUnaryCall(
// getChannel 后,就会通过 Channel 创建 call
getChannel(), getQueryMethod(), getCallOptions(), request);
}
public static <ReqT, RespT> RespT blockingUnaryCall(Channel channel, MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, ReqT req) {
// 1. 通过 ManagedChannel 创建一个 ClientCall
ClientCall call = channel.newCall(method, callOptions.withExecutor(executor));
// 2. 构建一个异步调用的 Future
ListenableFuture<RespT> responseFuture = futureUnaryCall(call, req);
// 3. 发起调用,这里应该是多线程等待线程执行
executor.waitAndDrain();
// 4. 等待回调 ,这里就不多看了,肯定是 future.get
getUnchecked(responseFuture)
}
后续就是 ServerCall 接收到消息了,忽略底层逻辑,大概就是从这里开始的:
C- ServerCall
// 1. 从 Message 中读取信息
private void messagesAvailableInternal(final MessageProducer producer) {
InputStream message;
while ((message = producer.next()) != null) {
//
listener.onMessage(call.method.parseRequest(message));
message.close();
}
}
// 2. JumpToApplicationThreadServerStreamListener # messagesAvailable 中触发执行
callExecutor.execute(new MessagesAvailable());
这里就接收到相关的信息了 :
这一块可能没看过 Netty 或者其他开源框架源码的会比较疑惑,打断点看源码也会有点迷糊,这里是一个典型的线程池处理方式:
简单点说就是具体的业务处理是开多线程处理的,而Selector会在之前就查询到所有的信息,给多线程取调用具体的方法执行。而 selector 是一个循环监听的角色,也就没办法快速找到具体的切换入口了。
所以,这里在具体执行方法上进行断点通常看不到前置的处理方法。
最简单的解题方式是找到多线程创建或者添加的地方,在那里打个断点就行 :
// 第一步 : 放入线程池
C- SerializingExecutor
public void execute(Runnable r) {
runQueue.add(checkNotNull(r, "'r' must not be null."));
// 这里还搞了个定时,推测是用于超时处理等等场景,先不细看
schedule(r);
}
// 第二步 : 从线程池取出来
C- SerializingExecutor
public void run() {
Runnable r;
// 在这个序列化执行器中,就是在不断的循环 RunQueue ,找到就执行
while ((r = runQueue.poll()) != null) {
r.run();
}
}
四. 补充
除了上述说的几个核心类,其实还有些其他的主要处理类 , 包括 :
- ManagedChannelBuilder : 用于构建和配置 gRPC 客户端
- CallOptions :定义调用 gRPC 服务时的一些选项,配置超时和拦截器
- StreamObserver :表示一个 gRPC 流的观察者,用于处理服务器流和客户端流的响应
- ServerInterceptor 、 ClientInterceptor : 服务端和客户端的拦截器
这些以后会在分析具体节点的时候加以补充。
总结
gRPC 的基础应用就到这里了,东西挺简单的,算是对整个调用都有了比较清晰的了解。
后续就要基于性能和优势来对细节点进行具体的分析了。