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_;
};

由於用戶端僅使用 EchoString,因此 SendString 處理常式是空的。

實作 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 版本以納入伺服器:

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

    fx build
    

實作用戶端

連線到 EchoLauncher 伺服器後,用戶端程式碼會使用 GetEchoGetEchoPipelined 連線至一個 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 版本以納入伺服器:

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

    fx build
    

執行範例程式碼

fuchsia.examples.Echofuchsia.examples.EchoLauncher

  1. 設定您的版本以納入包含 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