【grpc】option java_generic_services = true; が非推奨の理由とは？

# このページは？
gRPCを勉強している際に`option java_generic_services = true`が非推奨になっているが理由が不明だった。　そこで、`option java_generic_services`が`true`と`false`(default)の時で、スタブにどのような違いが出てくるかについて調査した時の記録を紹介するものである

調査記録を紹介するにあたって、以下の流れで進めていく
1. grpcの利用する流れを紹介
1. ↑の具体的なサンプルを紹介
1. 具体的なサンプルの`java_generic_services`をtrueにしてみてスタブにどのような違いが出るかを見てみる

# grpcとは？
> - Google が開発した RPC ( Remote Procedure Call )システム
> - Protocol Buffers と呼ばれるインタフェース記述言語(IDL)でインタフェースを定義する
> - バイナリファイルでデータの送受信がされるためRESTなどで用いられるjsonに比べて軽量である
> - 様々な言語に変換可能である
:::note 
gRPCのgはgoogleのgではなく、versionによって表すものが変わる
引用：https://github.com/grpc/grpc/blob/master/doc/g_stands_for.md
:::
# grpcを利用する流れ
1. `.proto`ファイルにデータの定義を行う
2. protocを用いて特定のプログラミング言語に応じてスタブを作成する
3. 作成したスタブを用いて、指定した開発言語のオブジェクトを生成する
    1. サーバープログラムを作成する
    2. クライアントプログラムを作成する
4. ↑のプログラムを実行する
# grpcの利用する流れを実際に体験してみる
今回は、`option java_generic_services = true;`を説明するのが目的のため、クライアントプログラムは、grpcで代用する
なので、作成するファイルは以下の３つになっている
1. EchoService.proto
1. EchoServer.java
1. pom.xml

また、ディレクトリ構成は以下のようになる。
```console
.
├── pom.xml
└── src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           └── grpc
        │               └── server
        │                   └── EchoServer.java
        └── proto
            └── EchoService.proto
```
## `.proto`ファイルにデータの定義を行う

```proto:EchoService.proto
syntax = "proto3";

package com.example.grpc;

option java_multiple_files = true;

message EchoRequest {
    string message = 1;
}

message EchoResponse {
    string message = 1;
    string from = 2;
}

service EchoService {
    rpc echo(EchoRequest) returns (EchoResponse);
}
```
## protocを用いて特定のプログラミング言語に応じてスタブを作成する
Mavenでスタブを作成する。
generate-sources フェーズで `protoc` が実行される
```console
$ mvn package
```

ちなみに `pom.xml`は以下のように記述している

<details><summary>pom.xml</summary>

```xml:pom.xml
<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <artifactId>echo-server</artifactId>

    <version>1.0-SNAPSHOT</version>
    <groupId>com.example.grpc</groupId>
    <packaging>jar</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <grpc.version>1.64.0</grpc.version>
        <protobuf.version>3.25.3</protobuf.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-protobuf</artifactId>
            <version>${grpc.version}</version>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-netty</artifactId>
            <version>${grpc.version}</version>
        </dependency>
        <dependency>
            <groupId>io.grpc</groupId>
            <artifactId>grpc-stub</artifactId>
            <version>${grpc.version}</version>
        </dependency>
        <dependency>
            <groupId>com.google.protobuf</groupId>
            <artifactId>protobuf-java</artifactId>
            <version>${protobuf.version}</version>
        </dependency>
        <dependency>
            <groupId>javax.annotation</groupId>
            <artifactId>javax.annotation-api</artifactId>
            <version>1.3.2</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <finalName>echo-server</finalName>
        <extensions>
            <extension>
                <groupId>kr.motd.maven</groupId>
                <artifactId>os-maven-plugin</artifactId>
                <version>1.7.0</version>
            </extension>
        </extensions>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.11.0</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                </configuration>
            </plugin>
            <!-- .protoからJavaに変換 -->
            <plugin>
                <groupId>org.xolstice.maven.plugins</groupId>
                <artifactId>protobuf-maven-plugin</artifactId>
                <version>0.6.1</version>
                <configuration>
                    <protocArtifact>com.google.protobuf:protoc:3.25.3:exe:${os.detected.classifier}</protocArtifact>
                    <pluginId>grpc-java</pluginId>
                    <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.64.0:exe:${os.detected.classifier}</pluginArtifact>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>compile-custom</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <!-- 実行可能なJARを作成 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.6.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <createDependencyReducedPom>false</createDependencyReducedPom>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass>com.example.grpc.server.EchoServer</mainClass>
                                </transformer>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                                    <resource>META-INF/io.netty.versions.properties</resource>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

```
</details>


## 作成したスタブを用いて、指定した開発言語のオブジェクトを生成する
### サーバープログラム
```java
package com.example.grpc.server;

import com.example.grpc.EchoRequest;
import com.example.grpc.EchoResponse;
import com.example.grpc.EchoServiceGrpc;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;

import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;

public class EchoServer {
  static public void main(String[] args) throws IOException, InterruptedException {

    Server server = ServerBuilder.forPort(8080)
        .addService(new EchoServiceImpl()).build();

    System.out.println("Starting server...");
    server.start();
    System.out.println("Server started!");
    server.awaitTermination();
  }
}

// protoから作成したスタブを利用してサーバープログラムを実装
class EchoServiceImpl extends EchoServiceGrpc.EchoServiceImplBase {

  @Override
  public void echo(EchoRequest request, StreamObserver<EchoResponse> responseObserver) {
    try {
      String from = InetAddress.getLocalHost().getHostAddress();
      System.out.println("Received: " + request.getMessage());
      responseObserver.onNext(EchoResponse.newBuilder()
          .setFrom(from)
          .setMessage(request.getMessage())
          .build());
      responseObserver.onCompleted();
    } catch (UnknownHostException e) {
      responseObserver.onError(e);
    }
  }
}
```

### クライアントプログラム
（再掲）今回は、`grpcurl`で代用するので省略


## ↑のプログラムを実行する
```console
$ java -jar target/echo-server.jar &
```

```console
$ grpcurl --plaintext \
    -d '{"message": "hello world!"}' \
    -proto src/main/proto/EchoService.proto \
    localhost:8080 com.example.grpc.EchoService.echo
{
  "message": "hello world!",
  "from": "127.0.0.1"
}
```

とりあえずプログラムは実行できたので、protoに`option java_generic_services = true;
`を追加して、スタブにどのような変化があるかをみていく

# protoに`option java_generic_services = true;`を追加してみる
まずは、`option java_generic_services = true;`なしの状態でスタブを作成する
そして、差分を見やすくするためにgitで管理する
```
$ mvn clean package
$ git init
$ git add -A
$ git commit -m "first commit"
```
`option java_generic_services = true;`をつける
```diff:EchoService.proto
syntax = "proto3";

package com.example.grpc;

option java_multiple_files = true;
+ option java_generic_services = true;

message EchoRequest {
    string message = 1;
}

message EchoResponse {
    string message = 1;
    string from = 2;
}

service EchoService {
    rpc echo(EchoRequest) returns (EchoResponse);
}
```

```
$ mvn clean package
$ git add -A
$ git commit -m "modify proto"
$ git status  target/generated-sources/protobuf                                        
On branch master
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   target/generated-sources/protobuf/java/com/example/grpc/EchoService.java
        modified:   target/generated-sources/protobuf/java/com/example/grpc/EchoServiceOuterClass.java
```
`target/generated-sources/protobuf`の配下を見てみると、
- EchoService.javaが新規作成されて
- EchoServiceOuterClass.javaが変更されていた

## EchoService.javaを見てみる

<details><summary>EchoService.java全文</summary>

```java
// Generated by the protocol buffer compiler.  DO NOT EDIT!
// source: EchoService.proto

// Protobuf Java Version: 3.25.3
package com.example.grpc;

/**
 * Protobuf service {@code com.example.grpc.EchoService}
 */
public  abstract class EchoService
    implements com.google.protobuf.Service {
  protected EchoService() {}

  public interface Interface {
    /**
     * <code>rpc echo(.com.example.grpc.EchoRequest) returns (.com.example.grpc.EchoResponse);</code>
     */
    public abstract void echo(
        com.google.protobuf.RpcController controller,
        com.example.grpc.EchoRequest request,
        com.google.protobuf.RpcCallback<com.example.grpc.EchoResponse> done);

  }

  public static com.google.protobuf.Service newReflectiveService(
      final Interface impl) {
    return new EchoService() {
      @java.lang.Override
      public  void echo(
          com.google.protobuf.RpcController controller,
          com.example.grpc.EchoRequest request,
          com.google.protobuf.RpcCallback<com.example.grpc.EchoResponse> done) {
        impl.echo(controller, request, done);
      }

    };
  }

  public static com.google.protobuf.BlockingService
      newReflectiveBlockingService(final BlockingInterface impl) {
    return new com.google.protobuf.BlockingService() {
      public final com.google.protobuf.Descriptors.ServiceDescriptor
          getDescriptorForType() {
        return getDescriptor();
      }

      public final com.google.protobuf.Message callBlockingMethod(
          com.google.protobuf.Descriptors.MethodDescriptor method,
          com.google.protobuf.RpcController controller,
          com.google.protobuf.Message request)
          throws com.google.protobuf.ServiceException {
        if (method.getService() != getDescriptor()) {
          throw new java.lang.IllegalArgumentException(
            "Service.callBlockingMethod() given method descriptor for " +
            "wrong service type.");
        }
        switch(method.getIndex()) {
          case 0:
            return impl.echo(controller, (com.example.grpc.EchoRequest)request);
          default:
            throw new java.lang.AssertionError("Can't get here.");
        }
      }

      public final com.google.protobuf.Message
          getRequestPrototype(
          com.google.protobuf.Descriptors.MethodDescriptor method) {
        if (method.getService() != getDescriptor()) {
          throw new java.lang.IllegalArgumentException(
            "Service.getRequestPrototype() given method " +
            "descriptor for wrong service type.");
        }
        switch(method.getIndex()) {
          case 0:
            return com.example.grpc.EchoRequest.getDefaultInstance();
          default:
            throw new java.lang.AssertionError("Can't get here.");
        }
      }

      public final com.google.protobuf.Message
          getResponsePrototype(
          com.google.protobuf.Descriptors.MethodDescriptor method) {
        if (method.getService() != getDescriptor()) {
          throw new java.lang.IllegalArgumentException(
            "Service.getResponsePrototype() given method " +
            "descriptor for wrong service type.");
        }
        switch(method.getIndex()) {
          case 0:
            return com.example.grpc.EchoResponse.getDefaultInstance();
          default:
            throw new java.lang.AssertionError("Can't get here.");
        }
      }

    };
  }

  /**
   * <code>rpc echo(.com.example.grpc.EchoRequest) returns (.com.example.grpc.EchoResponse);</code>
   */
  public abstract void echo(
      com.google.protobuf.RpcController controller,
      com.example.grpc.EchoRequest request,
      com.google.protobuf.RpcCallback<com.example.grpc.EchoResponse> done);

  public static final
      com.google.protobuf.Descriptors.ServiceDescriptor
      getDescriptor() {
    return com.example.grpc.EchoServiceOuterClass.getDescriptor().getServices().get(0);
  }
  public final com.google.protobuf.Descriptors.ServiceDescriptor
      getDescriptorForType() {
    return getDescriptor();
  }

  public final void callMethod(
      com.google.protobuf.Descriptors.MethodDescriptor method,
      com.google.protobuf.RpcController controller,
      com.google.protobuf.Message request,
      com.google.protobuf.RpcCallback<
        com.google.protobuf.Message> done) {
    if (method.getService() != getDescriptor()) {
      throw new java.lang.IllegalArgumentException(
        "Service.callMethod() given method descriptor for wrong " +
        "service type.");
    }
    switch(method.getIndex()) {
      case 0:
        this.echo(controller, (com.example.grpc.EchoRequest)request,
          com.google.protobuf.RpcUtil.<com.example.grpc.EchoResponse>specializeCallback(
            done));
        return;
      default:
        throw new java.lang.AssertionError("Can't get here.");
    }
  }

  public final com.google.protobuf.Message
      getRequestPrototype(
      com.google.protobuf.Descriptors.MethodDescriptor method) {
    if (method.getService() != getDescriptor()) {
      throw new java.lang.IllegalArgumentException(
        "Service.getRequestPrototype() given method " +
        "descriptor for wrong service type.");
    }
    switch(method.getIndex()) {
      case 0:
        return com.example.grpc.EchoRequest.getDefaultInstance();
      default:
        throw new java.lang.AssertionError("Can't get here.");
    }
  }

  public final com.google.protobuf.Message
      getResponsePrototype(
      com.google.protobuf.Descriptors.MethodDescriptor method) {
    if (method.getService() != getDescriptor()) {
      throw new java.lang.IllegalArgumentException(
        "Service.getResponsePrototype() given method " +
        "descriptor for wrong service type.");
    }
    switch(method.getIndex()) {
      case 0:
        return com.example.grpc.EchoResponse.getDefaultInstance();
      default:
        throw new java.lang.AssertionError("Can't get here.");
    }
  }

  public static Stub newStub(
      com.google.protobuf.RpcChannel channel) {
    return new Stub(channel);
  }

  public static final class Stub extends com.example.grpc.EchoService implements Interface {
    private Stub(com.google.protobuf.RpcChannel channel) {
      this.channel = channel;
    }

    private final com.google.protobuf.RpcChannel channel;

    public com.google.protobuf.RpcChannel getChannel() {
      return channel;
    }

    public  void echo(
        com.google.protobuf.RpcController controller,
        com.example.grpc.EchoRequest request,
        com.google.protobuf.RpcCallback<com.example.grpc.EchoResponse> done) {
      channel.callMethod(
        getDescriptor().getMethods().get(0),
        controller,
        request,
        com.example.grpc.EchoResponse.getDefaultInstance(),
        com.google.protobuf.RpcUtil.generalizeCallback(
          done,
          com.example.grpc.EchoResponse.class,
          com.example.grpc.EchoResponse.getDefaultInstance()));
    }
  }

  public static BlockingInterface newBlockingStub(
      com.google.protobuf.BlockingRpcChannel channel) {
    return new BlockingStub(channel);
  }

  public interface BlockingInterface {
    public com.example.grpc.EchoResponse echo(
        com.google.protobuf.RpcController controller,
        com.example.grpc.EchoRequest request)
        throws com.google.protobuf.ServiceException;
  }

  private static final class BlockingStub implements BlockingInterface {
    private BlockingStub(com.google.protobuf.BlockingRpcChannel channel) {
      this.channel = channel;
    }

    private final com.google.protobuf.BlockingRpcChannel channel;

    public com.example.grpc.EchoResponse echo(
        com.google.protobuf.RpcController controller,
        com.example.grpc.EchoRequest request)
        throws com.google.protobuf.ServiceException {
      return (com.example.grpc.EchoResponse) channel.callBlockingMethod(
        getDescriptor().getMethods().get(0),
        controller,
        request,
        com.example.grpc.EchoResponse.getDefaultInstance());
    }

  }

  // @@protoc_insertion_point(class_scope:com.example.grpc.EchoService)
}

```
</details>

> Starting with version 2.3.0, RPC implementations should not try to build on this, but should instead provide code generator plugins which generate code specific to the particular RPC implementation. 
[com.google.protobuf Interface Service](https://protobuf.dev/reference/java/api-docs/com/google/protobuf/Service.html#:~:text=Starting%20with%20version%202.3.0,%20RPC%20implementations%20should%20not%20try%20to%20build%20on%20this,%20but%20should%20instead%20provide%20code%20generator%20plugins%20which%20generate%20code%20specific%20to%20the%20particular%20RPC%20implementation.)

RPC 2.3.0以降はこのinterfaceを実装するのではなく、言語に応じてスタブを生成するプラグインを利用するのが推奨になった。
つまり、このファイル(EchoServiece)は、RPC 2.2.xまでで利用されていた形式。

詳細は割愛するが、この汎用APIの実装方式の場合はgRPCと比較して以下のような違いがある
| 観点 | 旧方式（汎用RPC） | 新方式（gRPCで主に利用） |
| --- | ----------------- | ------------------------ |
| コネクション管理・再試行 | 利用者が `RpcChannel` などで実装 | `ManagedChannel` や `CallOptions` に組み込まれており、再試行やタイムアウトも設定可能 |
| ロードバランシング & 名前解決 | 外部ロードバランサや独自実装に依存 | `PickFirst` や `RoundRobin`、`xDS` による動的構成が gRPC 標準でサポートされている |
| 複数の言語での通信方法 | 各言語ごとに `RpcChannel` 相当の実装が必要 | `.proto` から各言語に対応したスタブを自動生成（公式サポート：Java、Go、Python、C++ など） |


## EchoServiceOuterClass.javaを見てみる
差分は以下のような感じ
```diff
$ git diff HEAD target/generated-sources/protobuf/java/com/example/grpc/EchoServiceOuterClass.java             
diff --git a/target/generated-sources/protobuf/java/com/example/grpc/EchoServiceOuterClass.java b/target/generated-sources/protobuf/java/com/example/grpc/EchoServiceOuterClass.java
index 7d88860..94be40b 100644
--- a/target/generated-sources/protobuf/java/com/example/grpc/EchoServiceOuterClass.java
+++ b/target/generated-sources/protobuf/java/com/example/grpc/EchoServiceOuterClass.java
@@ -39,7 +39,7 @@ public final class EchoServiceOuterClass {
       "ponse\022\017\n\007message\030\001 \001(\t\022\014\n\004from\030\002 \001(\t2T\n\013" +
       "EchoService\022E\n\004echo\022\035.com.example.grpc.E" +
       "choRequest\032\036.com.example.grpc.EchoRespon" +
-      "seB\002P\001b\006proto3"
+      "seB\005P\001\210\001\001b\006proto3"
     };
     descriptor = com.google.protobuf.Descriptors.FileDescriptor
       .internalBuildGeneratedFileFrom(descriptorData,
```
変更後のファイルは以下のようになっている
```java
  private static  com.google.protobuf.Descriptors.FileDescriptor
      descriptor;
  static {
    java.lang.String[] descriptorData = {
      "\n\021EchoService.proto\022\020com.example.grpc\"\036\n" +
      "\013EchoRequest\022\017\n\007message\030\001 \001(\t\"-\n\014EchoRes" +
      "ponse\022\017\n\007message\030\001 \001(\t\022\014\n\004from\030\002 \001(\t2T\n\013" +
      "EchoService\022E\n\004echo\022\035.com.example.grpc.E" +
      "choRequest\032\036.com.example.grpc.EchoRespon" +
      "seB\005P\001\210\001\001b\006proto3"
    };
    descriptor = com.google.protobuf.Descriptors.FileDescriptor
      .internalBuildGeneratedFileFrom(descriptorData,
        new com.google.protobuf.Descriptors.FileDescriptor[] {
        });
```
上記のコードのクラスは、`FileDescriptor`に変換してから、`FileDescriptor#toProto`で差分を比較してみると、`java_generic_services`のみが有効になっていることがわかった。

<details><summary>FileDescriptor#toProtoで差分を比較する</summary>

```java
import com.google.protobuf.Descriptors.FileDescriptor;

public class ShowFileOptions {
  public static void main(String[] args) {

      String[] descriptorData = {
      "\n\021EchoService.proto\022\020com.example.grpc\"\036\n" +
      "\013EchoRequest\022\017\n\007message\030\001 \001(\t\"-\n\014EchoRes" +
      "ponse\022\017\n\007message\030\001 \001(\t\022\014\n\004from\030\002 \001(\t2T\n\013" +
      "EchoService\022E\n\004echo\022\035.com.example.grpc.E" +
      "choRequest\032\036.com.example.grpc.EchoRespon" +
      "seB\002P\001b\006proto3"
    };

      String[] descriptorDataWithOption = {
      "\n\021EchoService.proto\022\020com.example.grpc\"\036\n" +
      "\013EchoRequest\022\017\n\007message\030\001 \001(\t\"-\n\014EchoRes" +
      "ponse\022\017\n\007message\030\001 \001(\t\022\014\n\004from\030\002 \001(\t2T\n\013" +
      "EchoService\022E\n\004echo\022\035.com.example.grpc.E" +
      "choRequest\032\036.com.example.grpc.EchoRespon" +
      "seB\005P\001\210\001\001b\006proto3"
    };

    FileDescriptor descriptor = com.google.protobuf.Descriptors.FileDescriptor
      .internalBuildGeneratedFileFrom(descriptorData,
        new com.google.protobuf.Descriptors.FileDescriptor[] {
        });

    FileDescriptor descriptorWithOption = com.google.protobuf.Descriptors.FileDescriptor
      .internalBuildGeneratedFileFrom(descriptorDataWithOption,
        new com.google.protobuf.Descriptors.FileDescriptor[] {
        });


    System.out.println(descriptorWithOption.toProto());
  }
}
```

</details>

```diff
name: "EchoService.proto"
package: "com.example.grpc"
message_type {
  name: "EchoRequest"
  field {
    name: "message"
    number: 1
    label: LABEL_OPTIONAL
    type: TYPE_STRING
  }
}
message_type {
  name: "EchoResponse"
  field {
    name: "message"
    number: 1
    label: LABEL_OPTIONAL
    type: TYPE_STRING
  }
  field {
    name: "from"
    number: 2
    label: LABEL_OPTIONAL
    type: TYPE_STRING
  }
}
service {
  name: "EchoService"
  method {
    name: "echo"
    input_type: ".com.example.grpc.EchoRequest"
    output_type: ".com.example.grpc.EchoResponse"
  }
}
options {
  java_multiple_files: true
+ java_generic_services: true
}
syntax: "proto3"
```

# まとめ
- `option java_generic_services = true;`は、gRPC Java がまだ正式リリース前だった時代の当時は `protoc-gen-grpc-java` が無く、「Protobuf の標準 Service API(汎用サービス API)で RPC を書く」方式だった
- その後、gRPC 公式の Java プラグイン（protoc-gen-grpc-java）が登場したため、汎用サービス APIで書く必要は無くなった
- なので、汎用サービス APIを使わない場合は`java_generic_services`で生成されるインタフェースはほとんど利用されず、コードベースに余計なファイルを増やすだけになってしまう（不要なファイルが増える）
- また、汎用サービスAPIを利用する場合は、自前で実装する場合が多くかなり使いずらい（grpcの場合はライブラリに任せられる）

# 参考
- [とほほのgRPC入門](https://www.tohoho-web.com/ex/grpc.html)