实现同步 C++ FIDL 客户端

前提条件

本教程基于 FIDL 服务器教程构建而成。如需查看完整的 FIDL 教程,请参阅概览

概览

本教程将实现 FIDL 协议的客户端,并针对上一个教程中创建的服务器运行该客户端。本教程中的客户端是同步的。我们还提供了一个针对异步客户端的替代教程

客户端示例的结构

本教程随附的示例代码位于 Fuchsia 代码库的 //examples/fidl/cpp/client_sync 中。它由客户端组件及其包含的软件包组成。如需详细了解如何构建组件,请参阅构建组件

为了让客户端组件正常运行,//examples/fidl/cpp/client_sync/BUILD.gn 中定义了三个目标:

  1. 客户端的原始可执行文件。这会生成一个具有指定输出名称的二进制文件,该文件可在 Fuchsia 上运行:

    executable("bin") {
      output_name = "fidl_echo_cpp_client_sync"
      sources = [ "main.cc" ]
    
      deps = [
        "//examples/fidl/fuchsia.examples:fuchsia.examples_cpp",
    
        # This library is used to log messages.
        "//sdk/lib/syslog/cpp",
    
        # This library is used to consume capabilities, e.g. protocols,
        # from the component's incoming directory.
        "//sdk/lib/component/incoming/cpp",
      ]
    }
    
    
  2. 一种设置为运行客户端可执行文件的组件。 组件是 Fuchsia 上的软件执行单元。组件由其清单文件描述。在这种情况下,meta/client.cmlecho-client 配置为在 :bin 中运行 fidl_echo_cpp_client_sync 的可执行组件。

    fuchsia_component("echo-client") {
      component_name = "echo_client"
      manifest = "meta/client.cml"
      deps = [ ":bin" ]
    }
    
    

    服务器组件清单位于 //examples/fidl/cpp/client_sync/meta/client.cml。清单中的二进制名称必须与 BUILD.gn 中定义的 executable 的输出名称匹配。

    {
        include: [ "syslog/client.shard.cml" ],
    
        // Information about the program to run.
        program: {
            // Use the built-in ELF runner.
            runner: "elf",
    
            // The binary to run for this component.
            binary: "bin/fidl_echo_cpp_client_sync",
        },
    
        // Capabilities used by this component.
        use: [
            { protocol: "fuchsia.examples.Echo" },
        ],
    }
    
    
  3. 然后,将该组件放入软件包中,软件包是 Fuchsia 上的软件分发单元。在这种情况下,软件包包含客户端和服务器组件,并且 组件用于声明相应的功能和路由。

    # C++ sync client and server example package
    fuchsia_package("echo-cpp-client-sync") {
      deps = [
        ":echo-client",
        "//examples/fidl/cpp/server:echo-server",
        "//examples/fidl/echo-realm:echo_realm",
      ]
    }
    
    

构建客户端

  1. 将客户端添加到 build 配置中。此操作只需执行一次:

    fx set core.x64 --with //examples/fidl/cpp/client_sync
  2. 构建客户端:

    fx build examples/fidl/cpp/client_sync

连接到协议

在其主函数中,客户端组件会连接到 fuchsia.examples/Echo 协议。

int main(int argc, const char** argv) {
  // Connect to the |fuchsia.examples/Echo| protocol inside the component's
  // namespace. This can fail so it's wrapped in a |zx::result| and it must be
  // checked for errors.
  zx::result client_end = component::Connect<fuchsia_examples::Echo>();
  if (!client_end.is_ok()) {
    FX_LOGS(ERROR) << "Synchronous error when connecting to the |Echo| protocol: "
                   << client_end.status_string();
    return -1;
  }

  // ...

同时,组件管理器会将请求路由到服务器组件。在服务器教程中实现的服务器处理程序将通过服务器端点进行调用,从而将渠道绑定到服务器实现。

需要注意的一点是,此代码假定组件的命名空间已包含 Echo 协议的实例。当运行教程末尾的示例时,系统会使用 realm 组件来路由来自服务器的协议,并将其提供给客户端组件。

初始化客户端

为了向服务器发出 Echo 请求,请使用上一步中的客户端端点初始化客户端。

fidl::SyncClient client{std::move(*client_end)};

进行 FIDL 调用

用于进行 FIDL 调用的方法通过解引用运算符公开,因此 FIDL 调用看起来像 client->EchoString(...)

双向调用(例如 EchoString)会接受一个请求对象,并返回一个指示成功或失败的结果对象:

fidl::Result result = client->EchoString({"hello"});
// Check if the FIDL call succeeded or not.
if (!result.is_ok()) {
  // If the call failed, log the error, and quit the program.
  // Production code should do more graceful error handling depending
  // on the situation.
  FX_LOGS(ERROR) << "EchoString failed: " << result.error_value();
  return -1;
}
const std::string& reply_string = result->response();
FX_LOGS(INFO) << "Got response: " << reply_string;

您还可以使用 natural struct支持的指定初始化样式双大括号语法:

fidl::Result result = client->EchoString({{.value = "hello"}});

单向 SendString 调用没有回复。返回的结果表示发送请求时发生的任何错误。

fit::result<fidl::Error> result = client->SendString({"hi"});
if (!result.is_ok()) {
  FX_LOGS(ERROR) << "SendString failed: " << result.error_value();
  return -1;
}

处理事件

定义事件处理程序

// Define the event handler implementation for the client.
//
// The event handler should be an object that implements
// |fidl::SyncEventHandler<Echo>|, and override all pure virtual methods
// in that class corresponding to the events offered by the protocol.
class EventHandler : public fidl::SyncEventHandler<fuchsia_examples::Echo> {
 public:
  EventHandler() = default;

  void OnString(fidl::Event<fuchsia_examples::Echo::OnString>& event) override {
    const std::string& reply_string = event.response();
    FX_LOGS(INFO) << "Got event: " << reply_string;
  }
};

调用 client.HandleOneEvent 以阻塞,直到收到事件。如果识别到事件并成功解码,HandleOneEvent 会返回 fidl::Status::Ok()。否则,将返回相应的错误。如果服务器通过墓志铭关闭连接,则返回墓志铭中包含的状态。

// Block to receive exactly one event from the server, which is handled using
// the event handler defined above.
EventHandler event_handler;
fidl::Status status = client.HandleOneEvent(event_handler);
if (!status.ok()) {
  FX_LOGS(ERROR) << "HandleOneEvent failed: " << status.error();
  return -1;
}

使用 wire 网域对象进行调用

上述教程使用自然网域对象进行客户端调用:每次调用都会使用以自然网域对象表示的请求消息,并返回同样以自然网域对象表示的回复。在针对性能和堆分配进行优化时,可以使用 wire 网域对象进行调用。为此,请在进行调用时使用的解引用运算符(即 client.wire()->EchoString(...))之前插入 .wire()

使用以下有线类型进行 EchoString 双向调用:

fidl::WireResult result = client.wire()->EchoString("hello");
if (!result.ok()) {
  FX_LOGS(ERROR) << "EchoString failed: " << result.error();
  return -1;
}
FX_LOGS(INFO) << "Got response: " << result->response.get();

使用以下 wire 类型进行 SendString 单向调用:

fidl::Status wire_result = client.wire()->SendString("hi");
if (!wire_result.ok()) {
  FX_LOGS(ERROR) << "SendString failed: " << wire_result.error();
  return -1;
}

在有线客户端调用中使用的相关类和函数与在自然客户端调用中使用的类和函数具有相似的形状。当需要不同的类或函数时,有线对应项通常以 Wire 为前缀。指针与引用以及实参结构也存在差异:

  • 采用自然网域对象作为参数的 EchoString 方法接受一个实参,即请求网域对象:

    fidl::Result result = client->EchoString({{.value = "hello"}});
    

    如果请求载荷是结构体,则采用线网域对象的 EchoString 方法会将请求正文中的结构体字段列表扁平化为单独的实参(此处为单个 fidl::StringView 实参):

    fidl::WireResult result = client.wire()->EchoString("hello");
    
  • 双向自然通话会返回 fidl::Result<Method>

    fidl::Result result = client->EchoString({{.value = "hello"}});
    if (!result.is_ok()) {
      FX_LOGS(ERROR) << "EchoString failed: " << result.error_value();
      return -1;
    }
    const std::string& reply_string = result->response();
    FX_LOGS(INFO) << "Got response: " << reply_string;
    
    • 如需检查成功或错误,请使用 is_ok()is_error() 方法。
    • 如需在之后访问响应载荷,请使用 value()->
    • 您可以移出结果或载荷,因为这些类型都实现了分层对象所有权。

    双向有线调用会返回 fidl::WireResult<Method>

    fidl::WireResult result = client.wire()->EchoString("hello");
    if (!result.ok()) {
      FX_LOGS(ERROR) << "EchoString failed: " << result.error();
      return -1;
    }
    FX_LOGS(INFO) << "Got response: " << result->response.get();
    
    • 如需检查是否成功,请使用 ok() 方法。
    • 如需在之后访问响应载荷,请使用 value()->
    • 您无法移动结果对象。
  • 单向调用在自然情况下也会采用整个请求网域对象,并在序列化情况下将请求结构字段展平为单独的实参:

    // Make a SendString call using natural types.
    fit::result<fidl::Error> result = client->SendString({"hi"});
    
    // Make a SendString call using wire types.
    fidl::Status wire_result = client.wire()->SendString("hi");
    

运行客户端

为了使客户端和服务器能够使用 Echo 协议进行通信,组件框架必须将 fuchsia.examples.Echo 功能从服务器路由到客户端。本教程提供了一个组件,用于声明相应的功能和路由。

中探索 realm 组件的完整源代码
  1. 配置 build 以包含提供的软件包,该软件包包含回声 realm、服务器和客户端:

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

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

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

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

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

[echo_server][][I] Running echo server
[echo_client][I] Got response: hello
[echo_client][I] Got event: hi
[echo_client][I] Got response: hello
[echo_server][I] Client disconnected

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

ffx component destroy /core/ffx-laboratory:echo_realm

仅限有线网域对象的客户端

fidl::SyncClient 支持使用自然网域对象有线网域对象进行调用。如果您只需要使用网络网域对象,可以创建一个 WireSyncClient,该对象公开的等效方法调用接口与通过对 fidl::SyncClient 调用 client.wire() 获得的子集相同。

WireSyncClient 的创建方式与 SyncClient 相同:

fidl::WireSyncClient client(std::move(*client_end));

fidl::SyncClient 始终以自然网域对象的形式向用户公开接收到的事件。另一方面,fidl::WireSyncClient 将以有线网域对象的形式公开接收到的事件。为此,传递给 WireSyncClient 的事件处理脚本需要实现 fidl::WireSyncEventHandler<Protocol>

  • 实现自然事件处理脚本:

    // Define the event handler implementation for the client.
    //
    // The event handler should be an object that implements
    // |fidl::SyncEventHandler<Echo>|, and override all pure virtual methods
    // in that class corresponding to the events offered by the protocol.
    class EventHandler : public fidl::SyncEventHandler<fuchsia_examples::Echo> {
     public:
      EventHandler() = default;
    
      void OnString(fidl::Event<fuchsia_examples::Echo::OnString>& event) override {
        const std::string& reply_string = event.response();
        FX_LOGS(INFO) << "Got event: " << reply_string;
      }
    };
    
  • 实现有线事件处理脚本:

    // Define the event handler implementation for the client.
    //
    // The event handler should be an object that implements
    // |fidl::WireSyncEventHandler<Echo>|, and override all pure virtual methods
    // in that class corresponding to the events offered by the protocol.
    class EventHandler : public fidl::WireSyncEventHandler<fuchsia_examples::Echo> {
     public:
      EventHandler() = default;
    
      void OnString(fidl::WireEvent<fuchsia_examples::Echo::OnString>* event) override {
        FX_LOGS(INFO) << "Got event: " << event->response.get();
      }
    };
    

使用有线客户端的完整示例代码位于 Fuchsia 代码库的 //examples/fidl/cpp/client_sync/wire 中。