在 Rust 中实现 FIDL 服务器

前提条件

本教程假定您熟悉列出 FIDL Rust 的步骤 将某个库的绑定作为 GN 中的依赖项,并将绑定导入 Rust 代码,在 Rust FIDL crate 教程中进行了介绍。

概览

本教程介绍了如何实现 FIDL 协议 (fuchsia.examples.Echo) 并在 Fuchsia 上运行它。此协议有一种方法 :

  • EchoString 是带有响应的方法。
  • SendString 是没有响应的方法。
  • OnString是一个事件。
@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 概念页面。

本文档介绍了如何完成以下任务:

  • 实现 FIDL 协议。
  • 在 Fuchsia 上构建并运行软件包。
  • 提供 FIDL 协议。

本教程首先创建一个投放到 Fuchsia 设备的组件 并运行然后,逐步添加功能,让服务器正常运行。

如果要自行编写代码,请删除以下目录:

rm -r examples/fidl/rust/server/*

创建组件

要创建组件,请执行以下操作:

  1. main() 函数添加到 examples/fidl/rust/server/src/main.rs

    fn main() {
      println!("Hello, world!");
    }
    
  2. examples/fidl/rust/server/BUILD.gn 中声明服务器的目标:

    import("//build/rust/rustc_binary.gni")
    
    
    # Declare an executable for the server. This produces a binary with the
    # specified output name that can run on Fuchsia.
    rustc_binary("bin") {
      output_name = "fidl_echo_rust_server"
      edition = "2021"
    
      sources = [ "src/main.rs" ]
    }
    
    # Declare a component for the server, which consists of the manifest and the
    # binary that the component will run.
    fuchsia_component("echo-server") {
      component_name = "echo_server"
      manifest = "meta/server.cml"
      deps = [ ":bin" ]
    }
    
    # Declare a package that contains a single component, our server.
    fuchsia_package("echo-rust-server") {
      deps = [ ":echo-server" ]
    }
    

    要启动并运行服务器组件,需要设置 定义:

    • 服务器构建为在 Fuchsia 上运行的原始可执行文件。
    • 一种组件,设置为仅运行服务器可执行文件, 该声明使用组件的清单文件进行描述
    • 然后,该组件被放入软件包中,软件包是软件单元 在 Fuchsia 上的应用分发。在本例中, 包只包含 单个组件。

    如需详细了解软件包、组件以及如何构建它们,请参阅 构建组件页面。

  3. examples/fidl/rust/server/meta/server.cml 中添加组件清单:

    {
        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_rust_server",
        },
    
        // Capabilities provided by this component.
        capabilities: [
            { protocol: "fuchsia.examples.Echo" },
        ],
        expose: [
            {
                protocol: "fuchsia.examples.Echo",
                from: "self",
            },
        ],
    }
    
    
  4. 将该服务器添加到您的构建配置中:

    fx set core.x64 --with //examples/fidl/rust/server:echo-rust-server
  5. 构建 Fuchsia 映像:

    fx build

实现服务器

首先,您将实现 Echo 协议的行为。在 Rust 中, 可处理协议关联请求流类型的代码,在本例中为 EchoRequestStream。此类型是 Echo 请求流,也就是说,它会实现 futures::Stream<Item = Result<EchoRequest, fidl::Error>>

您将实现 run_echo_server() 来处理请求流, 后者是一个用于处理传入服务请求的异步函数。 它会返回在客户端通道关闭后完成的 Future。

添加依赖项

  1. 导入所需的依赖项:

    // we'll use anyhow to propagate errors that occur when handling the request stream
    use anyhow::{Context as _, Error};
    // the server will need to handle an EchoRequestStream
    use fidl_fuchsia_examples::{EchoRequest, EchoRequestStream};
    // import the futures prelude, which includes things like the Future and Stream traits
    use futures::prelude::*;
    
  2. 将它们作为 build 依赖项添加到 rustc_binary 目标中。deps 字段应如下所示:

    deps = [
      "//examples/fidl/fuchsia.examples:fuchsia.examples_rust",
      "//third_party/rust_crates:anyhow",
      "//third_party/rust_crates:futures",
    ]
    

定义 run_echo_server

// An implementation of the Echo stream, which handles a stream of EchoRequests
async fn run_echo_server(stream: EchoRequestStream) -> Result<(), Error> {
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async move {
            match request {
                // Handle each EchoString request by responding with the request
                // value
                EchoRequest::EchoString { value, responder } => {
                    println!("Received EchoString request for string {:?}", value);
                    responder.send(&value).context("error sending response")?;
                    println!("Response sent successfully");
                }
                // Handle each SendString request by sending a single OnString
                // event with the request value
                EchoRequest::SendString { value, control_handle } => {
                    println!("Received SendString request for string {:?}", value);
                    control_handle.send_on_string(&value).context("error sending event")?;
                    println!("Event sent successfully");
                }
            }
            Ok(())
        })
        .await
}

该实现包含以下元素:

  • 该代码会将请求流中的 fidl:Error 转换为 anyhow::Error,方法是将 对每个结果使用 .context() 方法:

    // An implementation of the Echo stream, which handles a stream of EchoRequests
    async fn run_echo_server(stream: EchoRequestStream) -> Result<(), Error> {
        stream
            .map(|result| result.context("failed request"))
            .try_for_each(|request| async move {
                match request {
                    // Handle each EchoString request by responding with the request
                    // value
                    EchoRequest::EchoString { value, responder } => {
                        println!("Received EchoString request for string {:?}", value);
                        responder.send(&value).context("error sending response")?;
                        println!("Response sent successfully");
                    }
                    // Handle each SendString request by sending a single OnString
                    // event with the request value
                    EchoRequest::SendString { value, control_handle } => {
                        println!("Received SendString request for string {:?}", value);
                        control_handle.send_on_string(&value).context("error sending event")?;
                        println!("Event sent successfully");
                    }
                }
                Ok(())
            })
            .await
    }
    

    在此阶段,Result<EchoRequest, fidl::Error> 流会变为 Result<EchoRequest, anyhow::Error>

  • 然后,该函数会对生成的流调用 try_for_each,从而返回 。此方法解封数据流中的 Result - 任何失败都会导致未来 立即返回包含该错误的错误消息,任何成功的内容都会传递到 。同样,如果闭包的返回值解析为失败,则生成的 Future 会立即返回并显示该错误:

    // An implementation of the Echo stream, which handles a stream of EchoRequests
    async fn run_echo_server(stream: EchoRequestStream) -> Result<(), Error> {
        stream
            .map(|result| result.context("failed request"))
            .try_for_each(|request| async move {
                match request {
                    // Handle each EchoString request by responding with the request
                    // value
                    EchoRequest::EchoString { value, responder } => {
                        println!("Received EchoString request for string {:?}", value);
                        responder.send(&value).context("error sending response")?;
                        println!("Response sent successfully");
                    }
                    // Handle each SendString request by sending a single OnString
                    // event with the request value
                    EchoRequest::SendString { value, control_handle } => {
                        println!("Received SendString request for string {:?}", value);
                        control_handle.send_on_string(&value).context("error sending event")?;
                        println!("Event sent successfully");
                    }
                }
                Ok(())
            })
            .await
    }
    
  • 闭包的内容通过将传入的 EchoRequest 与 确定请求类型:

    // An implementation of the Echo stream, which handles a stream of EchoRequests
    async fn run_echo_server(stream: EchoRequestStream) -> Result<(), Error> {
        stream
            .map(|result| result.context("failed request"))
            .try_for_each(|request| async move {
                match request {
                    // Handle each EchoString request by responding with the request
                    // value
                    EchoRequest::EchoString { value, responder } => {
                        println!("Received EchoString request for string {:?}", value);
                        responder.send(&value).context("error sending response")?;
                        println!("Response sent successfully");
                    }
                    // Handle each SendString request by sending a single OnString
                    // event with the request value
                    EchoRequest::SendString { value, control_handle } => {
                        println!("Received SendString request for string {:?}", value);
                        control_handle.send_on_string(&value).context("error sending event")?;
                        println!("Event sent successfully");
                    }
                }
                Ok(())
            })
            .await
    }
    

    此实现通过回显输入来处理 EchoString 请求, 通过发送 OnString 事件实现 SendString 请求。由于 SendString 是一个火并且忽略 方法,请求枚举变体附带一个控制句柄,可用于 与服务器通信

    在这两种情况下,将消息发送回客户端的错误都是通过添加上下文来传播 使用 ? 运算符。如果成功到达关闭的终点,则返回 Ok(())

  • 最后,服务器函数awaittry_for_each 返回至完成的 Future, 将对每个传入请求调用闭包,并在所有请求均已处理完毕时返回 或遇到任何错误。

您可以通过运行以下命令来验证实现是否正确:

fx build

提供协议

现在,您已经定义了用于处理传入请求的代码,接下来您需要监听传入 Echo 服务器的连接。方法是询问 组件管理器,以便为其他组件提供 Echo 协议。通过 然后,配套管理器会将 echo 协议的所有请求路由到我们的服务器。

要执行这些请求,组件管理器需要协议的名称 以及在有传入请求时应调用的处理程序。 连接到与指定名称匹配的协议。

添加依赖项

  1. 导入所需的依赖项:

    // Import the Fuchsia async runtime in order to run the async main function
    use fuchsia_async as fasync;
    // ServiceFs is a filesystem used to connect clients to the Echo service
    use fuchsia_component::server::ServiceFs;
    
  2. 将它们作为 build 依赖项添加到 rustc_binary 目标中。完整目标如下所示:

    rustc_binary("bin") {
      name = "fidl_echo_rust_server"
      edition = "2021"
    
      deps = [
        "//examples/fidl/fuchsia.examples:fuchsia.examples_rust",
        "//src/lib/fuchsia",
        "//src/lib/fuchsia-component",
        "//third_party/rust_crates:anyhow",
        "//third_party/rust_crates:futures",
      ]
    
      sources = [ "src/main.rs" ]
    }
    
    

定义 main 函数

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    // Initialize the outgoing services provided by this component
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Echo);

    // Serve the outgoing services
    fs.take_and_serve_directory_handle()?;

    // Listen for incoming requests to connect to Echo, and call run_echo_server
    // on each one
    println!("Listening for incoming connections...");
    const MAX_CONCURRENT: usize = 10_000;
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Echo(stream)| {
        run_echo_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

主要函数是异步的,因为它涉及监听到 回声服务器。fuchsia::main 属性指示紫红色异步运行时运行 main 未来在单个线程上完成。

main 也会返回 Result<(), Error>。如果从 main 返回 Error 因为其中有 ? 行,那么系统会输出 Debug 错误, 程序会返回一个表示失败的状态代码。

初始化 ServiceFs

获取 ServiceFs 的实例,该实例表示包含各种服务的文件系统。 由于服务器将在单线程中运行,因此请使用 ServiceFs::new_local(),而不是 ServiceFs::new()(后者支持多线程)。

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    // Initialize the outgoing services provided by this component
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Echo);

    // Serve the outgoing services
    fs.take_and_serve_directory_handle()?;

    // Listen for incoming requests to connect to Echo, and call run_echo_server
    // on each one
    println!("Listening for incoming connections...");
    const MAX_CONCURRENT: usize = 10_000;
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Echo(stream)| {
        run_echo_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

添加 Echo FIDL 服务

请求组件管理器公开 Echo FIDL 服务。此部分包含两个部分 函数调用:

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    // Initialize the outgoing services provided by this component
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Echo);

    // Serve the outgoing services
    fs.take_and_serve_directory_handle()?;

    // Listen for incoming requests to connect to Echo, and call run_echo_server
    // on each one
    println!("Listening for incoming connections...");
    const MAX_CONCURRENT: usize = 10_000;
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Echo(stream)| {
        run_echo_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}
  • 组件管理器必须知道如何处理传入的连接请求。这是指定的 方法是传入一个接受 fidl::endpoints::RequestStream 的闭包,并返回一些新的 值。例如,传入 |stream: EchoRequestStream| stream 的闭包会 是完全有效的。常见模式 定义服务器提供的可能服务的枚举,在此示例中:

    enum IncomingService {
        // Host a service protocol.
        Echo(EchoRequestStream),
        // ... more services here
    }
    

    然后传递枚举变体“structor”关闭。当有多项服务时 这会产生常见的返回值类型(IncomingService 枚举)。回归 在以下情况下,所有 add_fidl_service 闭包的值都将成为 ServiceFs 流中的元素: 监听传入连接

  • 组件管理器还必须知道该服务将在何处提供。 由于这是一项传出服务(即提供给其他组件的服务), 该服务必须在 /svc 目录内添加路径。add_fidl_service 会获取 通过采用 SERVICE_NAME 隐式创建路径 与闭包输入参数相关联。 在本例中,闭包实参 (IncomingService::Echo) 有一个类型为 EchoRequestStream,其关联 SERVICE_NAME"fuchsia.examples.Echo"。所以这个 调用会在 /svc/fuchsia.examples.Echo 添加一个条目,而客户需要搜索 名为 "fuchsia.examples.Echo" 的服务连接到此服务器。

提供传出目录

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    // Initialize the outgoing services provided by this component
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Echo);

    // Serve the outgoing services
    fs.take_and_serve_directory_handle()?;

    // Listen for incoming requests to connect to Echo, and call run_echo_server
    // on each one
    println!("Listening for incoming connections...");
    const MAX_CONCURRENT: usize = 10_000;
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Echo(stream)| {
        run_echo_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

此调用会将 ServiceFs 绑定到组件的 DirectoryRequest 启动句柄; 监听传入的连接请求。 请注意,由于这会从进程的句柄表中删除句柄, 每个进程只能调用此函数一次。如果您希望提供 ServiceFs 到其他频道,则可以使用 serve_connection 函数。

此过程在 开放协议的生命周期

监听传入连接

运行 ServiceFs 以完成操作,以监听传入连接:

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    // Initialize the outgoing services provided by this component
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Echo);

    // Serve the outgoing services
    fs.take_and_serve_directory_handle()?;

    // Listen for incoming requests to connect to Echo, and call run_echo_server
    // on each one
    println!("Listening for incoming connections...");
    const MAX_CONCURRENT: usize = 10_000;
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Echo(stream)| {
        run_echo_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

这会运行 ServiceFs Future,最多可处理 10,000 个传入 同时处理多个请求传递到此调用的闭包是用于传入请求的处理程序 - ServiceFs 将首先将传入的连接与提供给 add_fidl_service 的闭包进行匹配, 然后对结果(即 IncomingService)调用处理程序。处理程序将 IncomingService,然后对内部请求流调用 run_echo_server 来处理传入 回声请求。

此处处理的请求有两种。由 ServiceFs 包含连接到 Echo 服务器的请求(也就是说,每个客户端都会将此类型设为 一次请求),而由服务器处理的请求 run_echo_server 是基于 Echo 协议的请求(即每个客户端可以发出任意数量的 向服务器发出 EchoStringSendString 请求)。许多客户端可以请求连接到 同时回声服务器,以便同时处理这些请求流。不过, 针对单个客户端的请求是按顺序进行的,因此处理请求并不会带来任何好处 同时进行。

测试服务器

重建:

fx build

然后运行服务器组件:

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

注意:组件是使用其 组件网址 , 该值取决于 `fuchsia-pkg://` 架构。

您应该会在设备日志 (ffx log) 中看到类似于以下内容的输出:

[ffx-laboratory:echo_server][][I] Listening for incoming connections...

服务器现在正在运行并等待传入请求。 下一步是编写一个发送 Echo 协议请求的客户端。 目前,您可以直接终止服务器组件:

ffx component destroy /core/ffx-laboratory:echo_server