gRPC(3):拦截器

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了gRPC(3):拦截器相关的知识,希望对你有一定的参考价值。

参考技术A

在 gRPC 调用过程中,我们可以拦截 RPC 的执行,在 RPC 服务执行前或执行后运行一些自定义逻辑,这在某些场景下很有用,例如身份验证、日志等,我们可以在 RPC 服务执行前检查调用方的身份信息,若未通过验证,则拒绝执行,也可以在执行前后记录下详细的请求响应信息到日志。这种拦截机制与 Gin 中的中间件技术类似,在 gRPC 中被称为 拦截器 ,它是 gRPC 核心扩展机制之一

拦截器不止可以作用在服务端上,客户端同样可以拦截,在请求发出之前和收到响应之后执行一些自定义逻辑,根据拦截的 RPC 类型,可分为 一元拦截器 流拦截器

在 gRPC 服务端,可以插入一个或多个拦截器,收到的请求按注册顺序通过各个拦截器,返回响应时则倒序通过:

通过以下步骤实现一元拦截器:

流拦截器包括前置处理阶段和流操作阶段,前置处理阶段可以在流 RPC 进入具体服务实现之前进行拦截,而在流操作阶段,可以对流中的每一条消息进行拦截,通过以下步骤实现流拦截器:

在服务端可以拦截收到的 RPC 调用,客户端同样可以拦截发出去的 RPC 请求以及收到的响应,同样可以实现一元拦截器以及流拦截器:

和服务端一元拦截器一样的方法,只是方法参数略微有所差别,此外在建立连接的时候注册拦截器,同样可以注册多个拦截器:

流拦截器也是和服务端一样的步骤:

聊一聊 gRPC 中的拦截器

今天我们继续 gRPC 系列。

前面松哥跟大家聊了 gRPC 的简单案例,也说了四种不同的通信模式,感兴趣的小伙伴可以戳这里:

  1. 一个简单的案例入门 gRPC
  2. 聊一聊 gRPC 的四种通信模式

今天我们来继续聊一聊 gRPC 中的拦截器。

有请求的发送、处理,当然就会有拦截器的需求,例如在服务端通过拦截器统一进行请求认证等操作,这些就需要拦截器来完成,今天松哥先和小伙伴们来聊一聊 gRPC 中拦截器的基本用法,后面我再整一篇文章和小伙伴们做一个基于拦截器实现的 JWT 认证的 gRPC。

gRPC 中的拦截器整体上来说可以分为两大类:

  1. 服务端拦截器
  2. 客户端拦截器

我们分别来看。

1. 服务端拦截器

服务端拦截器的作用有点像我们 Java 中的 Filter,服务端拦截器又可以继续细分为一元拦截器流拦截器

一元拦截器对应我们上篇文章中所讲的一元 RPC,也就是一次请求,一次响应这种情况。

流拦截器则对应我们上篇文章中所讲的服务端流 RPC、客户端流 RPC 以及双向流 RPC。

不过,在 Java 代码中,无论是一元拦截器还是流拦截器,代码其实都是一样的。不过如果你是用 Go 实现的 gRPC,那么这块是不一样的。

所以接下来的内容我就不去区分一元拦截器和流拦截器了,我们直接来看一个服务端拦截器的例子。

这里我就不从头开始写了,我们直接在上篇文章的基础之上继续添加拦截器即可。

服务端拦截器工作位置大致如下:

从这张图中小伙伴们可以看到,我们可以在服务端处理请求之前将请求拦截下来,统一进行权限校验等操作,也可以在服务端将请求处理完毕之后,准备响应的时候将响应拦截下来,可以对响应进行二次处理。

首先我们来看请求拦截器,实际上是一个监听器:

public class BookServiceCallListener<R> extends ForwardingServerCallListener<R> 
    private final ServerCall.Listener<R> delegate;

    public BookServiceCallListener(ServerCall.Listener<R> delegate) 
        this.delegate = delegate;
    

    @Override
    protected ServerCall.Listener<R> delegate() 
        return delegate;
    

    @Override
    public void onMessage(R message) 
        System.out.println("这是客户端发来的消息,可以在这里进行预处理:"+message);
        super.onMessage(message);
    

这里我们自定义一个类,继承自 ForwardingServerCallListener 类,在这里重写 onMessage 方法,当有请求到达的时候,就会经过这里的 onMessage 方法。如果我们需要对传入的参数进行验证等操作,就可以在这里完成。

再来看看响应拦截器:

public class BookServiceCall<ReqT,RespT> extends ForwardingServerCall.SimpleForwardingServerCall<ReqT,RespT> 
    protected BookServiceCall(ServerCall<ReqT, RespT> delegate) 
        super(delegate);
    

    @Override
    protected ServerCall<ReqT, RespT> delegate() 
        return super.delegate();
    

    @Override
    public MethodDescriptor<ReqT, RespT> getMethodDescriptor() 
        return super.getMethodDescriptor();
    

    @Override
    public void sendMessage(RespT message) 
        System.out.println("这是服务端返回给客户端的消息:"+message);
        super.sendMessage(message);
    

小伙伴们可能发现了,我这里用到了很多泛型,请求类型和响应类型都不建议指定具体类型,因为拦截器可能会拦截多种类型的请求,请求参数和响应的数据类型都不一定一样。

这里是重写 sendMessage 方法,在这个方法中我们可以对服务端准备返回给客户端的消息进行预处理。

所以这个位置就相当于响应拦截器

最后,我们需要在启动服务的时候,将这两个拦截器配置进去,代码如下:

public void start() throws IOException 
    int port = 50051;
    server = ServerBuilder.forPort(port)
            .addService(ServerInterceptors.intercept(new BookServiceImpl(), new ServerInterceptor() 
                @Override
                public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) 
                    String fullMethodName = call.getMethodDescriptor().getFullMethodName();
                    System.out.println(fullMethodName + ":pre");
                    Set<String> keys = headers.keys();
                    for (String key : keys) 
                        System.out.println(key + ">>>" + headers.get(Metadata.Key.of(key, ASCII_STRING_MARSHALLER)));
                    
                    return new BookServiceCallListener<>(next.startCall(new BookServiceCall(call), headers));
                
            ))
            .build()
            .start();
    Runtime.getRuntime().addShutdownHook(new Thread(() -> 
        BookServiceServer.this.stop();
    ));

这是我之前服务启动的方法,以前我们调用 addService 方法的时候,直接添加对应的服务就可以了,现在,我们除了添加之前的 BookServiceImpl 服务之外,还额外给了一个拦截器。

每当请求到达的时候,就会经过拦截器的 interceptCall 方法,这个方法有三个参数:

  • 第一个参数 call 是消费传入的 RPC 消息的一个回调。
  • 第二个参数 headers 则是请求的消息头,如果我们通过 JWT 进行请求校验,那么就从这个 headers 中提取出请求的 JWT 令牌然后进行校验。
  • 第三个参数 next 就类似于我们在 Java 过滤器 filter 中的 filterChain 一样,让这个请求继续向下走。

在这个方法中,我们请求头的信息都打印出来给小伙伴们参考了。然后在返回值中,将我们刚刚写的请求拦截器和响应拦截器构建并返回。

好啦,这样我们的服务端拦截器就搞好啦~无论是一元的 RPC 消息还是流式的 RPC 消息,都会经过这个拦截器,响应也是一样。

2. 客户端拦截器

客户端拦截器就比较简单了,客户端拦截器可以将我们的请求拦截下来,例如我们如果想为所有请求添加统一的令牌 Token,那么就可以在这里来做,方式如下:

ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
        .usePlaintext()
        .intercept(new ClientInterceptor() 
            @Override
            public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) 
                System.out.println("!!!!!!!!!!!!!!!!");
                callOptions = callOptions.withAuthority("javaboy");
                return next.newCall(method,callOptions);
            
        )
        .build();
BookServiceGrpc.BookServiceStub stub = BookServiceGrpc.newStub(channel);

当我们的请求执行的时候,这个客户端拦截器就会被触发。

3. 小结

好啦,今天就和小伙伴们简单介绍一下服务端拦截器和客户端拦截器。下篇文章,松哥会通过一个 JWT 认证来和小伙伴们演示这个拦截器的具体用法。

以上是关于gRPC(3):拦截器的主要内容,如果未能解决你的问题,请参考以下文章

手把手教大家在 gRPC 中使用 JWT 完成身份校验

手把手教大家在 gRPC 中使用 JWT 完成身份校验

gRPC-Web中的拦截器

聊一聊 gRPC 中的拦截器

聊一聊 gRPC 中的拦截器

gRPC拦截器的实现