gRPC 实践 :基础 Demo

1,329 阅读5分钟

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164…
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca…

一. 前言

上一篇看了 gRPC 的构建方式,这一篇就来学习 gRPC 的使用方式了。

二. 基础使用

2.1 基础使用

参考这个案例 @ zhuanlan.zhihu.com/p/354095075

  1. 构建基础依赖包,用于创建 API 接口
  2. 准备Server端,Server 端需要继承相关的 gRPC 接口
  3. 通过 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步 :

  1. @Bean 构建 ManagedChannel
  2. 通过 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 的基础接口,定义了一些连接和请求的基本操作

image.png

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());

这里就接收到相关的信息了 :

image.png

这一块可能没看过 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();
    }
}

image.png

四. 补充

除了上述说的几个核心类,其实还有些其他的主要处理类 , 包括 :

  • ManagedChannelBuilder : 用于构建和配置 gRPC 客户端
  • CallOptions :定义调用 gRPC 服务时的一些选项,配置超时和拦截器
  • StreamObserver :表示一个 gRPC 流的观察者,用于处理服务器流和客户端流的响应
  • ServerInterceptor 、 ClientInterceptor : 服务端和客户端的拦截器

这些以后会在分析具体节点的时候加以补充。

总结

gRPC 的基础应用就到这里了,东西挺简单的,算是对整个调用都有了比较清晰的了解。

后续就要基于性能和优势来对细节点进行具体的分析了。