gRPC 双向函数 returns RequestType 而不是 ResponseType

gRPC bidirectional function returns RequestType instead of ResponseType

我正在使用官方文档学习 gRPC,但发现 client-streaming 和 bidirectional-streaming 的方法签名非常混乱(两者相同)。

根据文档here,该函数将StreamObserver<ResponseType>作为输入参数和returns一个StreamObserver<ResponseType>实例,如下所示:

public StreamObserver<RequestType> bidirectionalStreamingExample(
    StreamObserver<ResponseType> responseObserver)

但在我看来,它应该将 RequestType 类型作为输入,returns ResponseType 类型:

public StreamObserver<ResponseType> bidirectionalStreamingExample(
    StreamObserver<RequestType> responseObserver)

这让我很困惑,当我在 google 中搜索时,答案没有提示,我其实有点惊讶,我想很多人都会有同样的问题。我在这里遗漏了一些明显的东西吗?为什么 gRPC 会这样定义签名?

您的困惑可能源于习惯于 REST 或非流式框架,其中请求-响应通常映射到函数的参数-return。这里的范式转变是您不再提供请求-响应,而是 channels 来丢弃请求和响应。如果你学过 C 或 C++,这很像从

int get_square_root(int input);

void get_square_root(int input, int& output);

看看 output 现在如何成为 参数 ?但如果这完全没有意义(我的错 :-),这里有一条更自然的路径:

服务器流

让我们从 服务器流存根 开始,即使您的最终目标是客户端流。

public void serverStreamingExample(
    RequestType request,
    StreamObserver<ResponseType> responseObserver)

问:为什么参数列表中是“response”?答:参数列表中的不是 response,而是 channel 以提供最终响应。例如:

public void serverStreamingExample(
    RequestType request,
    StreamObserver<ResponseType> responseObserver) {
  
  ResponseType response = processRequest(request);

  responseObserver.onNext(response); // this is the "return"
  responseObserver.onCompleted();
}

为什么?因为,streaming 的要点是保持 channel 的活力,响应可以在其上持续流动。如果您只能 return 1 个响应,仅此而已,功能已完成,那么这不是流。通过提供 频道 ,作为开发人员,您可以选择根据需要传递它,通过 onNext() 提供您想要的尽可能多的响应,直到您满意为止,并且呼叫 onCompleted().

客户端流

现在,让我们继续讨论客户端流存根:

public StreamObserver<RequestType> clientStreamingExample(
    StreamObserver<ResponseType> responseObserver)

问:等等,什么!我们知道为什么 response 现在在参数列表中,但是它对 return 一个 request 有什么意义呢? A:同样,我们实际上并不是 return 发出 请求 ,而是 通道 供客户端丢弃请求!为什么?因为 client streaming 的要点是允许客户端 分段 提供请求。它不能通过对服务器的单个传统调用来做到这一点。所以这是可以实现的一种方法:

class ClientStreamingExample {

  int piecesRcvd = 0;

  public StreamObserver<RequestType> myClientStreamingEndpoint(
      StreamObserver<ResponseType> responseObserver) {

    return new StreamObserver<RequestType>() {

      @Override
      public void onNext(RequestType requestPiece) {
        // do whatever you want with the request pieces
        piecesRcvd++;
      }

      @Override
      public void onCompleted() {
        // when the client says they're done sending request pieces,
        // send them a response back (but you don't have to! or it can
        // be conditional!)
        ResponseType response =
            new ResponseType("received " + piecesRcvd + " pieces");
        responseObserver.onNext(response);
        responseObserver.onCompleted();
        piecesRcvd = 0;
      }

      @Override
      public void onError() {
        piecesRcvd = 0;
      }
    };
  }
}

您可能需要花一点时间研究它才能完全理解,但基本上,由于客户端现在可能会发送请求流,因此您必须为每个请求定义 handlers请求片段,以及客户端的处理程序,说明它已完成或出错。 (在我的例子中,我让服务器只在客户端说完成时响应,但你可以自由地做任何你想做的事。你甚至可以让服务器响应甚至 客户端之前说已经完成或根本没有回应。)

双向流媒体

这不是真的! :-) 我的意思是,教程只是想指出没有什么能阻止你完全实现上述内容,只是 双方。因此,您最终会得到 2 个应用程序,它们分段发送和接收请求,并发送和接收响应。他们将此设置称为 双向流式传输 ,他们的说法是正确的,但它只是有点误导,因为它在技术上与客户端流式传输没有任何不同。这正是签名相同的原因。恕我直言,教程应该像我在这里提到的那样,而不是重复存根。

可选:只是为了“好玩”...

我们从从

开始的 C++ 类比开始
int get_square_root(int input); // "traditional" request-response

void get_square_root(int input, int& output); // server streaming

我们要不要继续这个比喻?当然有。

你好,C++函数指针,我的老朋友...

void (*fnPtr)(int) get_square_root_fn(int& output); // client streaming

及其使用演示(无):

int main() { // aka the client
  int result;
  void (*fnPtr)(int) = server.get_square_root_fn(result);
  fnPtr(2);
  std::cout << result << std::endl; // 1.4142 assuming the fn actually does sqrt
}