gRPC客户端拦截器的踩坑心得

3,234 阅读4分钟

我正在参加「掘金·启航计划」

起因:前几天我的头头儿让我实现一下gRPC元数据的传递功能,主要用于服务调用过程中信息的传递,实现方式是通过gRPC的拦截器实现。

举个例子:假设我们有一个服务端A,外部请求过来先到网关或者k8s做一遍代理添加一些信息(比如版本信息等等)在metadata中,然后请求转发到了服务端A,这时候服务端A通过gRPC服务端拦截器可以获取到请求中的metadata,假设请求过程中服务端A需要调用其他的gRPC服务,那么此刻A就变成了客户端,我们要去调用服务端B(相当于重新发了一个请求),这时就可以用A中的客户端拦截器将之前的请求中的metadata写到新的请求中去,实现元数据信息的传递。

服务端拦截器

1、作用时机?

请求被具体的Handler相应前。

2、可以做什么?

编辑元数据

@GrpcGlobalServerInterceptor
public class GrpcServerInterceptor implements ServerInterceptor {
​
    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(ServerCall<ReqT, RespT> serverCall, Metadata metadata, ServerCallHandler<ReqT, RespT> serverCallHandler) {
        return new ForwardingServerCallListener.SimpleForwardingServerCallListener<ReqT>(serverCallHandler.startCall(serverCall, metadata)) {
            //......
            //将metadata信息保存在ThreadLocal在客户端拦截器中传递下去
            String roleId = metadata.get(Metadata.Key.of("roleid", Metadata.ASCII_STRING_MARSHALLER));
            if (StrUtil.isNotBlank(roleId)) {
                ThreadLocalUtil.put(roleId);
            }
            //.......
            }
        };
    }
}

通过@GrpcGlobalServerInterceptor注解并继承ServerInterceptor可以实现gRPC服务端的拦截器,重写其方法就能将实现获取metadata信息并保存在我们定义的容器中,这里我采用的是ThreadLocal。下面的客户端拦截器同理。

客户端拦截器

1、作用时机?

请求被分发出去之前。

2、可以做什么?

添加请求头数据、以便代理转发使用

@GrpcGlobalClientInterceptor
public class GrpcClientInterceptor implements ClientInterceptor {
    @Override
    public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(MethodDescriptor<ReqT, RespT> methodDescriptor, CallOptions callOptions, Channel channel) {
        return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(channel.newCall(methodDescriptor, callOptions)) {
​
            @Override
            public void start(Listener<RespT> responseListener, Metadata headers) {
                //获取保存在ThreadLocal中的元数据
                String roleId = ThreadLocalUtil.get();
                if (StrUtil.isNotBlank(roleId)) {
                    headers.put(Metadata.Key.of("roleid", Metadata.ASCII_STRING_MARSHALLER), roleId);
                    ThreadLocalUtil.remove();
                }
                super.start(responseListener, headers);
            }
        };
    }
}

3、踩坑经历

错误的代码示范:

return new ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(channel.newCall(methodDescriptor, callOptions)) {
​
    @Override
    public void start(Listener<RespT> responseListener, Metadata headers) {
        super.start(new ForwardingClientCallListener.SimpleForwardingClientCallListener<RespT>(responseListener) {
            @Override
            public void onHeaders(Metadata headers) {
                headers.put(Metadata.Key.of("test-bin", Metadata.ASCII_STRING_MARSHALLER), "test");
                headers = (Metadata) ThreadLocalUtil.get("metadata");
                System.out.println(System.currentTimeMillis() + "/client.metadata.toString() = " + headers.toString());
                super.onHeaders(headers);
            }
        }, headers);
    }
};

在使用该错误代码时会发生一个奇怪的现象:我的服务端接收请求后服务端拦截器正常响应,然后当服务端要去调别的grpc服务时应该是先走自己的客户端拦截器,然后再被我们请求的那个服务的服务端拦截器拦截,但测试后这两个顺序却反了过来,我们请求的服务的服务端拦截器先拦截了请求,然后才是自己的客户端拦截器拦截请求,这时候就算把版本信息写到metadata里也没用了,因为请求的那个服务端已经响应完了自己才把信息写到metadata里。经过分析发现问题出在new responseListener中,他是一个响应监听器,说明他是后置的,只有我请求的那个服务端响应了他才会去执行onHeaders方法,这时候已经晚了。

解决的办法就是不去new这个responseListener,在start()方法里就去设置metadata的信息,这样的话就可以不用等响应直接设置好版本信息在发送请求,然后才是被请求的服务端拦截器拦截,这时候就可以正常的在metadata中拿到版本信息了。

问题记录

  1. -bin是干什么用的以及-bin的乱码问题

    注意 HTTP2 并不允许随意使用字节序列来作为报头值,所以二进制的报头值必须使用 Base64 来编码,参见链接。 实现必须接受填充的和非填充的值,并且发出非填充的值。应用以“-bin”结尾的名称来定义二进制报头。运行时库在报头被发送和接收时,用这个后缀来检测二进制报头并且正确地在报头被发送和接收时进行 Base64 编码和解码。参考:HTTP2 协议上的 gRPC

    乱码问题:在调试的时候输出元数据信息发现已-bin结尾的key的值输出来是乱码,应该是因为上面说到的 Base64 编码的问题,因为打断点看他的byte数组值是正确的

    image-20220915163629217

    将其通过utf-8格式输出也是对的:

    image-20220915163724925

  2. (Metadata.Key.of("roleId-bin", Metadata.ASCII_STRING_MARSHALLER)报错:

    ASCII header is named roleId-bin. Only binary headers may end with -bin,以-bin结尾的key只能使用binary headers。