實作 C++ FIDL 伺服器

必要條件

本教學課程進行網域物件教學課程。如需完整的 FIDL 教學課程,請參閱總覽

總覽

本教學課程說明如何為 FIDL 通訊協定 (fuchsia.examples/Echo) 實作伺服器,並在 Fuchsia 上執行。此通訊協定在每個種類都有一種方法:單向方法、雙向方法和事件:

@discoverable
closed protocol Echo {
    strict EchoString(struct {
        value string:MAX_STRING_LENGTH;
    }) -> (struct {
        response string:MAX_STRING_LENGTH;
    });
    strict SendString(struct {
        value string:MAX_STRING_LENGTH;
    });
    strict -> OnString(struct {
        response string:MAX_STRING_LENGTH;
    });
};

如要進一步瞭解 FIDL 方法和訊息傳遞模型,請參閱 FIDL 概念頁面。

本文件說明如何完成下列工作:

伺服器範例的結構

本教學課程隨附的範例程式碼位於 Fuchsia 結帳頁面中的 //examples/fidl/cpp/server。其中包含伺服器元件及其包含的套件。如要進一步瞭解如何建構元件,請參閱「建構元件」。

如要讓伺服器元件順利運作,//examples/fidl/cpp/server/BUILD.gn 中定義的三個目標:

  1. 伺服器的原始可執行檔。這會產生具有指定輸出名稱的二進位檔,且可在 Fuchsia 上執行:

    executable("bin") {
      output_name = "fidl_echo_cpp_server"
      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 publish capabilities, e.g. protocols,
        # to the component's outgoing directory.
        "//sdk/lib/component/outgoing/cpp",
    
        # This library provides an the asynchronous event loop implementation.
        "//zircon/system/ulib/async-loop:async-loop-cpp",
      ]
    }
    
    
  2. 設定為執行伺服器執行檔的元件。元件是指軟體在 Fuchsia 上執行的單位。元件可透過其資訊清單檔案來描述。在這種情況下,meta/server.cml 會將 echo-server 設為可執行元件,並在 :bin 中執行 fidl_echo_cpp_server

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

    伺服器元件資訊清單位於 //examples/fidl/cpp/server/meta/server.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_server",
        },
    
        // Capabilities provided by this component.
        capabilities: [
            { protocol: "fuchsia.examples.Echo" },
        ],
        expose: [
            {
                protocol: "fuchsia.examples.Echo",
                from: "self",
            },
        ],
    }
    
    
  3. 元件隨後會放入套件中,也就是 Fuchsia 上的軟體發布單位。在此情況下,套件只包含單一元件。

    fuchsia_package("echo-cpp-server") {
      package_name = "echo-cpp-server"
      deps = [ ":echo-server" ]
    }
    
    

建構伺服器

您可以透過下列項目建構伺服器套件:

  1. 將伺服器新增至建構設定。這個步驟只需要執行一次:

    fx set core.x64 --with //examples/fidl/cpp/server
    
  2. 建構伺服器套件:

    fx build examples/fidl/cpp/server
    

實作 FIDL 通訊協定

EchoImpl 會實作 fuchsia.examples/Echo 通訊協定的伺服器要求處理常式。為此,EchoImpl 會沿用產生的純虛擬伺服器介面 fidl::Server<fuchsia_examples::Echo>,並覆寫自身與每方和雙向呼叫相對應的純虛擬方法:

class EchoImpl : public fidl::Server<fuchsia_examples::Echo> {
 public:
  // The handler for `fuchsia.examples/Echo.EchoString`.
  //
  // For two-way methods (those with a response) like this one, the completer is
  // used complete the call: either to send the reply via |completer.Reply|, or
  // close the channel via |completer.Close|.
  //
  // |EchoStringRequest| exposes the same API as the request struct domain
  // object, that is |fuchsia_examples::EchoEchoStringRequest|.
  void EchoString(EchoStringRequest& request, EchoStringCompleter::Sync& completer) override {
    // Call |Reply| to reply synchronously with the request value.
    completer.Reply({{.response = request.value()}});
  }

  // The handler for `fuchsia.examples/Echo.SendString`.
  //
  // For fire-and-forget methods like this one, the completer is normally not
  // used, but its |Close(zx_status_t)| method can be used to close the channel,
  // either when the protocol has reached its intended terminal state or the
  // server encountered an unrecoverable error.
  //
  // |SendStringRequest| exposes the same API as the request struct domain
  // object, that is |fuchsia_examples::EchoSendStringRequest|.
  void SendString(SendStringRequest& request, SendStringCompleter::Sync& completer) override {
    ZX_ASSERT(binding_ref_.has_value());

    // Handle a SendString request by sending an |OnString| event (an
    // unsolicited server-to-client message) back to the client.
    fit::result result = fidl::SendEvent(*binding_ref_)->OnString({request.value()});
    if (!result.is_ok()) {
      FX_LOGS(ERROR) << "Error sending event: " << result.error_value();
    }
  }

  // ... other methods from examples/fidl/cpp/server/main.cc omitted, to be covered later.

 private:
  // `ServerBindingRef` can be used to:
  // - Control the binding, such as to unbind the server from the channel or
  //   close the channel.
  // - Send events back to the client.
  // See the documentation comments on |fidl::ServerBindingRef|.
  std::optional<fidl::ServerBindingRef<fuchsia_examples::Echo>> binding_ref_;
};

將實作繫結至伺服器端點

實作要求處理常式只是工作的一部分,該伺服器必須能夠監控傳送到伺服器端點的新訊息。如要這麼做,EchoImpl 會定義另外兩個方法:一個 BindSelfManagedServer 靜態工廠函式,可建立新的 EchoImpl 例項來處理新伺服器端點 fidl::ServerEnd<fuchsia_examples::Echo> 上的要求,以及在連線終止時呼叫的 OnUnbound 方法:

/* Inside `class EchoImpl {`... */

  // Bind a new implementation of |EchoImpl| to handle requests coming from
  // the server endpoint |server_end|.
  static void BindSelfManagedServer(async_dispatcher_t* dispatcher,
                                    fidl::ServerEnd<fuchsia_examples::Echo> server_end) {
    // Create a new instance of |EchoImpl|.
    std::unique_ptr impl = std::make_unique<EchoImpl>();
    EchoImpl* impl_ptr = impl.get();

    // |fidl::BindServer| takes a FIDL protocol server implementation and a
    // channel. It asynchronously reads requests off the channel, decodes them
    // and dispatches them to the correct handler on the server implementation.
    //
    // The FIDL protocol server implementation can be passed as a
    // |std::shared_ptr|, |std::unique_ptr|, or raw pointer. For shared and
    // unique pointers, the binding will manage the lifetime of the
    // implementation object. For raw pointers, it's up to the caller to ensure
    // that the implementation object outlives the binding but does not leak.
    //
    // See the documentation comment of |fidl::BindServer|.
    fidl::ServerBindingRef binding_ref = fidl::BindServer(
        dispatcher, std::move(server_end), std::move(impl), std::mem_fn(&EchoImpl::OnUnbound));
    // Put the returned |binding_ref| into the |EchoImpl| object.
    impl_ptr->binding_ref_.emplace(std::move(binding_ref));
  }

  // This method is passed to the |BindServer| call as the last argument,
  // which means it will be called when the connection is torn down.
  // In this example we use it to log some connection lifecycle information.
  // Production code could do more things such as resource cleanup.
  void OnUnbound(fidl::UnbindInfo info, fidl::ServerEnd<fuchsia_examples::Echo> server_end) {
    // |is_user_initiated| returns true if the server code called |Close| on a
    // completer, or |Unbind| / |Close| on the |binding_ref_|, to proactively
    // teardown the connection. These cases are usually part of normal server
    // shutdown, so logging is unnecessary.
    if (info.is_user_initiated()) {
      return;
    }
    if (info.is_peer_closed()) {
      // If the peer (the client) closed their endpoint, log that as INFO.
      FX_LOGS(INFO) << "Client disconnected";
    } else {
      // Treat other unbind causes as errors.
      FX_LOGS(ERROR) << "Server error: " << info;
    }
  }

發布通訊協定實作

實作 FIDL 通訊協定的元件可以將該 FIDL 通訊協定公開給其他元件。 如要進一步瞭解完整的程序,請參閱「通訊協定開放流程」一文。我們可以使用 C++ 元件執行階段程式庫中的 component::OutgoingDirectory 執行繁重作業。

如何依附元件執行階段程式庫:

executable("bin") {
  output_name = "fidl_echo_cpp_server"
  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 publish capabilities, e.g. protocols,
    # to the component's outgoing directory.
    "//sdk/lib/component/outgoing/cpp",

    # This library provides an the asynchronous event loop implementation.
    "//zircon/system/ulib/async-loop:async-loop-cpp",
  ]
}

匯入 examples/fidl/cpp/server/main.cc 頂端的程式庫:

#include <fidl/fuchsia.examples/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/component/outgoing/cpp/outgoing_directory.h>
#include <lib/syslog/cpp/macros.h>

提供元件的傳出目錄:

int main(int argc, const char** argv) {
  // The event loop is used to asynchronously listen for incoming connections
  // and requests from the client. The following initializes the loop, and
  // obtains the dispatcher, which will be used when binding the server
  // implementation to a channel.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an |OutgoingDirectory| instance.
  //
  // The |component::OutgoingDirectory| class serves the outgoing directory for
  // our component. This directory is where the outgoing FIDL protocols are
  // installed so that they can be provided to other components.
  component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher);

  // The `ServeFromStartupInfo()` function sets up the outgoing directory with
  // the startup handle. The startup handle is a handle provided to every
  // component by the system, so that they can serve capabilities (e.g. FIDL
  // protocols) to other components.
  zx::result result = outgoing.ServeFromStartupInfo();
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string();
    return -1;
  }

  // ...

提供通訊協定

接著,伺服器會使用 outgoing.AddProtocol 註冊 Echo 通訊協定。

int main(int argc, const char** argv) {
  // The event loop is used to asynchronously listen for incoming connections
  // and requests from the client. The following initializes the loop, and
  // obtains the dispatcher, which will be used when binding the server
  // implementation to a channel.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an |OutgoingDirectory| instance.
  //
  // The |component::OutgoingDirectory| class serves the outgoing directory for
  // our component. This directory is where the outgoing FIDL protocols are
  // installed so that they can be provided to other components.
  component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher);

  // The `ServeFromStartupInfo()` function sets up the outgoing directory with
  // the startup handle. The startup handle is a handle provided to every
  // component by the system, so that they can serve capabilities (e.g. FIDL
  // protocols) to other components.
  zx::result result = outgoing.ServeFromStartupInfo();
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string();
    return -1;
  }

  // Register a handler for components trying to connect to fuchsia.examples.Echo.
  result = outgoing.AddUnmanagedProtocol<fuchsia_examples::Echo>(
      [dispatcher](fidl::ServerEnd<fuchsia_examples::Echo> server_end) {
        FX_LOGS(INFO) << "Incoming connection for "
                      << fidl::DiscoverableProtocolName<fuchsia_examples::Echo>;
        EchoImpl::BindSelfManagedServer(dispatcher, std::move(server_end));
      });
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to add Echo protocol: " << result.status_string();
    return -1;
  }

  FX_LOGS(INFO) << "Running C++ echo server with natural types";

  // This runs the event loop and blocks until the loop is quit or shutdown.
  // See documentation comments on |async::Loop|.
  loop.Run();
  return 0;
}

呼叫 AddProtocol 會在 FIDL 通訊協定名稱 (fidl::DiscoverableProtocolName<fuchsia_examples::Echo>"fuchsia.examples.Echo" 字串) 中安裝處理常式。當用戶端元件連線至 fuchsia.examples.Echo 時,outgoing 會呼叫透過與用戶端對應用戶端端點對應的伺服器端點所建立的 lambda 函式,而這個 lambda 函式會呼叫上述的 EchoImpl::BindSelfManagedServer,將伺服器端點繫結至 EchoImpl 的新執行個體。

我們的主要方法會持續監聽非同步迴圈中的傳入要求。

測試伺服器

建構伺服器後,您可以透過以下方式,在 Fuchsia 模擬器的運作中執行個體上執行範例:

ffx component run /core/ffx-laboratory:echo-server fuchsia-pkg://fuchsia.com/echo-cpp-server#meta/echo_server.cm

您應該會在裝置記錄 (ffx log) 中看到類似以下的輸出內容:

[ffx-laboratory:echo_server][][I] Running C++ echo server with natural types

伺服器現已執行並等候傳入要求。下一步是編寫傳送 Echo 通訊協定要求的用戶端。目前,您可以直接終止伺服器元件:

ffx component destroy /core/ffx-laboratory:echo_server

使用線路網域物件處理要求

上述教學課程會實作含有自然網域物件的伺服器:該伺服器會接收自然網域物件中代表的要求,並傳送以自然網域物件編碼的回覆。在最佳化效能與堆積分配量時,可能會實作支援線域物件 (例如接線伺服器) 的伺服器。以下是重新編寫的 EchoImpl,以使用線路網域物件:

class EchoImpl final : public fidl::WireServer<fuchsia_examples::Echo> {
 public:
  // The handler for `fuchsia.examples/Echo.EchoString`.
  //
  // For two-way methods (those with a response) like this one, the completer is
  // used complete the call: either to send the reply via |completer.Reply|, or
  // close the channel via |completer.Close|.
  //
  // |EchoStringRequestView| exposes the same API as a pointer to the request
  // struct domain object, that is
  // |fuchsia_examples::wire::EchoEchoStringRequest*|.
  void EchoString(EchoStringRequestView request, EchoStringCompleter::Sync& completer) override {
    // Call |Reply| to reply synchronously with the request value.
    completer.Reply(request->value);
  }

  // The handler for `fuchsia.examples/Echo.SendString`.
  //
  // For fire-and-forget methods like this one, the completer is normally not
  // used, but its |Close(zx_status_t)| method can be used to close the channel,
  // either when the protocol has reached its intended terminal state or the
  // server encountered an unrecoverable error.
  //
  // |SendStringRequestView| exposes the same API as a pointer to the request
  // struct domain object, that is
  // |fuchsia_examples::wire::EchoSendStringRequest*|.
  void SendString(SendStringRequestView request, SendStringCompleter::Sync& completer) override {
    // Handle a SendString request by sending an |OnString| event (an
    // unsolicited server-to-client message) back to the client.
    fidl::Status status = fidl::WireSendEvent(binding_)->OnString(request->value);
    if (!status.ok()) {
      FX_LOGS(ERROR) << "Error sending event: " << status.error();
    }
  }

  // ... |BindSelfManagedServer| etc omitted. Those stay the same.
};

在線路伺服器中使用的相關類別和函式的形狀與自然伺服器中使用的類別類似。呼叫其他類別或函式時,線路對應的開頭通常會加上 Wire。指標與參照及引數結構之間也有些微差異:

  • 自然伺服器實作的伺服器介面為 fidl::Server<fuchsia_examples::Echo>。有線伺服器實作的伺服器介面為 fidl::WireServer<fuchsia_examples::Echo>

  • 自然伺服器中的處理常式函式會接受要求訊息的參照。Reply 方法使用單一引數,即回應酬載網域物件:

    void EchoString(EchoStringRequest& request, EchoStringCompleter::Sync& completer) override {
      // Call |Reply| to reply synchronously with the request value.
      completer.Reply({{.response = request.value()}});
    }
    

    接線伺服器中的處理常式函式會接收要求訊息的檢視畫面 (類似指標)。如果回應酬載是結構,Reply 方法會將回應酬載中的結構體欄位清單壓縮成個別引數 (在此,一個 fidl::StringView 引數):

    void EchoString(EchoStringRequestView request, EchoStringCompleter::Sync& completer) override {
      // Call |Reply| to reply synchronously with the request value.
      completer.Reply(request->value);
    }
    
  • 傳送自然類型事件的函式為 fidl::SendEvent。傳送具有線類型事件的函式為 fidl::WireSendEvent。傳送事件時,結構欄位也會分割成不同的引數。

同一個 fidl::BindServer 函式可用來繫結自然伺服器或線路伺服器。

線路伺服器的完整範例程式碼位於 //examples/fidl/cpp/server/wire 的 Fuchsia 結帳頁面中。