Prerequisites
In this tutorial, you'll learn about the request pipelining pattern and its benefits. This tutorial expects you to already be familiar with the basics of writing and running FIDL clients and servers, which is covered in the Rust getting started tutorials.
Overview
A common aspect of using FIDL on Fuchsia is passing protocols themselves across protocols. Many FIDL messages include either the client end or the server end of a channel, where the channel is used to communicate over a different FIDL protocol. In this case, client end means that the remote end of the channel implements the specified protocol, whereas server end means that the remote end is making requests for the specified protocol. An alternate set of terms for client end and server end are protocol and protocol request.
This tutorial covers:
- The usage of these client and server ends, both in FIDL and in the Rust FIDL bindings.
- The request pipelining pattern and its benefits.
The full example code for this tutorial is located at
//examples/fidl/rust/request_pipelining
.
The FIDL protocol
This tutorial implements the EchoLauncher
protocol from the
fuchsia.examples library:
@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;
});
};
This is a protocol that lets clients retrieve an instance of the Echo
protocol. Clients can specify a prefix, and the resulting Echo
instance
adds that prefix to every response.
There are two methods that can be used to accomplish this:
GetEcho
: Takes the prefix as a request, and responds with the client end of a channel connected to an implementation of theEcho
protocol. After receiving the client end in the response, the client can start making requests on theEcho
protocol using the client end.GetEchoPipelined
: Takes the prefix and the server end of a channel as a request, and binds an implementation ofEcho
to it. The client that made the request is assumed to already hold the client end, and will start makingEcho
requests on that channel after callingGetEchoPipeliend
.
As the name suggests, the latter uses a pattern called protocol request pipelining, and is the preferred approach. This tutorial implements both approaches.
Implement the server
Implement the Echo protocol
This implementation of Echo
allows specifying a prefix in order to
distinguish between the different instances of Echo
servers:
// 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
}
The SendString
handler is empty as the client just uses EchoString
.
Implement the EchoLauncher protocol
The general structure is similar to the Echo
implementation, but one difference is that the
try_for_each_concurrent
is used instead of try_for_each
. The client in this example launches
two instances of Echo
, so, using the concurrent version allows the two calls to
run_echo_server
to be run concurrently:
// 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
}
Both of the EchoLauncher
methods are handled by calling run_echo_server
on the server end of
the channel. The difference is that in GetEcho
, the server is responsible for initializing the
channel - it uses one end as the server end and sends the other end back to the client. In
GetEchoPipelined
, the server end is provided as part of the request, so no additional work needs
to be done by the server, and no response is necessary.
// 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
}
Serve the EchoLauncher protocol
The main loop should is the same as in the
server tutorial but serves an EchoLauncher
instead of 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(())
}
Build the server
Optionally, to check that things are correct, try building the server:
Configure your GN build to include the server:
fx set core.x64 --with //examples/fidl/rust/request_pipelining/server:echo-server
Build the Fuchsia image:
fx build
Implement the client
After connecting to the EchoLauncher
server, the client
code connects to one instance of Echo
using GetEcho
and another using
GetEchoPipelined
and then makes an EchoString
request on each instance.
This is the non-pipelined code:
#[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(())
}
This code chains together two futures. First, it makes the GetEcho
request to the client. It then
takes the result of that future, and then uses it to create a client object (the proxy
), calls
EchoString
, and then blocks on the result using await
.
Despite having to initialize the channel first, the pipelined code is much simpler:
#[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
is used, which is a shortcut for creating the two ends of a channel and converting
one end into a proxy. After the call to GetEchoPipelined
, the client can immediately make the
EchoString
request.
Finally, the two futures corresponding to the non-pipelined and pipelined calls are run to completion concurrently, to see which one completes first:
#[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(())
}
Build the client
Optionally, to check that things are correct, try building the client:
Configure your GN build to include the server:
fx set core.x64 --with //examples/fidl/rust/request_pipelining/client:echo-client
Build the Fuchsia image:
fx build
Run the example code
For this tutorial, a
realm
component is
provided to declare the appropriate capabilities and routes for
fuchsia.examples.Echo
and fuchsia.examples.EchoLauncher
.
Configure your build to include the provided package that includes the echo realm, server, and client:
fx set core.x64 --with //examples/fidl/rust:echo-launcher-rust
Build the Fuchsia image:
fx build
Run the
echo_realm
component. This creates the client and server component instances and routes the capabilities:ffx component run /core/ffx-laboratory:echo_realm fuchsia-pkg://fuchsia.com/echo-launcher-rust#meta/echo_realm.cm
Start the
echo_client
instance:ffx component start /core/ffx-laboratory:echo_realm/echo_client
The server component starts when the client attempts to connect to the
EchoLauncher
protocol. You should see output similar to the following
in the device logs (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
Based on the print order, you can see that the pipelined case is faster. The echo response for the pipelined case arrives first, even though the non pipelined request is sent first, since request pipelining saves a roundtrip between the client and server. Request pipelining also simplifies the code.
For further reading about protocol request pipelining, including how to handle protocol requests that may fail, see the FIDL API rubric.
Terminate the realm component to stop execution and clean up the component instances:
ffx component destroy /core/ffx-laboratory:echo_realm