必要條件
在本教學課程中,您將瞭解要求管道模式及其優點。本教學課程假設您已熟悉編寫及執行 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(())
}
建構伺服器
視需要建構伺服器,確認一切正常:
設定 GN 建構作業,加入伺服器:
fx set core.x64 --with //examples/fidl/rust/request_pipelining/server:echo-server
建構 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 的結果,然後用來建立用戶端物件 (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
要求。
最後,系統會並行執行對應於非管道化和管道化呼叫的兩個 Future,查看哪個先完成:
#[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(())
}
建構用戶端
視需要嘗試建構用戶端,確認一切是否正確:
設定 GN 建構作業,加入伺服器:
fx set core.x64 --with //examples/fidl/rust/request_pipelining/client:echo-client
建構 Fuchsia 映像檔:
fx build
執行範例程式碼
本教學課程提供
<0x0Afuchsia.examples.Echo
fuchsia.examples.EchoLauncher
設定建構作業,以便納入提供的套件,其中包含 echo 領域、伺服器和用戶端:
fx set core.x64 --with //examples/fidl/rust:echo-launcher-rust
建構 Fuchsia 映像檔:
fx build
執行
echo_realm
元件。這會建立用戶端和伺服器元件例項,並將功能路徑導向:ffx component run /core/ffx-laboratory:echo_realm fuchsia-pkg://fuchsia.com/echo-launcher-rust#meta/echo_realm.cm
啟動
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