C++ 中的协议请求管道

前提条件

本教程以 C++ 入门教程为基础。

概览

在 Fuchsia 上使用 FIDL 的一个常见方面是跨协议传递协议端点。许多 FIDL 消息都包含通道的客户端或服务器端,其中通道用于通过不同的 FIDL 协议进行通信。在这种情况下,客户端允许向指定的协议发出请求,而服务器端必须实现指定的协议。客户端和服务器端的另一组术语是协议和协议请求。

本教程涵盖以下内容:

  • 这些客户端和服务器的使用在 FIDL 和 C++ FIDL 绑定中结束。
  • 协议请求管道模式及其优势。

本教程的完整示例代码位于 //examples/fidl/cpp/request_pipelining

FIDL 协议

本教程实现了 fuchsia.examples 库中的 EchoLauncher 协议:

@discoverable
closed protocol EchoLauncher {
    strict GetEcho(struct {
        echo_prefix string:MAX_STRING_LENGTH;
    }) -> (resource struct {
        response client_end:Echo;
    });
    strict GetEchoPipelined(resource struct {
        echo_prefix string:MAX_STRING_LENGTH;
        request server_end:Echo;
    });
};

这是一个允许客户端检索 Echo 协议实例的协议。客户端可以指定一个前缀,生成的 Echo 实例会将该前缀添加到每个响应中。

可以使用以下两种方法完成此操作:

  • GetEcho:接受前缀作为请求,并使用连接到 Echo 协议实现的通道的客户端端进行响应。在响应中收到客户端后,客户端就可以开始使用客户端发出采用 Echo 协议的请求。
  • GetEchoPipelined:将通道的服务器端作为请求参数之一,并将 Echo 的实现绑定到该服务器端。系统会假定发出请求的客户端已持有客户端,并将在调用 GetEchoPipelined 之后开始在相应通道上发出 Echo 请求。

顾名思义,后一种方式使用称为协议请求流水线的模式,是首选方法。本教程同时实现了这两种方法。

实现服务器

实现 Echo 协议

这种 Echo 实现允许指定前缀,以区分 Echo 服务器的不同实例:

// Implementation of the Echo protocol that prepends a prefix to every response.
class EchoImpl final : public fidl::Server<fuchsia_examples::Echo> {
 public:
  explicit EchoImpl(std::string prefix) : prefix_(prefix) {}
  // This method is not used in the request pipelining example, so requests are ignored.
  void SendString(SendStringRequest& request, SendStringCompleter::Sync& completer) override {}

  void EchoString(EchoStringRequest& request, EchoStringCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "Got echo request for prefix " << prefix_;
    completer.Reply(prefix_ + request.value());
  }

  const std::string prefix_;
};

SendString 处理程序为空,因为客户端仅使用 EchoString

实现 EchoLauncher 协议

// Implementation of EchoLauncher. Each method creates an instance of EchoImpl
// with the specified prefix.
class EchoLauncherImpl final : public fidl::Server<fuchsia_examples::EchoLauncher> {
 public:
  explicit EchoLauncherImpl(async_dispatcher_t* dispatcher) : dispatcher_(dispatcher) {}

  void GetEcho(GetEchoRequest& request, GetEchoCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "Got non-pipelined request";
    auto endpoints = fidl::CreateEndpoints<fuchsia_examples::Echo>();
    ZX_ASSERT(endpoints.is_ok());
    auto [client_end, server_end] = *std::move(endpoints);
    fidl::BindServer(dispatcher_, std::move(server_end),
                     std::make_unique<EchoImpl>(request.echo_prefix()));
    completer.Reply(std::move(client_end));
  }

  void GetEchoPipelined(GetEchoPipelinedRequest& request,
                        GetEchoPipelinedCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "Got pipelined request";
    fidl::BindServer(dispatcher_, std::move(request.request()),
                     std::make_unique<EchoImpl>(request.echo_prefix()));
  }

  async_dispatcher_t* dispatcher_;
};

对于 GetEcho,代码首先需要实例化通道的两端。接着,它使用服务器端启动 Echo 实例,然后通过客户端发回响应。对于 GetEchoPipelined,客户端已经完成了创建通道两端的工作。它会保留一端,并将另一端传递给服务器,因此代码需要做的所有操作就是将服务器端绑定到新的 EchoImpl

提供 EchoLauncher 协议

主循环与服务器教程中的相同,但提供 EchoLauncher,而不是 Echo

int main(int argc, char** argv) {
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher);
  zx::result result = outgoing.ServeFromStartupInfo();
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string();
    return -1;
  }

  result = outgoing.AddUnmanagedProtocol<fuchsia_examples::EchoLauncher>(
      [dispatcher](fidl::ServerEnd<fuchsia_examples::EchoLauncher> server_end) {
        FX_LOGS(INFO) << "Incoming connection for "
                      << fidl::DiscoverableProtocolName<fuchsia_examples::EchoLauncher>;
        fidl::BindServer(dispatcher, std::move(server_end),
                         std::make_unique<EchoLauncherImpl>(dispatcher));
      });
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to add EchoLauncher protocol: " << result.status_string();
    return -1;
  }

  FX_LOGS(INFO) << "Running echo launcher server" << std::endl;
  loop.Run();
  return 0;
}

构建服务器

(可选)如需检查内容是否正确,请尝试构建服务器:

  1. 配置您的 GN build 以包含该服务器:

    fx set core.x64 --with //examples/fidl/cpp/request_pipelining/server:echo-server
    
  2. 构建 Fuchsia 映像:

    fx build
    

实现客户端

连接到 EchoLauncher 服务器后,客户端代码会使用 GetEcho 连接到 Echo 的一个实例,使用 GetEchoPipelined 连接到另一个 Echo 实例,然后对每个实例发出 EchoString 请求。

非流水线客户端

以下是非流水线代码:

int main(int argc, const char** argv) {
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();
  int num_responses = 0;

  // Connect to the EchoLauncher protocol
  zx::result launcher_client_end = component::Connect<fuchsia_examples::EchoLauncher>();
  ZX_ASSERT(launcher_client_end.is_ok());
  fidl::Client launcher(std::move(*launcher_client_end), dispatcher);

  // Make a non-pipelined request to get an instance of Echo
  launcher->GetEcho({"non pipelined: "})
      .ThenExactlyOnce([&](fidl::Result<fuchsia_examples::EchoLauncher::GetEcho>& result) {
        ZX_ASSERT(result.is_ok());
        // Take the Echo client end in the response, bind it to another client, and
        // make an EchoString request on it.
        fidl::SharedClient echo(std::move(result->response()), dispatcher);
        echo->EchoString({"hello!"})
            .ThenExactlyOnce(
                // Clone |echo| into the callback so that the client
                // is only destroyed after we receive the response.
                [&, echo = echo.Clone()](fidl::Result<fuchsia_examples::Echo::EchoString>& result) {
                  ZX_ASSERT(result.is_ok());
                  FX_LOGS(INFO) << "Got echo response " << result->response();
                  if (++num_responses == 2) {
                    loop.Quit();
                  }
                });
      });

  zx::result endpoints = fidl::CreateEndpoints<fuchsia_examples::Echo>();
  ZX_ASSERT(endpoints.status_value() == ZX_OK);
  auto [client_end, server_end] = *std::move(endpoints);
  // Make a pipelined request to get an instance of Echo
  ZX_ASSERT(launcher->GetEchoPipelined({"pipelined: ", std::move(server_end)}).is_ok());
  // A client can be initialized using the client end without waiting for a response
  fidl::Client echo_pipelined(std::move(client_end), dispatcher);
  echo_pipelined->EchoString({"hello!"})
      .ThenExactlyOnce([&](fidl::Result<fuchsia_examples::Echo::EchoString>& result) {
        ZX_ASSERT(result.is_ok());
        FX_LOGS(INFO) << "Got echo response " << result->response();
        if (++num_responses == 2) {
          loop.Quit();
        }
      });

  loop.Run();
  return num_responses == 2 ? 0 : 1;
}

此代码有两层回调:

  • 外层负责处理启动器 GetEcho 响应。
  • 内层负责处理 EchoString 响应。

GetEcho 响应回调内,代码将返回的客户端绑定到 fidl::SharedClient<Echo>,并将克隆放入 EchoString 回调中,以便客户端的生命周期延长,直到收到 echo 响应(很可能在顶级回调返回之后)。

流水线客户端

尽管必须先创建一对端点,但流水线代码要简单得多:

int main(int argc, const char** argv) {
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();
  int num_responses = 0;

  // Connect to the EchoLauncher protocol
  zx::result launcher_client_end = component::Connect<fuchsia_examples::EchoLauncher>();
  ZX_ASSERT(launcher_client_end.is_ok());
  fidl::Client launcher(std::move(*launcher_client_end), dispatcher);

  // Make a non-pipelined request to get an instance of Echo
  launcher->GetEcho({"non pipelined: "})
      .ThenExactlyOnce([&](fidl::Result<fuchsia_examples::EchoLauncher::GetEcho>& result) {
        ZX_ASSERT(result.is_ok());
        // Take the Echo client end in the response, bind it to another client, and
        // make an EchoString request on it.
        fidl::SharedClient echo(std::move(result->response()), dispatcher);
        echo->EchoString({"hello!"})
            .ThenExactlyOnce(
                // Clone |echo| into the callback so that the client
                // is only destroyed after we receive the response.
                [&, echo = echo.Clone()](fidl::Result<fuchsia_examples::Echo::EchoString>& result) {
                  ZX_ASSERT(result.is_ok());
                  FX_LOGS(INFO) << "Got echo response " << result->response();
                  if (++num_responses == 2) {
                    loop.Quit();
                  }
                });
      });

  zx::result endpoints = fidl::CreateEndpoints<fuchsia_examples::Echo>();
  ZX_ASSERT(endpoints.status_value() == ZX_OK);
  auto [client_end, server_end] = *std::move(endpoints);
  // Make a pipelined request to get an instance of Echo
  ZX_ASSERT(launcher->GetEchoPipelined({"pipelined: ", std::move(server_end)}).is_ok());
  // A client can be initialized using the client end without waiting for a response
  fidl::Client echo_pipelined(std::move(client_end), dispatcher);
  echo_pipelined->EchoString({"hello!"})
      .ThenExactlyOnce([&](fidl::Result<fuchsia_examples::Echo::EchoString>& result) {
        ZX_ASSERT(result.is_ok());
        FX_LOGS(INFO) << "Got echo response " << result->response();
        if (++num_responses == 2) {
          loop.Quit();
        }
      });

  loop.Run();
  return num_responses == 2 ? 0 : 1;
}

客户端教程不同,异步循环会运行完成一次,这会同时运行非流水线和流水线代码,以观察操作顺序。客户端会跟踪收到的响应数量,以便能够在预期不再收到来自服务器的消息时退出循环。

构建客户端

(可选)如需检查内容是否正确,请尝试构建客户端:

  1. 配置您的 GN build 以包含该服务器:

    fx set core.x64 --with //examples/fidl/cpp/request_pipelining/client:echo-client
    
  2. 构建 Fuchsia 映像:

    fx build
    

运行示例代码

fuchsia.examples.Echofuchsia.examples.EchoLauncher

  1. 配置 build 以添加所提供的包含 echo 领域、服务器和客户端的软件包:

    fx set core.x64 --with //examples/fidl/cpp/request_pipelining
    
  2. 构建 Fuchsia 映像:

    fx build
    
  3. 运行 echo_realm 组件。这将创建客户端和服务器组件实例并路由功能:

    ffx component run /core/ffx-laboratory:echo_realm fuchsia-pkg://fuchsia.com/echo-launcher-cpp#meta/echo_realm.cm
    
  4. 启动 echo_client 实例:

    ffx component start /core/ffx-laboratory:echo_realm/echo_client
    

当客户端尝试连接到 EchoLauncher 协议时,服务器组件便会启动。您应该会在设备日志 (ffx log) 中看到类似于以下内容的输出:

[echo_server][I] Running echo launcher server
[echo_server][I] Incoming connection for fuchsia.examples.EchoLauncher
[echo_server][I] Got non-pipelined request
[echo_server][I] Got pipelined request
[echo_server][I] Got echo request for prefix pipelined:
[echo_server][I] Got echo request for prefix non pipelined:
[echo_client][I] Got echo response pipelined: hello!
[echo_client][I] Got echo response non pipelined: hello!

根据打印订单,您可以看到,采用流水线的情形更快。即使先发送非流水线请求,也会先发送流水线用例的 echo 响应,因为请求流水线可以省去客户端与服务器之间的往返。请求管道也会简化代码。

如需详细了解协议请求流水线,包括如何处理可能失败的协议请求,请参阅 FIDL API 评分准则

终止领域组件以停止执行并清理组件实例:

ffx component destroy /core/ffx-laboratory:echo_realm