在 Rust 中要求管道

必要條件

在本教學課程中,您將瞭解要求管道化模式及其優點。本教學課程假設您已熟悉撰寫及執行 FIDL 用戶端和伺服器的基本概念,這部分已在 Rust 入門教學課程中說明。

總覽

在 Fuchsia 上使用 FIDL 的常見做法,就是在通訊協定之間傳遞通訊協定本身。許多 FIDL 訊息皆包含用戶端端或通道伺服器端,而管道會透過不同的 FIDL 通訊協定進行通訊。在這種情況下,用戶端端指的是管道的遠端端實作指定的通訊協定,而伺服器端指的是遠端端針對指定的通訊協定提出要求。用戶端和伺服器端的另一組條款為通訊協定和通訊協定要求。

本教學課程涵蓋以下內容:

  • 在 FIDL 和 Rust FIDL 繫結中,這些用戶端和伺服器端的用法都會結束。
  • 要求管線模式及其優點。

本教學課程的完整範例程式碼位於 //examples/fidl/rust/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 的實作項目繫結至該管道。系統會假設提出要求的用戶端已持有用戶端,並在呼叫 GetEchoPipeliend 後,開始在該管道發出 Echo 要求。

顧名思義,後者會使用稱為通訊協定要求管道化的模式,也是較佳的做法。本教學課程會實作這兩種方法。

實作伺服器

實作 Echo 通訊協定

這個 Echo 實作可讓您指定前置字串,以便區分不同 Echo 伺服器執行個體:

// An Echo implementation that adds a prefix to every response
async fn run_echo_server(stream: EchoRequestStream, prefix: &str) -> Result<(), Error> {
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async move {
            match request {
                // The SendString request is not used in this example, so just
                // ignore it
                EchoRequest::SendString { value: _, control_handle: _ } => {}
                EchoRequest::EchoString { value, responder } => {
                    println!("Got echo request for prefix {}", prefix);
                    let response = format!("{}: {}", prefix, value);
                    responder.send(&response).context("error sending response")?;
                }
            }
            Ok(())
        })
        .await
}

由於用戶端只使用 EchoString,因此 SendString 處理常式為空。

實作 EchoLauncher 通訊協定

一般結構與 Echo 實作方式相似,但差別在於會使用 try_for_each_concurrent 而非 try_for_each。此範例中的用戶端會啟動兩個 Echo 例項,因此使用並行版本可讓兩個對 run_echo_server 的呼叫同時執行:

// The EchoLauncher implementation that launches Echo servers with the specified
// prefix
async fn run_echo_launcher_server(stream: EchoLauncherRequestStream) -> Result<(), Error> {
    // Currently the client only connects at most two Echo clients for each EchoLauncher
    stream
        .map(|result| result.context("request error"))
        .try_for_each_concurrent(2, |request| async move {
            let (echo_prefix, server_end) = match request {
                // In the non pipelined case, we need to initialize the
                // communication channel ourselves
                EchoLauncherRequest::GetEcho { echo_prefix, responder } => {
                    println!("Got non pipelined request");
                    let (client_end, server_end) = create_endpoints::<EchoMarker>();
                    responder.send(client_end)?;
                    (echo_prefix, server_end)
                }
                // In the pipelined case, the client is responsible for
                // initializing the channel, and passes the server its end of
                // the channel
                EchoLauncherRequest::GetEchoPipelined {
                    echo_prefix,
                    request,
                    control_handle: _,
                } => {
                    println!("Got pipelined request");
                    (echo_prefix, request)
                }
            };
            // Run the Echo server with the specified prefix
            run_echo_server(server_end.into_stream()?, &echo_prefix).await
        })
        .await
}

兩種 EchoLauncher 方法都會在管道的伺服器端呼叫 run_echo_server 來處理。差異在於,在 GetEcho 中,伺服器負責初始化管道 - 它會將一端用作伺服器端,並將另一端傳回用戶端。在 GetEchoPipelined 中,伺服器端會做為要求的一部分提供,因此伺服器不需要進行其他工作,也不需要做出回應。

// The EchoLauncher implementation that launches Echo servers with the specified
// prefix
async fn run_echo_launcher_server(stream: EchoLauncherRequestStream) -> Result<(), Error> {
    // Currently the client only connects at most two Echo clients for each EchoLauncher
    stream
        .map(|result| result.context("request error"))
        .try_for_each_concurrent(2, |request| async move {
            let (echo_prefix, server_end) = match request {
                // In the non pipelined case, we need to initialize the
                // communication channel ourselves
                EchoLauncherRequest::GetEcho { echo_prefix, responder } => {
                    println!("Got non pipelined request");
                    let (client_end, server_end) = create_endpoints::<EchoMarker>();
                    responder.send(client_end)?;
                    (echo_prefix, server_end)
                }
                // In the pipelined case, the client is responsible for
                // initializing the channel, and passes the server its end of
                // the channel
                EchoLauncherRequest::GetEchoPipelined {
                    echo_prefix,
                    request,
                    control_handle: _,
                } => {
                    println!("Got pipelined request");
                    (echo_prefix, request)
                }
            };
            // Run the Echo server with the specified prefix
            run_echo_server(server_end.into_stream()?, &echo_prefix).await
        })
        .await
}

提供 EchoLauncher 通訊協定

主迴圈應與伺服器教學課程中的迴圈相同,但會提供 EchoLauncher 而非 Echo

enum IncomingService {
    EchoLauncher(EchoLauncherRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::EchoLauncher);
    fs.take_and_serve_directory_handle()?;

    const MAX_CONCURRENT: usize = 1000;
    let fut = fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::EchoLauncher(stream)| {
        run_echo_launcher_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    });

    println!("Running echo launcher server");
    fut.await;
    Ok(())
}

建構伺服器

您可以視需要嘗試建構伺服器,以便檢查設定是否正確無誤:

  1. 設定 GN 建構作業,以便納入伺服器:

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

    fx build
    

實作用戶端

連線至 EchoLauncher 伺服器後,用戶端程式碼會使用 GetEcho 連線至一個 Echo 例項,並使用 GetEchoPipelined 連線至另一個例項,然後對每個例項提出 EchoString 要求。

以下是非管道的程式碼:

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    let echo_launcher =
        connect_to_protocol::<EchoLauncherMarker>().context("Failed to connect to echo service")?;

    // Create a future that obtains an Echo protocol using the non-pipelined
    // GetEcho method
    let non_pipelined_fut = async {
        let client_end = echo_launcher.get_echo("not pipelined").await?;
        // "Upgrade" the client end in the response into an Echo proxy, and
        // make an EchoString request on it
        let proxy = client_end.into_proxy()?;
        proxy.echo_string("hello").map_ok(|val| println!("Got echo response {}", val)).await
    };

    // Create a future that obtains an Echo protocol using the pipelined GetEcho
    // method
    let (proxy, server_end) = create_proxy::<EchoMarker>()?;
    echo_launcher.get_echo_pipelined("pipelined", server_end)?;
    // We can make a request to the server right after sending the pipelined request
    let pipelined_fut =
        proxy.echo_string("hello").map_ok(|val| println!("Got echo response {}", val));

    // Run the two futures to completion
    let (non_pipelined_result, pipelined_result) = join!(non_pipelined_fut, pipelined_fut);
    pipelined_result?;
    non_pipelined_result?;
    Ok(())
}

此程式碼會將兩個 Future 鏈結在一起。首先,它會向用戶端提出 GetEcho 要求。接著,它會採用該 Future 的結果,然後使用該 Future 建立用戶端物件 (proxy)、呼叫 EchoString,然後使用 await 對結果進行封鎖。

雖然必須先初始化管道,但管道化程式碼會簡單許多:

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    let echo_launcher =
        connect_to_protocol::<EchoLauncherMarker>().context("Failed to connect to echo service")?;

    // Create a future that obtains an Echo protocol using the non-pipelined
    // GetEcho method
    let non_pipelined_fut = async {
        let client_end = echo_launcher.get_echo("not pipelined").await?;
        // "Upgrade" the client end in the response into an Echo proxy, and
        // make an EchoString request on it
        let proxy = client_end.into_proxy()?;
        proxy.echo_string("hello").map_ok(|val| println!("Got echo response {}", val)).await
    };

    // Create a future that obtains an Echo protocol using the pipelined GetEcho
    // method
    let (proxy, server_end) = create_proxy::<EchoMarker>()?;
    echo_launcher.get_echo_pipelined("pipelined", server_end)?;
    // We can make a request to the server right after sending the pipelined request
    let pipelined_fut =
        proxy.echo_string("hello").map_ok(|val| println!("Got echo response {}", val));

    // Run the two futures to completion
    let (non_pipelined_result, pipelined_result) = join!(non_pipelined_fut, pipelined_fut);
    pipelined_result?;
    non_pipelined_result?;
    Ok(())
}

使用 create_proxy,這是用來建立管道兩端的捷徑,並將其中一個端轉換為 Proxy。呼叫 GetEchoPipelined 後,用戶端可以立即提出 EchoString 要求。

最後,我們會同時執行對應於非管線和管線呼叫的兩個未來,看看哪一個會先完成:

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    let echo_launcher =
        connect_to_protocol::<EchoLauncherMarker>().context("Failed to connect to echo service")?;

    // Create a future that obtains an Echo protocol using the non-pipelined
    // GetEcho method
    let non_pipelined_fut = async {
        let client_end = echo_launcher.get_echo("not pipelined").await?;
        // "Upgrade" the client end in the response into an Echo proxy, and
        // make an EchoString request on it
        let proxy = client_end.into_proxy()?;
        proxy.echo_string("hello").map_ok(|val| println!("Got echo response {}", val)).await
    };

    // Create a future that obtains an Echo protocol using the pipelined GetEcho
    // method
    let (proxy, server_end) = create_proxy::<EchoMarker>()?;
    echo_launcher.get_echo_pipelined("pipelined", server_end)?;
    // We can make a request to the server right after sending the pipelined request
    let pipelined_fut =
        proxy.echo_string("hello").map_ok(|val| println!("Got echo response {}", val));

    // Run the two futures to completion
    let (non_pipelined_result, pipelined_result) = join!(non_pipelined_fut, pipelined_fut);
    pipelined_result?;
    non_pipelined_result?;
    Ok(())
}

建構用戶端

您可以視需要嘗試建構用戶端,以便檢查設定是否正確無誤:

  1. 設定 GN 建構作業,以便納入伺服器:

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

    fx build
    

執行程式碼範例

在本教學課程中,我們會提供 fuchsia.examples.Echofuchsia.examples.EchoLauncher

中探索領域元件的完整來源。
  1. 設定建構作業,加入提供的套件,其中包含回音領域、伺服器和用戶端:

    fx set core.x64 --with //examples/fidl/rust:echo-launcher-rust
    
  2. 建構 Fuchsia 映像檔:

    fx build
    
  3. 執行 echo_realm 元件。這會建立用戶端和伺服器元件執行個體,並將功能導向:

    ffx component run /core/ffx-laboratory:echo_realm fuchsia-pkg://fuchsia.com/echo-launcher-rust#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] Got pipelined request
[echo_server][][I] Got echo request for prefix pipelined
[echo_server][][I] Got non pipelined request
[echo_client][][I] Got echo response pipelined: hello
[echo_server][][I] Got echo request for prefix not pipelined
[echo_client][][I] Got echo response not pipelined: hello

根據列印順序,您可以看到管線化案例的速度較快。即使先傳送非管線要求,管線情況的回音回應還是會先傳送,因為要求管線可節省用戶端和伺服器之間的來回傳輸。要求管道也會簡化程式碼。

如要進一步瞭解通訊協定要求管道化,包括如何處理可能失敗的通訊協定要求,請參閱 FIDL API 大綱

終止領域元件以停止執行並清除元件執行個體:

ffx component destroy /core/ffx-laboratory:echo_realm