关于protocol buffers:如何直接从protobuf创建GRPC客户端而不用编译成java代码

How to create GRPC client directly from protobuf without compiling it into java code

使用 GRPC 时,我们需要通过协议缓冲区编译器 (protoc) 或使用 Gradle 或 Maven protoc 构建插件从我们的 .proto 服务定义生成 gRPC 客户端和服务器接口。

1
Flow now: protobuf file -> java code -> gRPC client.

那么,有没有办法跳过这一步?

如何创建一个通用的GRPC客户端,可以直接从protobuf文件调用服务器而不编译成java代码?
或者,有没有办法在运行时生成代码?

1
Flow expect: protobuf file -> gRPC client.

我想构建一个通用的 gRPC 客户端系统,输入是 protobuf 文件以及方法、包、消息请求的描述......而不必为每个 protobuf 重新编译。

非常感谢。


Protobuf 系统确实需要运行 protoc。但是,可以跳过生成的代码。除了将 --java_out--grpc_java_out 之类的东西传递给 protoc 之外,您还可以传递 --descriptor_set_out=FILE ,它会将 .proto 文件解析为描述符文件。描述符文件是原始编码的 FileDescriptorSet。这与反射服务使用的基本格式相同。

一旦你有了一个描述符,你就可以一次加载一个 FileDescriptor 并创建一个 DynamicMessage。

那么对于 gRPC 部分,您需要创建一个 gRPC MethodDescriptor。

1
2
3
4
5
6
7
8
9
10
11
MethodDescriptor.<DynamicMessage, DynamicMessage>newBuilder()
    // UNKNOWN is fine, but the"correct" value can be computed from
    // methodDesc.toProto().getClientStreaming()/getServerStreaming()
    .setType(MethodDescriptor.MethodType.UNKNOWN)
    .setFullMethodName(MethodDescriptor.generateFullMethodName(
        serviceDesc.getFullName(), methodDesc.getName()))
    .setRequestMarshaller(ProtoUtils.marshaller(
        DynamicMessage.newBuilder(methodDesc.getInputType()).buildPartial()))
    .setResponseMarshaller(ProtoUtils.marshaller(
        DynamicMessage.newBuilder(methodDesc.getOutputType()).buildPartial()))
    .build();

此时你已经拥有了你需要的一切,并且可以在 gRPC 中调用 Channel.newCall(method, CallOptions.DEFAULT)。您还可以自由地使用 ClientCalls 来使用更类似于Stubbing API 的东西。

所以动态调用绝对是可能的,并且用于诸如grpcurl之类的东西。但这也不是一件容易的事,所以一般只在必要时才做。


我是用Java做的,步骤是:

  • 调用反射服务通过方法名获取FileDescriptorProto列表
  • 通过包名、服务名从 FileDescriptorProto 列表中获取方法的 FileDescriptor
  • ServiceDescriptor 中获取 MethodDescriptor,从 FileDescriptor 中获取
  • 通过 MethodDescriptor 生成一个 MethodDescriptor<DynamicMessage, DynamicMessage>
  • 从 JSON 或其他内容构建请求 DynamicMessage
  • 调用方法
  • 将响应内容从 DynamicMessage 响应解析为 JSON
  • 您可以在项目 helloworlde/grpc-java-sample#reflection

    中参考完整示例

    而proto是:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    syntax ="proto3";

    package io.github.helloworlde.grpc;

    option go_package ="api;grpc_gateway";
    option java_package ="io.github.helloworlde.grpc";
    option java_multiple_files = true;
    option java_outer_classname ="HelloWorldGrpc";

    service HelloService{
      rpc SayHello(HelloMessage) returns (HelloResponse){
      }
    }

    message HelloMessage {
      string message = 2;
    }

    message HelloResponse {
      string message = 1;
    }

    为这个 proto 自己启动服务器,完整的 Java 代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    import com.google.protobuf.ByteString;
    import com.google.protobuf.DescriptorProtos;
    import com.google.protobuf.Descriptors;
    import com.google.protobuf.DynamicMessage;
    import com.google.protobuf.InvalidProtocolBufferException;
    import com.google.protobuf.TypeRegistry;
    import com.google.protobuf.util.JsonFormat;
    import io.grpc.CallOptions;
    import io.grpc.ManagedChannel;
    import io.grpc.ManagedChannelBuilder;
    import io.grpc.MethodDescriptor;
    import io.grpc.protobuf.ProtoUtils;
    import io.grpc.reflection.v1alpha.ServerReflectionGrpc;
    import io.grpc.reflection.v1alpha.ServerReflectionRequest;
    import io.grpc.reflection.v1alpha.ServerReflectionResponse;
    import io.grpc.stub.ClientCalls;
    import io.grpc.stub.StreamObserver;
    import lombok.SneakyThrows;
    import lombok.extern.slf4j.Slf4j;

    import java.util.List;
    import java.util.Map;
    import java.util.Objects;
    import java.util.concurrent.TimeUnit;
    import java.util.stream.Collectors;

    @Slf4j
    public class ReflectionCall {

        public static void main(String[] args) throws InterruptedException {
            // ????°??–1?3???????????a?"ˉ??? package.service.method ??–者 package.service
            String methodSymbol ="io.github.helloworlde.grpc.HelloService.SayHello";
            String requestContent ="{"message": "Reflection"}";

            // ?????o Channel
            ManagedChannel channel = ManagedChannelBuilder.forAddress("127.0.0.1", 9090)
                                                          .usePlaintext()
                                                          .build();
            // ????"¨ Channel ?????o BlockingStub
            ServerReflectionGrpc.ServerReflectionStub reflectionStub = ServerReflectionGrpc.newStub(channel);
            // ?"??o"è§??ˉ???¨
            StreamObserver<ServerReflectionResponse> streamObserver = new StreamObserver<ServerReflectionResponse>() {
                @Override
                public void onNext(ServerReflectionResponse response) {
                    try {
                        // ??aé?€è|??…3?3¨?–???????è?°?±????????"??o"
                        if (response.getMessageResponseCase() == ServerReflectionResponse.MessageResponseCase.FILE_DESCRIPTOR_RESPONSE) {
                            List<ByteString> fileDescriptorProtoList = response.getFileDescriptorResponse().getFileDescriptorProtoList();
                            handleResponse(fileDescriptorProtoList, channel, methodSymbol, requestContent);
                        } else {
                            log.warn("??a??¥?"??o"?±????:" + response.getMessageResponseCase());
                        }
                    } catch (Exception e) {
                        log.error("?¤?????"??o"?¤±è′¥: {}", e.getMessage(), e);
                    }
                }

                @Override
                public void onError(Throwable t) {

                }

                @Override
                public void onCompleted() {
                    log.info("Complete");
                }
            };
            // èˉ·?±?è§??ˉ???¨
            StreamObserver<ServerReflectionRequest> requestStreamObserver = reflectionStub.serverReflectionInfo(streamObserver);

            // ?????o?1???‘é€?è?·??–?–1?3??–???????è?°èˉ·?±?
            ServerReflectionRequest getFileContainingSymbolRequest = ServerReflectionRequest.newBuilder()
                                                                                            .setFileContainingSymbol(methodSymbol)
                                                                                            .build();
            requestStreamObserver.onNext(getFileContainingSymbolRequest);
            channel.awaitTermination(10, TimeUnit.SECONDS);
        }

        /**
         * ?¤?????"??o"
         */

        private static void handleResponse(List<ByteString> fileDescriptorProtoList,
                                           ManagedChannel channel,
                                           String methodFullName,
                                           String requestContent) {
            try {
                // 解????–1?3??’???????????§°
                String fullServiceName = extraPrefix(methodFullName);
                String methodName = extraSuffix(methodFullName);
                String packageName = extraPrefix(fullServiceName);
                String serviceName = extraSuffix(fullServiceName);

                // ?1????"??o"解??? FileDescriptor
                Descriptors.FileDescriptor fileDescriptor = getFileDescriptor(fileDescriptorProtoList, packageName, serviceName);

                // ??¥?‰??????????è?°
                Descriptors.ServiceDescriptor serviceDescriptor = fileDescriptor.getFile().findServiceByName(serviceName);
                // ??¥?‰??–1?3????è?°
                Descriptors.MethodDescriptor methodDescriptor = serviceDescriptor.findMethodByName(methodName);

                // ??‘èμ·èˉ·?±?
                executeCall(channel, fileDescriptor, methodDescriptor, requestContent);
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
        }

        /**
         * 解????1???¥?‰??–1?3??ˉ1?o"????–???????è?°
         */

        private static Descriptors.FileDescriptor getFileDescriptor(List<ByteString> fileDescriptorProtoList,
                                                                    String packageName,
                                                                    String serviceName) throws Exception {

            Map<String, DescriptorProtos.FileDescriptorProto> fileDescriptorProtoMap =
                    fileDescriptorProtoList.stream()
                                           .map(bs -> {
                                               try {
                                                   return DescriptorProtos.FileDescriptorProto.parseFrom(bs);
                                               } catch (InvalidProtocolBufferException e) {
                                                   e.printStackTrace();
                                               }
                                               return null;
                                           })
                                           .filter(Objects::nonNull)
                                           .collect(Collectors.toMap(DescriptorProtos.FileDescriptorProto::getName, f -> f));


            if (fileDescriptorProtoMap.isEmpty()) {
                log.error("??????????-???¨");
                throw new IllegalArgumentException("?–1?3?????–???????è?°????-???¨");
            }

            // ??¥?‰????????ˉ1?o"??? Proto ???è?°
            DescriptorProtos.FileDescriptorProto fileDescriptorProto = findServiceFileDescriptorProto(packageName, serviceName, fileDescriptorProtoMap);

            // è?·??–è????a Proto ??????èμ–
            Descriptors.FileDescriptor[] dependencies = getDependencies(fileDescriptorProto, fileDescriptorProtoMap);

            // ?"???? Proto ??? FileDescriptor
            return Descriptors.FileDescriptor.buildFrom(fileDescriptorProto, dependencies);
        }


        /**
         * ?1?????…????’????????????¥?‰?????o"????–???????è?°
         */

        private static DescriptorProtos.FileDescriptorProto findServiceFileDescriptorProto(String packageName,
                                                                                           String serviceName,
                                                                                           Map<String, DescriptorProtos.FileDescriptorProto> fileDescriptorProtoMap) {
            for (DescriptorProtos.FileDescriptorProto proto : fileDescriptorProtoMap.values()) {
                if (proto.getPackage().equals(packageName)) {
                    boolean exist = proto.getServiceList()
                                         .stream()
                                         .anyMatch(s -> serviceName.equals(s.getName()));
                    if (exist) {
                        return proto;
                    }
                }
            }

            throw new IllegalArgumentException("??????????-???¨");
        }

        /**
         * è?·??–?‰???€
         */

        private static String extraPrefix(String content) {
            int index = content.lastIndexOf(".");
            return content.substring(0, index);
        }

        /**
         * è?·??–?????€
         */

        private static String extraSuffix(String content) {
            int index = content.lastIndexOf(".");
            return content.substring(index + 1);
        }

        /**
         * è?·??–???èμ–?±????
         */

        private static Descriptors.FileDescriptor[] getDependencies(DescriptorProtos.FileDescriptorProto proto,
                                                                    Map<String, DescriptorProtos.FileDescriptorProto> finalDescriptorProtoMap) {
            return proto.getDependencyList()
                        .stream()
                        .map(finalDescriptorProtoMap::get)
                        .map(f -> toFileDescriptor(f, getDependencies(f, finalDescriptorProtoMap)))
                        .toArray(Descriptors.FileDescriptor[]::new);
        }

        /**
         * ?°? FileDescriptorProto è????o FileDescriptor
         */

        @SneakyThrows
        private static Descriptors.FileDescriptor toFileDescriptor(DescriptorProtos.FileDescriptorProto fileDescriptorProto,
                                                                   Descriptors.FileDescriptor[] dependencies) {
            return Descriptors.FileDescriptor.buildFrom(fileDescriptorProto, dependencies);
        }


        /**
         * ?‰§è???–1?3?è°??"¨
         */

        private static void executeCall(ManagedChannel channel,
                                        Descriptors.FileDescriptor fileDescriptor,
                                        Descriptors.MethodDescriptor originMethodDescriptor,
                                        String requestContent) throws Exception {

            // é???–°?"???? MethodDescriptor
            MethodDescriptor<DynamicMessage, DynamicMessage> methodDescriptor = generateMethodDescriptor(originMethodDescriptor);

            CallOptions callOptions = CallOptions.DEFAULT;

            TypeRegistry registry = TypeRegistry.newBuilder()
                                                .add(fileDescriptor.getMessageTypes())
                                                .build();

            // ?°?èˉ·?±???…??1?"± JSON ?-—??|??2è????o????o"????±????
            JsonFormat.Parser parser = JsonFormat.parser().usingTypeRegistry(registry);
            DynamicMessage.Builder messageBuilder = DynamicMessage.newBuilder(originMethodDescriptor.getInputType());
            parser.merge(requestContent, messageBuilder);
            DynamicMessage requestMessage = messageBuilder.build();

            // è°??"¨???è°??"¨?–1?????ˉ??¥é€?è?? originMethodDescriptor.isClientStreaming() ?’? originMethodDescriptor.isServerStreaming() ??¨?–-
            DynamicMessage response = ClientCalls.blockingUnaryCall(channel, methodDescriptor, callOptions, requestMessage);

            // ?°??"??o"解?????o JSON ?-—??|??2
            JsonFormat.Printer printer = JsonFormat.printer()
                                                   .usingTypeRegistry(registry)
                                                   .includingDefaultValueFields();
            String responseContent = printer.print(response);

            log.info("?"??o": {}", responseContent);
        }

        /**
         * é???–°?"?????–1?3????è?°
         */

        private static MethodDescriptor<DynamicMessage, DynamicMessage> generateMethodDescriptor(Descriptors.MethodDescriptor originMethodDescriptor) {
            // ?"?????–1?3??…¨???
            String fullMethodName = MethodDescriptor.generateFullMethodName(originMethodDescriptor.getService().getFullName(), originMethodDescriptor.getName());
            // èˉ·?±??’??"??o"?±????
            MethodDescriptor.Marshaller<DynamicMessage> inputTypeMarshaller = ProtoUtils.marshaller(DynamicMessage.newBuilder(originMethodDescriptor.getInputType())
                                                                                                                  .buildPartial());
            MethodDescriptor.Marshaller<DynamicMessage> outputTypeMarshaller = ProtoUtils.marshaller(DynamicMessage.newBuilder(originMethodDescriptor.getOutputType())
                                                                                                                   .buildPartial());

            // ?"?????–1?3????è?°, originMethodDescriptor ??? fullMethodName ????-£???
            return MethodDescriptor.<DynamicMessage, DynamicMessage>newBuilder()
                    .setFullMethodName(fullMethodName)
                    .setRequestMarshaller(inputTypeMarshaller)
                    .setResponseMarshaller(outputTypeMarshaller)
                    // ????"¨ UNKNOWN???è?a??¨????"1
                    .setType(MethodDescriptor.MethodType.UNKNOWN)
                    .build();
        }
    }

    在技术上没有太多可以防止这种情况发生。两大障碍是:

  • 具有用于读取 .proto 的运行时可调用解析器,以及
  • 有一个通用的 gRPC 客户端可用,它将服务方法名称等内容作为文字
  • 两者都有可能,但都不是微不足道的。

    对于1,粗略的方法是使用descriptor-set选项来shell/invoke protoc以生成模式二进制文件,然后将其反序列化为FileDescriptorSet(来自descriptor.proto);此模型使您可以访问 protoc 如何查看文件。一些平台也有本机解析器(本质上是重新实现 protoc 作为该平台中的库),例如 protobuf-net.Reflection 在 .NET-land

    中执行此操作

    对于 2,这是 C# 中的一个实现。即使细节有所不同,该方法也应该相当可移植到 Java。您可以查看生成的实现以了解它在任何特定语言中的工作方式。

    (抱歉,具体示例是 C#/.NET,但那是我住的地方;方法应该是可移植的,即使具体代码:不是直接的)


    技术上两者都是可能的。

    代码生成器只是生成少数类;主要是 protobuf 消息、grpc 方法描述符和Stubbing。您可以实现它或签入生成的代码以绕过 codegen。我不确定这样做有什么好处。另外,如果更改了 proto 会很烦人。

    只要您签入一些接口/抽象类来表示那些生成的Stubbing/方法描述符和protobuf消息,也可以使用字节码生成动态地执行此操作。您必须确保那些非动态代码与原型定义同步(很可能是运行时检查/异常)。