连接组件

协议句柄是一个众所周知的对象,它提供了可使用组件命名空间发现的 FIDL 协议实现。 功能路由描述了哪个组件应该充当任何给定客户端的提供程序。

请参考以下示例,了解 fuchsia.example.Foo 协议:

展示连接组件如何同时兼顾功能路由和协议传送的示意图。组件必须实现它们向其他组件提供的协议。

下图突出显示了执行连接所涉及的主要元素:

  1. 提供程序组件在清单的 capabilities 部分中静态声明协议。这样,组件框架就能够执行功能路由。
  2. 客户端组件在清单的 use 部分中静态请求协议。如果功能路由成功,此操作会在客户端的命名空间中创建 /svc/fuchsia.example.Foo 协议条目。
  3. 提供方代码在运行时发布实现。这会在提供程序的传出目录中的 /svc/fuchsia.example.Foo 中创建一个协议条目。
  4. 客户端代码在运行时连接到协议句柄。这会打开与提供程序组件中运行的实现的 FIDL 连接。

发布协议实现

实现 FIDL 协议的组件会在其组件清单中声明该协议并将其作为功能进行声明公开。这样,组件框架就可以执行从该组件到拓扑中请求该功能的其他组件的功能路由。

{
    // ...
    capabilities: [
        { protocol: "fuchsia.example.Foo" },
    ],
    expose: [
        {
            protocol: "fuchsia.example.Foo",
            from: "self",
        },
    ],
}

功能路由会描述对协议的访问权限,但不会为连接建立必要的端点。组件必须使用 fuchsia.io 协议将实现发布为传出目录中的 /svc/ 句柄。生成的 FIDL 绑定会封装此句柄,并使提供程序能够连接请求句柄以开始接收 FIDL 消息。

// Serve the protocol
FooImplementation instance;
fidl::Binding<fuchsia::example::Foo> binding(&instance);
instance.event_sender_ = &binding.events();
fidl::InterfaceRequestHandler<fuchsia::example::Foo> handler =
    [&](fidl::InterfaceRequest<fuchsia::example::Foo> request) {
      binding.Bind(std::move(request));
    };
context->outgoing()->AddPublicService(std::move(handler));

连接到协议实现

客户端组件会在其组件清单中将协议声明为必需功能。这样,组件框架就可以确定组件是否有权访问协议实现。如果存在有效路由,则组件的命名空间会包含相应的 /svc/ 句柄。

{
    // ...
    use: [
        { protocol: "fuchsia.example.Foo" },
    ],
}

客户端组件使用 fuchsia.io 协议与协议实现建立连接并打开一个通道。生成的 FIDL 绑定会封装此通道,并使客户端能够开始向提供程序发送消息。

// Connect to FIDL protocol
fuchsia::example::FooSyncPtr proxy;
auto context = sys::ComponentContext::Create();
context->svc()->Connect(proxy.NewRequest());

练习:Echo 服务器和客户端

在本部分中,您将使用为 examples.routing.echo 生成的 FIDL 绑定来实现客户端和服务器组件。

启动模拟器

如果您尚未运行实例,请启动提供网络支持的 FEMU:

ffx emu start workstation_eng.x64 --headless

创建服务器组件

首先,创建一个新的组件项目以实现 echo 服务器。此组件将提供 Echo 协议并处理传入请求。

在 Bazel 工作区中为名为 echo_server 的新组件创建一个新项目目录:

mkdir -p fuchsia-codelab/echo-server

完成本部分后,项目应具有以下目录结构:

//fuchsia-codelab/echo-server
                  |- BUILD.bazel
                  |- meta
                  |   |- echo_server.cml
                  |
                  |- main.cc

创建 echo-server/meta/echo_server.cml 组件清单,将 Echo 协议声明为服务器组件提供的 capability,并将其公开以供父领域使用:

echo-server/meta/echo_server.cml:

{
    include: [
        "syslog/client.shard.cml",
        "inspect/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/echo_server_cpp",
    },

    // Capabilities provided by this component.
    capabilities: [
        { protocol: "examples.routing.echo.Echo" },
    ],
    expose: [
        {
            protocol: "examples.routing.echo.Echo",
            from: "self",
        },
    ],
}

添加以下 BUILD.bazel 规则以构建和打包服务器组件:

echo-server/BUILD.bazel:

load(
    "@rules_fuchsia//fuchsia:defs.bzl",
    "fuchsia_cc_binary",
    "fuchsia_component",
    "fuchsia_component_manifest",
)

fuchsia_cc_binary(
    name = "echo_server_cpp",
    srcs = [
        "main.cc",
    ],
    deps = [
        "//fuchsia-codelab/echo-fidl:examples.routing.echo.fidl_cc",
        "@fuchsia_sdk//pkg/async-default",
        "@fuchsia_sdk//pkg/async-loop",
        "@fuchsia_sdk//pkg/async-loop-cpp",
        "@fuchsia_sdk//pkg/async-loop-default",
        "@fuchsia_sdk//pkg/fdio",
        "@fuchsia_sdk//pkg/inspect",
        "@fuchsia_sdk//pkg/inspect_component_cpp",
        "@fuchsia_sdk//pkg/component_outgoing_cpp",
        "@fuchsia_sdk//pkg/syslog",
    ],
)

fuchsia_component_manifest(
    name = "manifest",
    src = "meta/echo_server.cml",
    component_name = "echo_server_component",
    includes = [
        "@fuchsia_sdk//pkg/inspect:client",
        "@fuchsia_sdk//pkg/syslog:client",
    ],
)

fuchsia_component(
    name = "echo_server_component",
    component_name = "echo_server_component",
    manifest = ":manifest",
    visibility = ["//visibility:public"],
    deps = [
        ":echo_server_cpp",
    ],
)

实现服务器

打开主源文件,并将 import 语句替换为以下代码:

echo-server/main.cc:

#include <fidl/examples.routing.echo/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/inspect/component/cpp/component.h>
#include <lib/component/outgoing/cpp/outgoing_directory.h>
#include <lib/syslog/global.h>

添加以下代码以实现协议请求处理程序:

echo-server/main.cc:

// Handler for incoming FIDL protocol requests
class EchoImplementation : public fidl::Server<examples_routing_echo::Echo> {
 public:
  // The handler for `examples.routing.echo/Echo.EchoString` requests.
  //
  // Replies back to the caller with the original request value.
  void EchoString(EchoStringRequest& request, EchoStringCompleter::Sync& completer) override {
    completer.Reply({{request.value()}});
  }

  // Called when the FIDL connection is torn down.
  void OnUnbound(fidl::UnbindInfo info, fidl::ServerEnd<examples_routing_echo::Echo> server_end) {
    if (info.is_user_initiated()) {
      return;
    }
    if (info.is_peer_closed()) {
      // The peer (the client) closed their endpoint.
      FX_LOG(DEBUG, "echo_server", "Client disconnected.");
    } else {
      // Treat other unbind causes as errors.
      FX_LOGF(ERROR, "echo_server", "Server error: %s", info.status_string());
    }
  }
};

每个 Echo 协议方法都有对应的替换函数 (EchoString()),并包含用于发回返回值的回调接口。

此实现只会在响应载荷中“回显”请求中的字符串值。

将以下代码添加到 main() 以提供 Echo 协议:

echo-server/main.cc:

int main(int argc, const char** argv) {
  async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);
  component::OutgoingDirectory outgoing = component::OutgoingDirectory(loop.dispatcher());

  // Initialize inspect
  inspect::ComponentInspector inspector(outgoing, loop.dispatcher());
  inspector.Health().StartingUp();

  // Serve the Echo protocol
  std::unique_ptr<EchoImplementation> echo_instance = std::make_unique<EchoImplementation>();
  zx::result result = outgoing.AddProtocol<examples_routing_echo::Echo>(std::move(echo_instance));
  if (result.is_error()) {
    FX_LOGF(ERROR, "echo_server", "Failed to add Echo protocol: %s", result.status_string());
    return -1;
  }

  result = outgoing.ServeFromStartupInfo();
  if (result.is_error()) {
    FX_LOGF(ERROR, "echo_server", "Failed to serve outgoing directory: %s", result.status_string());
    return -1;
  }

  // Component is serving and ready to handle incoming requests
  inspector.Health().Ok();

  return loop.Run();
}

此代码执行以下步骤以提供 Echo 协议:

  1. 初始化 ComponentContext,并在传出目录中的 /svc/examples.routing.echo.Echo 下添加一个条目。
  2. 提供目录并开始监听传入连接。
  3. EchoImplementation 实例附加为任何匹配的 Echo 请求的请求处理程序。

创建客户端组件

创建另一个新的组件项目以实现 echo 客户端。此组件将连接到协议实现并发送请求。

在 Bazel 工作区中为名为 echo_client 的新组件创建一个新项目目录:

mkdir -p fuchsia-codelab/echo-client

完成本部分后,项目应具有以下目录结构:

//fuchsia-codelab/echo-client
                  |- BUILD.bazel
                  |- meta
                  |   |- echo_client.cml
                  |
                  |- main.cc

创建 echo-client/meta/echo_client.cml 组件清单,并配置客户端组件以请求服务器公开的 examples.routing.echo.Echo 功能:

echo-client/meta/echo_client.cml:

{
    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/echo_client_cpp",

        // Program arguments
        args: [ "Hello Fuchsia!" ],
    },

    // Capabilities used by this component.
    use: [
        { protocol: "examples.routing.echo.Echo" },
    ],
}

添加以下 BUILD.bazel 规则以构建和打包客户端组件:

echo-client/BUILD.bazel:

load(
    "@rules_fuchsia//fuchsia:defs.bzl",
    "fuchsia_cc_binary",
    "fuchsia_component",
    "fuchsia_component_manifest",
)

fuchsia_cc_binary(
    name = "echo_client_cpp",
    srcs = [
        "main.cc",
    ],
    deps = [
        "//fuchsia-codelab/echo-fidl:examples.routing.echo.fidl_cc",
        "@fuchsia_sdk//pkg/async-default",
        "@fuchsia_sdk//pkg/async-loop",
        "@fuchsia_sdk//pkg/async-loop-cpp",
        "@fuchsia_sdk//pkg/async-loop-default",
        "@fuchsia_sdk//pkg/fdio",
        "@fuchsia_sdk//pkg/component_incoming_cpp",
        "@fuchsia_sdk//pkg/syslog",
    ],
)

fuchsia_component_manifest(
    name = "manifest",
    src = "meta/echo_client.cml",
    component_name = "echo_client_component",
    includes = ["@fuchsia_sdk//pkg/syslog:client"],
)

fuchsia_component(
    name = "echo_client_component",
    component_name = "echo_client_component",
    deps = [
        ':echo_client_cpp',
    ],
    manifest = ":manifest",
    visibility = ["//visibility:public"],
)

实现客户端

echo 类似,客户端会将程序参数作为消息传递给服务器。回想一下,echo_client.cmlprogram 块中对参数进行了描述:

echo-client/meta/echo_client.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/echo_client_cpp",

    // Program arguments
    args: [ "Hello Fuchsia!" ],
},

打开主源文件,并将 import 语句替换为以下代码:

echo-client/main.cc:

#include <fidl/examples.routing.echo/cpp/fidl.h>
#include <lib/component/incoming/cpp/service_client.h>
#include <lib/syslog/global.h>

#include <cstdlib>
#include <iostream>
#include <string>

将以下代码添加到 main() 以连接到 Echo 协议并发送请求:

echo-client/main.cc:

int main(int argc, const char* argv[], char* envp[]) {
  // Connect to FIDL protocol
  zx::result client_end = component::Connect<examples_routing_echo::Echo>();
  if (!client_end.is_ok()) {
    FX_LOGF(ERROR, "echo_client", "Error connecting to Echo protocol: %s", client_end.status_string());
    return -1;
  }
  fidl::SyncClient client{std::move(*client_end)};

  // Send messages over FIDL interface for each argument
  for (int i = 1; i < argc; i++) {
    fidl::Result result = client->EchoString({argv[i]});
    ZX_ASSERT(result.is_ok());

    auto response = result->response();
    if (!response.has_value()) {
      FX_LOG(INFO, "echo_client", "echo_string got empty result");
    } else {
      FX_LOGF(INFO, "echo_client", "Server response: %s", response->c_str());
    }
  }

  return 0;
}

EchoSyncPtr 提供了一个封装容器,用于按名称连接到公开的 capability,并返回打开的代理接口的句柄。此代理包含 EchoString() FIDL 协议方法。

集成组件

服务器提供的功能必须通过组件框架路由到客户端。要实现这一点,您需要实现一个领域组件来充当父级并管理功能路由。

为 Realm 组件定义创建一个新的项目目录:

mkdir -p fuchsia-codelab/echo-realm

完成本部分后,项目应具有以下目录结构:

//fuchsia-codelab/echo-realm
                  |- BUILD.bazel
                  |- meta
                      |- echo_realm.cml

创建包含以下内容的新组件清单文件 echo-realm/meta/echo_realm.cml

echo-realm/meta/echo_realm.cml:

{
    // Two children: a server and client.
    children: [
        {
            name: "echo_server",
            url: "#meta/echo_server.cm",
        },
        {
            name: "echo_client",
            url: "#meta/echo_client.cm",
        },
    ],

    // Route Echo service from server to client.
    offer: [
        {
            protocol: "examples.routing.echo.Echo",
            from: "#echo_server",
            to: "#echo_client",
        },
        {
            protocol: "fuchsia.logger.LogSink",
            from: "parent",
            to: [
                "#echo_client",
                "#echo_server",
            ],
        },
    ],
}

这会创建一个组件领域,其中服务器和客户端作为子组件,并将 examples.routing.echo.Echo 协议功能路由到客户端。

添加 BUILD.bazel 文件,以便为 Realm 组件和包含服务器和客户端的 Fuchsia 软件包创建 build 目标:

echo-realm/BUILD.bazel:

load(
    "@rules_fuchsia//fuchsia:defs.bzl",
    "fuchsia_component",
    "fuchsia_component_manifest",
    "fuchsia_package",
)

fuchsia_component_manifest(
    name = "manifest",
    src = "meta/echo_realm.cml",
    component_name = "echo_realm",
)

fuchsia_component(
    name = "echo_realm",
    component_name = "echo_realm",
    manifest = ":manifest",
)

fuchsia_package(
    name = "pkg",
    package_name = "echo-realm",
    visibility = ["//visibility:public"],
    components = [
        ":echo_realm",
        "//fuchsia-codelab/echo-client:echo_client_component",
        "//fuchsia-codelab/echo-server:echo_server_component",
    ],
)

构建软件包并将其发布到 fuchsiasamples.com 代码库:

bazel run //fuchsia-codelab/echo-realm:pkg.publish -- \
    --repo_name fuchsiasamples.com

将组件添加到拓扑中

您要将组件添加到 ffx-laboratory(一个用于产品的核心领域内开发的受限集合)。通过集合,可以在运行时动态创建和销毁组件。

echo-realm 组件网址和 ffx-laboratory 内的相应名称传递给 ffx component create,以创建组件实例:

ffx component create /core/ffx-laboratory:echo-realm \
    fuchsia-pkg://fuchsiasamples.com/echo-realm#meta/echo_realm.cm

然后,使用 ffx component resolve 解析 echo-realm 组件:

ffx component resolve /core/ffx-laboratory:echo-realm

使用 ffx component show 验证服务器和客户端的实例是否也同时创建为子组件:

ffx component show echo
               Moniker: /core/ffx-laboratory:echo-realm/echo_client
                   URL: #meta/echo_client.cm
                  Type: CML static component
       Component State: Unresolved
       Execution State: Stopped

               Moniker: /core/ffx-laboratory:echo-realm/echo_server
                   URL: #meta/echo_server.cm
                  Type: CML static component
       Component State: Unresolved
       Execution State: Stopped

               Moniker: /core/ffx-laboratory:echo-realm
                   URL: fuchsia-pkg://fuchsiasamples.com/echo-realm#meta/echo_realm.cm
                  Type: CML dynamic component
       Component State: Resolved
       Execution State: Stopped
           Merkle root: 666c40477785f89b0ace22b30d65f1338f1d308ecceacb0f65f5140baa889e1b

验证组件交互

使用 ffx component start 启动现有的客户端组件实例:

ffx component start /core/ffx-laboratory:echo-realm/echo_client

打开另一个终端窗口,并验证客户端组件的日志输出:

ffx log --filter echo

您应该会在设备日志中看到以下输出:

[echo_client][I] Server response: Hello, Fuchsia!

在客户端连接到 examples.routing.echo.Echo capability 后,服务器组件便会启动,并会继续运行以处理其他 FIDL 请求。

使用 ffx component show 查看在组件实例树中运行的 echo 服务器:

ffx component show echo_server
               Moniker: /core/ffx-laboratory:echo-realm/echo_server
                   URL: #meta/echo_server.cm
                  Type: CML static component
       Component State: Resolved
 Incoming Capabilities: fuchsia.logger.LogSink
  Exposed Capabilities: diagnostics
                        examples.routing.echo.Echo
       Execution State: Running
                Job ID: 474691
            Process ID: 474712
           Running for: 2026280474361 ticks
           Merkle root: 666c40477785f89b0ace22b30d65f1338f1d308ecceacb0f65f5140baa889e1b
 Outgoing Capabilities: diagnostics
                        examples.routing.echo.Echo

销毁实例

使用以下命令清理 echo-realm 实例:

ffx component destroy /core/ffx-laboratory:echo-realm