一般建议
本部分介绍了有关 定义协议的 Fuchsia 接口定义 语言。
另请参阅 FIDL 样式指南。
协议而非对象
FIDL 是一种用于定义进程间通信协议的语言。虽然 其语法类似于面向对象的接口的定义, 更类似于网络协议,而不是对象系统。对于 例如,要设计高质量的协议,您需要考虑带宽、 延迟时间和流控制你还应该考虑 不仅仅是操作的逻辑分组:协议还施加了 FIFO 对请求进行排序并将协议分解为两个较小的协议意味着 两个不同协议发出的请求可以按照 相互通信。
专注于类型
设计 FIDL 协议的一个良好起点是设计数据 您的协议将使用的结构。例如,有关 网络可能包含适用于各种 IP 地址的数据结构, 地址和关于图形的 FIDL 协议可能包含数据, 各种几何概念的结构。您应该能够查看 对协议操作的概念有一定的了解 以及这些概念的结构。
语言中立性
有许多不同语言的 FIDL 后端。您应该避免 针对任何特定目标语言过度专业化 FIDL 定义。 随着时间的推移,您的 FIDL 协议可能会用于许多不同的语言, 甚至是目前不支持的语言。FIDL 是 将系统固定在一起的粘合剂,让 Fuchsia 支持各种 语言包和运行时如果您对自己偏爱的语言过于专业, 会破坏核心价值主张。
序数
协议包含许多方法。系统会自动为每种方法分配 唯一的 64 位标识符(称为序数)。服务器使用序数值 以确定应分派的协议方法。
编译器通过对库、协议和
方法名称。在极少数情况下,同一协议中的序数可能会冲突。如果
在这种情况下,您可以使用 Selector
属性更改
是编译器用来进行哈希处理的方法。以下示例将使用方法
名称“C”而不是方法名称“B”计算哈希值:
protocol A {
@selector("C")
B(struct {
s string;
b bool;
});
};
选择器还可用于保持与数据线的向后兼容性 格式。
诊断
有时,需要公开一些有助于调试或诊断 计划。这些数据可以采用统计信息和指标的形式(如错误次数、来电次数、 尺寸等)、对开发有用的信息、组件的运行状况或类似信息。
我们很容易会试图在生产环境中通过测试协议或调试方法公开这些信息。 协议。但是, Fuchsia 提供了一种单独的机制来公开此类信息: 检查:应将其纳入考虑范围以就如何公开数据做出最佳决策 这种类型的数据。需要检查时,应使用检查,而不是 FIDL 方法/协议 显示有关在测试中进行调试的程序的诊断信息,供开发者使用 或通过崩溃报告或指标在现场进行检索,前提是其他任何程序 以便做出运行时决策。
如果根据其他工具的诊断信息做出运行时决策,则应使用 FIDL 计划。绝不能将检查用于项目间的沟通,这是一种尽力而为的系统 在生产环境中运行时,不得据此作出决策或更改行为。
决定是使用 Inspect 还是 FIDL 的启发法可以是:
其他程序是否会在生产环境中使用这些数据?
- 是:使用 FIDL。
这些数据是否会用于崩溃报告或指标?
- 是:使用“检查”。
测试或开发者工具会使用这些数据吗?它是否有机会用于生产环境?
- 是:使用 FIDL。
- 否:两者都使用。
库结构
将 FIDL 声明分组到 FIDL 库中有两个特定目标:
- 帮助 FIDL 开发者(使用 FIDL 库的开发者)熟悉 API 。
- 提供以分层方式确定 FIDL 中 FIDL 声明的范围的结构 库。
请仔细考虑如何将类型和协议定义划分为 库。如何将这些定义分解为库有很大一部分 对这些定义的使用者的影响,因为 FIDL 库是 依赖项和分布。
FIDL 编译器要求库之间的依赖关系图为 DAG, 这意味着您不能跨库边界创建循环依赖关系。 不过,您可以在库中创建(某些)循环依赖项。
如需决定是否将库分解为较小的库,请考虑使用 以下问题:
该库的客户是否分为不同的角色, 想要使用该库中的功能或声明的子集?如果 因此,您可以考虑将库分解为多个单独的库,分别针对 角色。
库对应的行业概念是否 结构?如果是这样,请考虑对库进行结构化设计, 符合业界标准的结构例如,蓝牙分为
fuchsia.bluetooth.le
和fuchsia.bluetooth.gatt
,以匹配这些 业内对概念的理解比较简单。同样,fuchsia.net.http
对应于业界标准的 HTTP 网络 协议。许多其他库是否依赖于该库?如果是,请检查这些 传入的依赖项确实需要依赖于整个库 有一个“核心”指标可以从 用于接收大部分传入依赖项。
理想情况下,我们会为整个 Fuchsia 生成一个 FIDL 库结构, 是全球最佳选择。然而,康威法律规定, 设计系统 [...] 只能生成 这些组织的沟通结构。”我们应该投入 反抗康威定律的时间适度
访问权限控制以协议粒度级别
在决定在哪个库中定义协议时,不要考虑 访问权限控制注意事项通常,访问控制以 协议粒度。定义协议的库没有任何作用 并且不能用于确定是否可 。
例如,进程可能会访问 fuchsia.logger.LogSink
,或
给定 fuchsia.media.StreamSource
协议的客户端。不过,FIDL
不可用于指定对fuchsia.logger
的访问权限,
库,或阻止访问 fuchsia.ldsvc
库。
fuchsia
命名空间
平台源代码树中定义的 FIDL 库(即
fuchsia.googlesource.com)必须位于
fuchsia
顶级命名空间(例如,fuchsia.ui
),除非出现以下任一情况
为 true:
- 库定义了 FIDL 语言本身或其
一致性测试套件,在这种情况下,顶级命名空间必须为
fidl
。 - 该库仅用于内部测试,不包含在 SDK 中
或者在正式版 build 中,在这种情况下,顶级命名空间必须为
test
。
顶级命名空间 fuchsia
命名空间中的 FIDL 库严格遵循
最好使用不超过四个组件,即 fuchsia.<api-namespace>
、
fuchsia.<api-namespace>.<name>
或 fuchsia.<api-namespace>.<name>.<subname>
。
选择合适的 api-namespace
(可以在 API 委员会的帮助下完成)
成员。
例如,在平台源代码树中定义的 FIDL 库
必须属于
fuchsia.hardware
命名空间。例如,公开以太网的协议
设备可能命名为 fuchsia.hardware.ethernet.Device
。较高层面
基于这些 FIDL 协议构建的功能不属于
fuchsia.hardware
命名空间。例如,它更适合于
协议小于 fuchsia.hardware
的 fuchsia.net
。
避免嵌套过深
首选包含三个组成部分的库名称(例如 fuchsia.hardware.network
),
并避免使用包含四个以上的组件的库名称(例如,
fuchsia.apps.foo.bar.baz
)。如果您使用的组件超过四个,应该
有具体理由做出该选择
库依赖项
最好从具有更具体的库的依赖项引入
更改为名称较少的库例如:fuchsia.foo.bar
可能依赖于 fuchsia.foo
,但 fuchsia.foo
不应依赖于
fuchsia.foo.bar
。这种模式更适合可扩展性,因为随着时间的推移,
可以添加更多具有更具体的名称的库,但只有一个有限的库
名称不太具体的库的数量。
可查看导入库
用于扩展将 FIDL 声明分组到 FIDL 的第二个目标
库,我们希望改进 FIDL,以便提供可见性规则,
元素可通过导入库(“子库”)来使用,例如public
或
private
修饰符。
应特殊处理 internal
库组件名称,
表示可见性规则的局部限制。例如,公共
fuchsia.net.dhcp.internal.foo
库中的声明可能
与其父级 fuchsia.net.dhcp
或其同级(例如,
fuchsia.net.dhcp.internal.bar
。
使用多字词库组件
库名称中包含连接多个单词的组件(例如,
fuchsia.modular.storymodel
),则它们应该非常特别。
如果存在以下情况,库作者可能会将多个单词组合在一起
名称会违反嵌套规则,或者两个词都不应优先于
另一个是分层考虑库的位置。
版本字符串
如果需要对库进行版本控制,则应使用单个版本号
后缀,例如fuchsia.io2
或 fuchsia.something.something4.
版本号
不应分为多部分,例如不接受 fuchsia.io2.1
,
改为 fuchsia.io3
。任何库组件都可能带有版本编号
强烈建议不要拥有多个版本化组件,例如
fuchsia.hardware.cpu2.ctrl
,但不包括 fuchsia.hardware.cpu2.ctrl4
。
版本号应仅表示库的较新版本,
完全不同的网域。反过来,fuchsia.input
库
用于较低级别的设备处理,而 fuchsia.ui.input{2,3}
用于与风景优美的模式和软件组件交互的输入,
呈现界面的方法如果只关注版本控制,则如同
fuchsia.ui.scenic.input
和 fuchsia.ui.scenic.input2
,以便与
fuchsia.input
提供服务的另一个网域。
类型
正如“一般建议”部分所提到的您应特别注意 协议定义中所使用的协议类型。
保持一致
为同一概念使用一致的类型。例如,使用 uint32
或
int32
。如果您
为概念创建 struct
,使用该结构体
代表概念。
理想情况下,类型也会跨库边界一致地使用。
检查相关库是否存在类似的概念,并与这些概念保持一致
库。如果库之间共享许多概念,请考虑
将这些概念的类型定义分解到通用库中。对于
例如,fuchsia.mem
和 fuchsia.math
包含许多常用类型,
分别代表记忆和数学概念。
首选语义类型
创建结构体来命名常用概念,即使这些概念 用基元表示。例如,IPv4 地址是 概念,并且应该使用结构体来命名,甚至 可以使用一个原语来表示:
type Ipv4Address = struct {
octets array<uint8, 4>;
};
在性能关键型目标语言中,结构体以行表示, 降低使用结构体为重要概念命名的成本。
zx.Time
具有明确定义的时基
zx.Time
类型单调测量
从指定的
设备专属时基。
使用 zx.Time
可以假定采用此时基,无需拼写
。
谨慎使用匿名类型
匿名类型对于更流畅地描述 API 非常有用。具体来说, 如果您事先知道 某个命名类型的子元素本质上与该命名类型相关联,并且将 在包含 容器
例如,考虑将一些内容汇总到一起的并集变体。 联合变体单独使用极其罕见,也就是说,我们知道 联合变体仅在特定语境下才有意义 。因此,可以对联合变体使用匿名类型, 建议。
理想情况下,类型应该与 API 的主要概念一一对应,不能将两者
类型应具有相同的定义。想要同时实现这两者,并不总是
特别是在类型命名的情况下, 这会引入一个
不同概念1 - 除了用作 API 接口之外,还有其他意义
元素。请考虑使用名为 type EntityId = struct { id
uint64; };
和 type OtherEntityId = struct { id uint64; };
的实例,这些标识符分别表示
但具有相同的类型定义,只是名称不同。
使用匿名类型会创建多个类型,所有这些类型都与每种类型不兼容 其他。因此,如果有多个匿名类型用于表示相同的 否则会导致 API 过于复杂,导致无法进行通用处理 支持大部分目标语言。
因此,在使用匿名类型时,您必须避免使用多个匿名类型 都表示相同的概念例如,不得在 API 的发展可能会导致多种匿名类型 都表示相同的概念
考虑使用虚拟内存对象 (VMO)
虚拟内存对象 (VMO) 是一种内核对象,代表一组
以及逻辑大小。使用此类型进行转移
存储在 FIDL 消息中,并使用 ZX_PROP_VMO_CONTENT_SIZE
属性跟踪
对象中包含的数据量。
为矢量和字符串指定边界
所有 vector
和 string
声明都应指定长度边界。
声明通常分为两类:
- 数据存在固有限制。例如,包含
文件系统名称组件的长度不得超过
fuchsia.io.MAX_FILENAME
。 - 除了“越多越好”之外,没有其他约束条件。在这些情况下
应使用内置常量
MAX
。
每当使用 MAX
时,都请考虑消息接收者是否会
真正想要处理任意长度的序列,
序列代表滥用行为。
请注意,所有声明都隐式受到消息数上限
通过 zx::channel
发送时的长度。如果确实有一些应用场景
任意长度的序列,直接使用 MAX
可能无法解决这些用例
因为尝试提供超长序列的客户端可能会
最大消息长度。
为了应对具有任意大序列的用例,请考虑破坏 使用一种分页模式将消息排序为多条消息, 或者考虑将数据移出消息本身, 转换为 VMO
FIDL 配方:大小约束
FIDL 矢量和字符串可以带有大小限制,该约束条件可指定 该类型可以包含的成员数量。对于向量,指的是 向量中存储的元素数,而对于字符串,它是指 字符串包含的字节数。
强烈建议使用大小限制,因为它可以设置 原本将是无界限大型类型的内容
按顺序迭代是对键值存储的有用操作: 用于返回在 让它按顺序显示
推理
在 FIDL 中,最好使用迭代器来完成,迭代器通常以 可进行此迭代的单独协议。使用单独的 因此使用单独的频道具有诸多好处,包括 通过 主协议
协议 P
的通道连接的客户端和服务器端可以是
以 FIDL 数据类型表示,以 client_end:P
和 server_end:P
的形式表示,
。这些类型统称为“协议结束”,
代表另一种(非@discoverable
)将 FIDL 客户端连接到
相应的服务器:通过现有的 FIDL 连接!
协议终止是一般 FIDL 概念的具体实例:资源 type。资源类型旨在包含 FIDL 句柄,这必不可少 对类型使用方式的额外限制。类型必须始终为 唯一,因为底层资源由其他 capability Manager 调控 (通常是 Zircon 内核)。通过简单的内存中复制此类资源 不让经理参与,是不可行的。为避免此类重复, FIDL 中的所有资源类型始终只能移动。
最后,Iterator
协议本身的 Get()
方法使用
大小限制。这限制了
在单次拉取中传输,从而可以衡量资源使用情况
控制。它还创建了自然的分页边界:而不是一个巨大的转储。
所有结果,则服务器只需在
。
实现
FIDL、CML 和 Realm 接口的定义如下所示:
FIDL
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. library examples.keyvaluestore.additerator; /// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That /// is, it must start with a letter, end with a letter or number, contain only letters, numbers, /// periods, and slashes, and be between 4 and 64 characters long. type Item = struct { key string:128; value vector<byte>:64000; }; /// An enumeration of things that may go wrong when trying to write a value to our store. type WriteError = flexible enum { UNKNOWN = 0; INVALID_KEY = 1; INVALID_VALUE = 2; ALREADY_EXISTS = 3; }; /// An enumeration of things that may go wrong when trying to create an iterator. type IterateConnectionError = flexible enum { /// The starting key was not found. UNKNOWN_START_AT = 1; }; /// A key-value store which supports insertion and iteration. @discoverable open protocol Store { /// Writes an item to the store. flexible WriteItem(struct { attempt Item; }) -> () error WriteError; /// Iterates over the items in the store, using lexicographic ordering over the keys. /// /// The [`iterator`] is [pipelined][pipelining] to the server, such that the client can /// immediately send requests over the new connection. /// /// [pipelining]: https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#request-pipelining flexible Iterate(resource struct { /// If present, requests to start the iteration at this item. starting_at string:<128, optional>; /// The [`Iterator`] server endpoint. The client creates both ends of the channel and /// retains the `client_end` locally to use for pulling iteration pages, while sending the /// `server_end` off to be fulfilled by the server. iterator server_end:Iterator; }) -> () error IterateConnectionError; }; /// An iterator for the key-value store. Note that this protocol makes no guarantee of atomicity - /// the values may change between pulls from the iterator. Unlike the `Store` protocol above, this /// protocol is not `@discoverable`: it is not independently published by the component that /// implements it, but rather must have one of its two protocol ends transmitted over an existing /// FIDL connection. /// /// As is often the case with iterators, the client indicates that they are done with an instance of /// the iterator by simply closing their end of the connection. /// /// Since the iterator is associated only with the Iterate method, it is declared as closed rather /// than open. This is because changes to how iteration works are more likely to require replacing /// the Iterate method completely (which is fine because that method is flexible) rather than /// evolving the Iterator protocol. closed protocol Iterator { /// Gets the next batch of keys. /// /// The client pulls keys rather than having the server proactively push them, to implement /// [flow control][flow-control] over the messages. /// /// [flow-control]: /// https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#prefer_pull_to_push strict Get() -> (struct { /// A list of keys. If the iterator has reached the end of iteration, the list will be /// empty. The client is expected to then close the connection. entries vector<string:128>:10; }); };
CML
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/client_bin", }, use: [ { protocol: "examples.keyvaluestore.additerator.Store" }, ], config: { write_items: { type: "vector", max_count: 16, element: { type: "string", max_size: 64, }, }, // A key to iterate from, after all items in `write_items` have been written. iterate_from: { type: "string", max_size: 64, }, }, }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/server_bin", }, capabilities: [ { protocol: "examples.keyvaluestore.additerator.Store" }, ], expose: [ { protocol: "examples.keyvaluestore.additerator.Store", from: "self", }, ], }
大区
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { children: [ { name: "client", url: "#meta/client.cm", }, { name: "server", url: "#meta/server.cm", }, ], offer: [ // Route the protocol under test from the server to the client. { protocol: "examples.keyvaluestore.additerator.Store", from: "#server", to: "#client", }, // Route diagnostics support to all children. { protocol: [ "fuchsia.inspect.InspectSink", "fuchsia.logger.LogSink", ], from: "parent", to: [ "#client", "#server", ], }, ], }
然后,可以使用任何受支持的语言编写客户端和服务器实现:
Rust
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{Context as _, Error}; use config::Config; use fuchsia_component::client::connect_to_protocol; use std::{thread, time}; use fidl::endpoints::create_proxy; use fidl_examples_keyvaluestore_additerator::{Item, IteratorMarker, StoreMarker}; use futures::join; #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Load the structured config values passed to this component at startup. let config = Config::take_from_startup_handle(); // Use the Component Framework runtime to connect to the newly spun up server component. We wrap // our retained client end in a proxy object that lets us asynchronously send `Store` requests // across the channel. let store = connect_to_protocol::<StoreMarker>()?; println!("Outgoing connection enabled"); // This client's structured config has one parameter, a vector of strings. Each string is the // path to a resource file whose filename is a key and whose contents are a value. We iterate // over them and try to write each key-value pair to the remote store. for key in config.write_items.into_iter() { let path = format!("/pkg/data/{}.txt", key); let value = std::fs::read_to_string(path.clone()) .with_context(|| format!("Failed to load {path}"))?; match store.write_item(&Item { key: key, value: value.into_bytes() }).await? { Ok(_) => println!("WriteItem Success"), Err(err) => println!("WriteItem Error: {}", err.into_primitive()), } } if !config.iterate_from.is_empty() { // This helper creates a channel, and returns two protocol ends: the `client_end` is already // conveniently bound to the correct FIDL protocol, `Iterator`, while the `server_end` is // unbound and ready to be sent over the wire. let (iterator, server_end) = create_proxy::<IteratorMarker>()?; // There is no need to wait for the iterator to connect before sending the first `Get()` // request - since we already hold the `client_end` of the connection, we can start queuing // requests on it immediately. let connect_to_iterator = store.iterate(Some(config.iterate_from.as_str()), server_end); let first_get = iterator.get(); // Wait until both the connection and the first request resolve - an error in either case // triggers an immediate resolution of the combined future. let (connection, first_page) = join!(connect_to_iterator, first_get); // Handle any connection error. If this has occurred, it is impossible for the first `Get()` // call to have resolved successfully, so check this error first. if let Err(err) = connection.context("Could not connect to Iterator")? { println!("Iterator Connection Error: {}", err.into_primitive()); } else { println!("Iterator Connection Success"); // Consecutively repeat the `Get()` request if the previous response was not empty. let mut entries = first_page.context("Could not get page from Iterator")?; while !&entries.is_empty() { for entry in entries.iter() { println!("Iterator Entry: {}", entry); } entries = iterator.get().await.context("Could not get page from Iterator")?; } } } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. thread::sleep(time::Duration::from_secs(2)); Ok(()) }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{Context as _, Error}; use fuchsia_component::server::ServiceFs; use futures::prelude::*; use lazy_static::lazy_static; use regex::Regex; use fidl_examples_keyvaluestore_additerator::{ Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest, StoreRequestStream, WriteError, }; use fuchsia_async as fasync; use std::collections::btree_map::Entry; use std::collections::BTreeMap; use std::ops::Bound::*; use std::sync::{Arc, Mutex}; lazy_static! { static ref KEY_VALIDATION_REGEX: Regex = Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile"); } /// Handler for the `WriteItem` method. fn write_item(store: &mut BTreeMap<String, Vec<u8>>, attempt: Item) -> Result<(), WriteError> { // Validate the key. if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) { println!("Write error: INVALID_KEY, For key: {}", attempt.key); return Err(WriteError::InvalidKey); } // Validate the value. if attempt.value.is_empty() { println!("Write error: INVALID_VALUE, For key: {}", attempt.key); return Err(WriteError::InvalidValue); } // Write to the store, validating that the key did not already exist. match store.entry(attempt.key) { Entry::Occupied(entry) => { println!("Write error: ALREADY_EXISTS, For key: {}", entry.key()); Err(WriteError::AlreadyExists) } Entry::Vacant(entry) => { println!("Wrote value at key: {}", entry.key()); entry.insert(attempt.value); Ok(()) } } } /// Handler for the `Iterate` method, which deals with validating that the requested start position /// exists, and then sets up the asynchronous side channel for the actual iteration to occur over. fn iterate( store: Arc<Mutex<BTreeMap<String, Vec<u8>>>>, starting_at: Option<String>, stream: IteratorRequestStream, ) -> Result<(), IterateConnectionError> { // Validate that the starting key, if supplied, actually exists. if let Some(start_key) = starting_at.clone() { if !store.lock().unwrap().contains_key(&start_key) { return Err(IterateConnectionError::UnknownStartAt); } } // Spawn a detached task. This allows the method call to return while the iteration continues in // a separate, unawaited task. fasync::Task::spawn(async move { // Serve the iteration requests. Note that access to the underlying store is behind a // contended `Mutex`, meaning that the iteration is not atomic: page contents could shift, // change, or disappear entirely between `Get()` requests. stream .map(|result| result.context("failed request")) .try_fold( match starting_at { Some(start_key) => Included(start_key), None => Unbounded, }, |mut lower_bound, request| async { match request { IteratorRequest::Get { responder } => { println!("Iterator page request received"); // The `page_size` should be kept in sync with the size constraint on // the iterator's response, as defined in the FIDL protocol. static PAGE_SIZE: usize = 10; // An iterator, beginning at `lower_bound` and tracking the pagination's // progress through iteration as each page is pulled by a client-sent // `Get()` request. let held_store = store.lock().unwrap(); let mut entries = held_store.range((lower_bound.clone(), Unbounded)); let mut current_page = vec![]; for _ in 0..PAGE_SIZE { match entries.next() { Some(entry) => { current_page.push(entry.0.clone()); } None => break, } } // Update the `lower_bound` - either inclusive of the next item in the // iteration, or exclusive of the last seen item if the iteration has // finished. This `lower_bound` will be passed to the next request // handler as its starting point. lower_bound = match entries.next() { Some(next) => Included(next.0.clone()), None => match current_page.last() { Some(tail) => Excluded(tail.clone()), None => lower_bound, }, }; // Send the page. At the end of this scope, the `held_store` lock gets // dropped, and therefore released. responder.send(¤t_page).context("error sending reply")?; println!("Iterator page sent"); } } Ok(lower_bound) }, ) .await .ok(); }) .detach(); Ok(()) } /// Creates a new instance of the server. Each server has its own bespoke, per-connection instance /// of the key-value store. async fn run_server(stream: StoreRequestStream) -> Result<(), Error> { // Create a new in-memory key-value store. The store will live for the lifetime of the // connection between the server and this particular client. // // Note that we now use an `Arc<Mutex<BTreeMap>>`, replacing the previous `RefCell<HashMap>`. // The `BTreeMap` is used because we want an ordered map, to better facilitate iteration. The // `Arc<Mutex<...>>` is used because there are now multiple async tasks accessing the: one main // task which handles communication over the protocol, and one additional task per iterator // protocol. `Arc<Mutex<...>>` is the simplest way to synchronize concurrent access between // these racing tasks. let store = &Arc::new(Mutex::new(BTreeMap::<String, Vec<u8>>::new())); // Serve all requests on the protocol sequentially - a new request is not handled until its // predecessor has been processed. stream .map(|result| result.context("failed request")) .try_for_each(|request| async { // Match based on the method being invoked. match request { StoreRequest::WriteItem { attempt, responder } => { println!("WriteItem request received"); // The `responder` parameter is a special struct that manages the outgoing reply // to this method call. Calling `send` on the responder exactly once will send // the reply. responder .send(write_item(&mut store.clone().lock().unwrap(), attempt)) .context("error sending reply")?; println!("WriteItem response sent"); } StoreRequest::Iterate { starting_at, iterator, responder } => { println!("Iterate request received"); // The `iterate` handler does a quick check to see that the request is valid, // then spins up a separate worker task to serve the newly minted `Iterator` // protocol instance, allowing this call to return immediately and continue the // request stream with other work. responder .send(iterate(store.clone(), starting_at, iterator.into_stream()?)) .context("error sending reply")?; println!("Iterate response sent"); } // StoreRequest::_UnknownMethod { ordinal, .. } => { println!("Received an unknown method with ordinal {ordinal}"); } } Ok(()) }) .await } // A helper enum that allows us to treat a `Store` service instance as a value. enum IncomingService { Store(StoreRequestStream), } #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Add a discoverable instance of our `Store` protocol - this will allow the client to see the // server and connect to it. let mut fs = ServiceFs::new_local(); fs.dir("svc").add_fidl_service(IncomingService::Store); fs.take_and_serve_directory_handle()?; println!("Listening for incoming connections"); // The maximum number of concurrent clients that may be served by this process. const MAX_CONCURRENT: usize = 10; // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit. fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| { run_server(stream).unwrap_or_else(|e| println!("{:?}", e)) }) .await; Ok(()) }
C++(自然)
客户端
// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.
服务器
// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.
C++(有线)
客户端
// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.
服务器
// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.
HLCPP
客户端
// TODO(https://fxbug.dev/42060656): HLCPP implementation.
服务器
// TODO(https://fxbug.dev/42060656): HLCPP implementation.
字符串编码、字符串内容和长度边界
FIDL string
采用 UTF-8 编码,
使用 1、2、3 或 4 个字节的
Unicode 代码点。
绑定会对字符串强制执行有效的 UTF-8,因此字符串不会 适合任意二进制数据。请参阅 我应该使用字符串还是矢量?。
由于长度限定声明的目的是提供
FIDL 消息总字节大小的可计算上限,string
边界
指定字段中的最大字节数。为了安全起见,您
通常需要 (4 bytes · code points in
string)
的预算。(如果您确定文本中仅使用了代码
表示单字节 ASCII 范围内的值,例如电话号码或信用卡
每个码位 1 个字节就足够了。)
一个字符串中有多少个码位?这个问题可能会比较复杂, 尤其是针对用户生成的字符串内容, Unicode 代码点和 用户可能会把它视为“字符”。
例如,字符串
á
呈现为单个用户感知的“字符”,但实际上 码位:
1. LATIN SMALL LETTER A (U+0061)
2. COMBINING ACUTE ACCENT (U+0301)
在 Unicode 术语中,这种用户感知的“字符”称为 字素集群。
单个字素集群可以由任意多个代码点组成。考虑 下面这个较长的示例:
á🇨🇦b👮🏽♀️
如果您的系统和字体支持它,您应该会看到四个字形聚类。 上方:
1. 'a' with acute accent
2. emoji of Canadian flag
3. 'b'
4. emoji of a female police officer with a medium skin tone
这四个字素集群编码为 10 个码位:
1. LATIN SMALL LETTER A (U+0061)
2. COMBINING ACUTE ACCENT (U+0301)
3. REGIONAL INDICATOR SYMBOL LETTER C (U+1F1E8)
4. REGIONAL INDICATOR SYMBOL LETTER A (U+1F1E6)
5. LATIN SMALL LETTER B (U+0062)
6. POLICE OFFICER (U+1F46E)
7. EMOJI MODIFIER FITZPATRICK TYPE-4 (U+1F3FD)
8. ZERO WIDTH JOINER (U+200D)
9. FEMALE SIGN (U+2640)
10. VARIATION SELECTOR-16 (U+FE0F)
采用 UTF-8 编码时,该字符串占 28 个字节。
从此示例中,您应该很清楚,如果应用的界面显示了
一个文本输入框,允许输入 N 个任意字形聚类(即用户所说的
“字符”),并且您计划将这些用户输入的字符串
FIDL,则必须在4·N
FIDL string
字段。
这个倍数应该是多少?这取决于您的数据。如果您正在处理 相当受限的用例(例如人名、邮政地址、信用卡) 数字),您可以假设每个字形聚类有 1-2 个码位。如果 你正在构建一个聊天客户端,其中表情符号的使用非常普遍,每个 字形集群可能更安全。在任何情况下,您的输入验证界面都应 显示清晰的视觉反馈,以便用户在用完存储空间后不会感到惊讶 房间。
整数类型
选择适合您的应用场景的整数类型,并确保
。如果您最好将值视为数据字节,请使用 byte
。
如果负值没有任何意义,请使用无符号类型。一般来说,如果
对于数量较少的情况,请使用 32 位值,
大型语言模型
如果可能存在更多状态,请避免使用布尔值
添加布尔值字段时,如果该字段可以
扩展以表示将来的其他状态。例如,一个布尔值
is_gif
字段可以更好地表示为
type FileType = strict enum {
UNKNOWN = 0;
GIF = 1;
};
然后,如果需要,可以使用 JPEG = 2
扩展枚举。
如何表示错误?
请为您的用例选择合适的错误类型,并保持一致的 您报告错误。
使用 error
语法清晰地记录和表达
并利用量身定制的目标语言
绑定。
(使用出错的可选值 enum 模式已弃用。)
使用错误语法
方法可以带有可选的 error <type>
说明符来指明它们
返回值,或者出错并生成 <type>
。示例如下:
// Only erroneous statuses are listed
type MyErrorCode = strict enum {
MISSING_FOO = 1; // avoid using 0
NO_BAR = 2;
};
protocol Frobinator {
Frobinate() -> (struct {
value SuccessValue;
}) error MyErrorCode;
};
使用此模式时,您可以使用 int32
、uint32
或枚举
用于表示返回的错误类型。在大多数情况下,返回
enum 是首选方法。
协议的所有方法最好使用一个错误类型。
首选网域专用枚举
定义和控制网域时,请使用专门构建的枚举错误类型。对于
例如,在协议是专门构建时定义一个枚举,并传递
错误的语义设计是唯一的设计约束条件。如
enum 部分,则最好避免使用值 0
。
在某些情况下,先使用空的灵活枚举是合适的做法:
type MyEmptyErrorCode = flexible enum {};
protocol Frobinator2 {
Frobinate() -> (struct {
value SuccessValue;
}) error MyEmptyErrorCode;
BadFrobinate() -> (struct {
value SuccessValue;
}) error flexible enum {}; // avoid anonymous enum
};
灵活枚举具有默认的未知成员。一个 因此,空的灵活枚举是类型化占位符,可提供 可进化性。使用此模式时,建议定义一个独立的 一个协议(或一个库中)多个方法可以重复使用该类型,而 与匿名枚举相比。使用匿名枚举创建 多种类型,而所有类型都彼此不兼容,这会导致过度 并防止对大多数目标语言中的错误进行常规处理。
如果您遵循明确定义的 规范(例如 HTTP 错误代码),并且该枚举旨在满足人体工学要求 来表示规范所规定的原始值。
特别是,对于与内核对象或zx.Status
IO。例如,fuchsia.process
会使用 zx.Status
,因为库是
主要涉及操控内核对象。再举一个例子
fuchsia.io
广泛使用 zx.Status
,因为该库关注的是
IO。
将可选值与错误枚举搭配使用
过去,使用 两个返回值、一个可选值和一个错误代码。例如:
type MyStatusCode = strict enum {
OK = 0; // The success value should be 0,
MISSING_FOO = 1; // with erroneous status next.
NO_BAR = 2;
};
protocol Frobinator3 {
Frobinate() -> (struct {
value box<SuccessValue>;
err MyStatusCode;
});
};
不过,此模式现已弃用,取而代之的是错误 语法:在分析模型中之前一直存在的性能优势, 在信封中内嵌较小的值已废弃;低级别 如今普遍支持工会。
避免错误消息和说明出现在错误中
在一些不寻常的情况下,协议可能会包含
错误以及 status
或枚举值(如果可能的错误范围)
说明性错误消息可能对
客户。不过,添加字符串会带来一些困难。例如:
客户可能会尝试解析该字符串以了解所发生的情况,这意味着
字符串的确切格式会成为协议的一部分,
尤其是在字符串
经过本地化。
安全注意事项: 同样,您也可以向客户端报告堆栈轨迹或异常消息, 无意间泄露机密信息。
将字符串和错误消息本地化
如果您要构建一项充当界面后端的服务,请使用结构化 并将渲染工作留给界面层完成。
如果您的所有消息都简单且未参数化,请使用 enum
处理错误
报告和常规界面字符串。要查看更详细的消息,可使用参数
例如姓名、数字和位置,请使用 table
或 xunion
,并将
以字符串或数字字段的形式传递参数。
您可能很想在服务中生成英文消息并提供 将它们作为字符串发送到界面 - 界面只会接收一个字符串,并弹出一个 通知或错误对话框。
不过,这种比较简单的方法也存在一些严重缺点:
- 您的服务是否知道当前使用的语言区域(语言和区域) 界面?您必须在每个请求中传递语言区域(请参阅 example),或跟踪每个已连接 以便以正确的语言提供消息。
- 服务的开发环境是否很好地支持
本地化?如果您使用 C++ 编写代码,则可以轻松访问
ICU 库和
MessageFormat
,但如果你使用 Rust,库支持 更加有限。 - 您的任何错误消息中是否需要包含已知 而不是服务呢?
- 您的服务是否仅提供单个界面实现?此服务是否提供 您知道界面有多大的空间来显示消息?
- 错误是否仅显示为文字?您可能还需要特定错误的提醒图标, 音效或文字转语音提示。
- 用户能否在界面仍在运行时更改显示语言区域?如果 发生这种情况时,可能很难将预本地化的字符串更新为新的 语言区域,尤其是当它们是某些非幂等操作的结果时。
除非您要构建的是高度专业化、 单个界面实现,则您不应公开用户可见的界面 字符串。
是否应定义一个结构体来封装方法参数(或响应)?
无论何时定义方法,您都需要决定是否传递形参 也可以将参数封装在结构体中。制作精品 在选择过程中需要权衡多种因素。请考虑以下问题 指导您做出决策:
是否存在有意义的封装边界?如果一组参数 因为除了这个部分之外,它们之间还有一定的连贯性, 方法,您可能需要将这些参数封装在一个结构体中。 (希望您在 因为您遵循了“一般建议” 并在早期重点介绍类型。)
除了所调用的方法之外,结构体是否还有其他用途?如果 请考虑单独传递参数。
您是否在许多方法中重复了相同的一组参数?如果是, 不妨考虑将这些参数归入一个或多个结构。您可能会 还要考虑重复的参数是否表明这些参数 因为它们代表了协议中的某个重要概念。
是否有大量参数可选或不是其他参数? 通常会被指定为默认价值?如果是这样,请考虑使用结构体来减少 样板文件。
是否有一组参数始终为 null 或非 null 呢?如果是这样,请考虑将这些参数分组为可为 null 的结构体, 在协议本身中强制实施这种不变性。例如, 上面定义的
FrobinateResult
结构体包含始终为 null 的值 在error
不是MyError.OK
时同步。
我应该使用 string
还是 vector<byte>
?
在 FIDL 中,string
数据必须是有效的 UTF-8,这意味着字符串可以表示
Unicode 码位序列,但不能表示任意二进制数据。在
相反,vector<byte>
或 array<byte, N>
可表示任意二进制文件
数据,且不要暗示 Unicode。
对于文本数据,请使用 string
:
使用
string
表示软件包名称,因为软件包名称必须 是有效的 UTF-8 字符串(包含某些排除的字符)。使用
string
表示软件包中的文件名,因为文件名 必须为有效的 UTF-8 字符串(某些 字符)。使用
string
表示媒体编解码器名称,因为媒体编解码器名称 从有效的 UTF-8 字符串固定词汇表中选择。使用
string
表示 HTTP 方法,因为 HTTP 方法由 一组固定的字符,这些字符始终有效的 UTF-8。
对于小型非文本数据,请使用 vector<byte>
或 array<byte, N>
:
为 HTTP 标头字段使用
vector<byte>
,因为 HTTP 标头字段不能 指定编码,因此不一定能以 UTF-8 表示。请使用
array<byte, 6>
作为 MAC 地址,因为 MAC 地址是二进制数据。对 UUID 使用
array<byte, 16>
,因为 UUID(几乎!)是任意二进制文件 数据。
对 blob 使用共享内存基元:
- 对于图片和(大型)protobuf,请使用
zx.Handle:VMO
完全缓冲数据。 - 使用
zx.Handle:SOCKET
进行音频和视频流,因为数据可能会通过 或者需要在将数据完全写入或运行时 可用。
我应该使用 vector
还是 array
?
vector
是一个可变长度序列,在
线上传输格式。array
是内嵌表示的固定长度序列
采用线上传输格式
对于长度可变的数据,请使用 vector
:
- 为日志消息中的标记使用
vector
,因为日志消息可以 0 个和 5 个标签
对于固定长度的数据,请使用 array
:
- 由于 MAC 地址的长度始终为 6 个字节,因此请使用
array
作为 MAC 地址。
我应该使用 struct
还是 table
?
结构体和表都表示具有多个命名字段的对象。通过 不同之处在于,结构体在传输格式中具有固定布局, 因此无法在不破坏二进制文件兼容性的情况下对其进行修改。相比之下 表格采用线上布局灵活布局,这意味着相关字段可以 会不断添加到表中,而不会破坏二进制文件的兼容性。
对性能关键型协议元素或协议元素使用结构体 并且将来不太可能发生变化例如,使用结构体 表示 MAC 地址,因为 MAC 地址的结构不太可能发生 以便在未来进行更改
使用表格列出将来可能会更改的协议元素。对于 例如,使用表来表示有关摄像头设备的元数据信息 因为元数据中的字段可能会随时间而变化。
如何表示常量?
表示常量有三种方式,具体取决于 常量:
- 对于特殊值,例如 PI 或 MAX_NAME_LEN,请使用
const
。 - 当值是集合的元素时,使用
enum
,例如重复 媒体播放器的播放模式:OFF、SINGLE_TRACK 或 ALL_TRACKS。 - 为构成一组标志(例如 capability)的常量使用
bits
WLAN、SYNTH 和 LOOPBACK。
const
如果您想以符号方式使用某个值,请使用 const
而无需每次都输入值典型示例是 PI,
通常编码为 const
,因为不需要输入文字会很方便,
3.141592653589
。
此外,当值可能会更改,但需要更改时,您可以使用 const
否则在整个过程中始终会用到输入的字符数的
就是一个很好的示例(例如,MAX_NAME_LEN)。修改者
使用 const
,您可以将该数字的定义集中到一起,因此无需
会在所有代码中呈现不同的值
选择 const
的另一个原因是,您可以同时使用它来约束
消息,稍后在代码中进行。例如:
const MAX_BATCH_SIZE int32 = 128;
protocol Sender {
Emit(struct {
batch vector<uint8>:MAX_BATCH_SIZE;
});
};
然后,您可以在代码中使用常量 MAX_BATCH_SIZE
来组建批次。
枚举
如果枚举值集受
Fuchsia 项目的定义。例如,Fucsia 项目定义了指针事件,
输入模型,从而控制 PointerEventPhase
枚举的值。
在某些情况下,即使 Fuchsia 项目本身也应使用枚举 不会控制枚举值的集合,前提是我们可以合理地预期 希望注册新值的用户会将补丁提交到 Fuchsia 源代码树来注册它们的值。例如,纹理格式需要 这也意味着新的纹理格式 由开发这些驱动程序的开发者添加,即使纹理集 格式由图形硬件供应商控制。反过来, 请勿使用枚举来表示 HTTP 方法,因为我们无法合理地预期 使用全新 HTTP 方法将补丁提交到平台源代码树的用户。
对于先验无界限集,如果存在以下情况,string
可能是更合适的选择
且您希望动态扩展该集合。例如,使用 string
用于表示媒体编解码器名称
命名一个合理名称。
如果枚举值集由外部实体控制,请使用
整数(适当大小)或 string
。例如,使用一个整数 (
表示 USB HID 标识符,因为 USB HID 集合
由行业联盟进行控制。同样,使用 string
表示 MIME 类型,因为 MIME 类型是受控的(至少在理论上)
由 IANA 注册库管理。
我们建议开发者尽可能避免将 0
用作枚举值。
由于许多目标语言都使用 0
作为整数的默认值,因此
难以区分 0
值是有意设置的,还是
因为这是默认设置。例如,
fuchsia.module.StoryState
定义了三个值:值为 1
的 RUNNING
、
STOPPING
(值为 2
)和 STOPPED
(值为 3
)。
在以下两种情况下,适合使用值 0
:
- 枚举具有自然的默认、初始或未知状态;
- 该枚举定义了 包含错误枚举的可选值 模式。
位
如果您的协议有位字段,请使用 bits
值表示其值(对于
详情请参阅 RFC-0025
:“Bit Flags”)。
例如:
type InfoFeaturesHex = strict bits : uint32 {
WLAN = 0x00000001; // If present, this device represents WLAN hardware
SYNTH = 0x00000002; // If present, this device is synthetic (not backed by h/w)
LOOPBACK = 0x00000004; // If present, this device receives all messages it sends
};
这表示 InfoFeatures
位字段由无符号 32 位支持
整数,然后定义所使用的三个位。
您还可以使用 0b
以二进制(而不是十六进制)形式表示值
表示法:
type InfoFeaturesBits = strict bits : uint32 {
WLAN = 0b00000001; // If present, this device represents WLAN hardware
SYNTH = 0b00000010; // If present, this device is synthetic (not backed by h/w)
LOOPBACK = 0b00000100; // If present, this device receives all messages it sends
};
这与上一个示例相同。
我应该使用 resource
吗?
FIDL 编译器将强制对已包含
resource
带有相应标记。
如果 flexible
类型不包含资源,但将来可能会包含,
应预先添加 resource
修饰符,以避免
转换状态这种情况很少见,但经验表明,大多数情况下
消息不包含资源,且在协议中传递资源需要
关注和提前规划
我应该对类型使用 strict
还是 flexible
?
将类型标记为 flexible
可以处理数据
当前 FIDL 架构未知,建议用于
日后添加或移除成员(例如配置、元数据或错误)。它
始终可在 strict
之间软转换
和 flexible
(如果是现有类型)。
时间是 时尚 在类型允许的情况下始终指定此修饰符。Fuchsia 项目 会通过 linter 检查来强制实施此样式
使用 strict
或 flexible
不会对性能产生显著影响。
处理权利
本部分介绍了在 FIDL 中的句柄。
请参阅 FIDL 绑定规范 或 RFC-0028 了解详情 有关如何在绑定中使用权限的详细信息。
如需了解 zircon 权限的定义,请参阅内核权限。 FIDL 使用 rights.fidl 来解决权利限制问题。
始终指定标识名的权限
所有标识名都应指定您希望明确传达应用意图的权利 。这一要求迫使我们预先决定要 而不是根据观察到的行为进行预测。拥有明确权利 也有助于提升 API Surface 的可审核性。
使用收件人所需的最低权限
在确定要提供哪些权利时,优先选择最小程度,即最小程度
实现上述功能所需的权利数量。例如,如果
已知只需要 zx.Rights.READ
和 zx.Rights.WRITE
,
则只需指定这两项权利
请勿根据推测需求添加权利。如果需要添加权利 以后就可以从数据源开始添加, 一直到最后一个使用点为止。
谨慎使用 zx.Rights.SAME_RIGHTS
zx.Rights.SAME_RIGHTS
非常适合用于转发
权利未知,但在大多数情况下,应使用一组特定的权利。
。这样做的部分动机是 zx.Rights.SAME_RIGHTS
告诉
来跳过权限检查,所以它会禁用
提供的标识名。此外,zx.Rights.SAME_RIGHTS
还拥有
也就是说,某个程序获得的权利可能会
所需的资源
值得注意的是,zx.Rights.SAME_RIGHTS
不同于默认值
为某种类型(例如zx.DEFAULT_CHANNEL_RIGHTS
。而前者则跳过
而后一种方法需要针对给定对象类型的所有正常权利
存在。
良好的设计模式
本部分介绍了许多 FIDL 中反复出现的几种良好的设计模式 协议
我应该对方法和事件使用 strict
或 flexible
吗?
将方法或事件标记为 flexible
后,
当不同的组件可能出现时,移除相应方法或事件
以不同的版本构建而成,以至于有些组件认为
方法或事件,而与之通信的其他方法或事件则不存在。因为
通常希望能够灵活适应不断演变的协议,因此建议
除非有充分的理由,否则对于方法和事件,请选择 flexible
strict
。
将方法设为 flexible
对单向方法或事件没有开销。对于
双向方法,选择 flexible
会增加少量开销(16 个字节或
并且可能需要额外增加一些时间
解码。总体而言,打造灵活双向方法的成本应该很小
几乎不需要考虑任何应用场景。
只应在方法和事件strict
对
表示协议的正确行为,即协议上缺少该方法或事件,
双方之间的所有通信都足够严肃
并关闭连接。
这在针对前馈广告进行设计时尤其有用 Dataflow。考虑此记录器协议,它支持 安全处理包含个人身份信息 (PII) 的日志。它使用 使用前馈模式添加记录,以便客户端可以发起 而无需等待往返时间, 末尾的待处理操作
open protocol Logger {
flexible AddRecord(struct {
message string;
});
strict EnablePIIMode();
flexible DisablePIIMode();
flexible Flush() -> ();
};
除 EnablePIIMode
外,此处的所有方法均为 flexible
;考虑一下
如果服务器无法识别其中任何一种方法,则会发生以下情况:
AddRecord
:服务器只是未能将数据添加到日志输出。通过 发送应用将正常运行,只是其日志记录变少了 实用。这很麻烦,但也很安全。EnablePIIMode
:服务器无法启用 PII 模式,这意味着该模式可能会失败 采取安全预防措施并泄露个人身份信息。这是一个非常严重的问题 如果服务器无法识别此方法,最好关闭通道。DisablePIIMode
:服务器为 不需要 PII 日志记录的消息。这可能会给某人带来不便 但这对系统来说是安全的Flush
:服务器未能按照请求刷新记录, 这样可能会给您带来不便,但仍然安全。
要将此协议设计为具有充分灵活性,另一种方法是
将 EnablePIIMode
设为双向方法 (flexible EnablePIIMode() -> ();
),
客户端可以确定服务器是否没有 方法。请注意
从而为客户创造了更大的灵活性如此设置,客户端
可以选择是否对无法识别的服务器进行响应
EnablePIIMode
:关闭连接或直接选择不记录个人身份信息,
而使用 strict
时,该协议始终会自动关闭。不过,
会中断前馈流。
请注意,严格程度取决于发件人。假设您有一些
版本 1 中的 strict A();
方法,然后在版本 1 中将其更改为 flexible A();
然后在版本 3 中将其删除。如果版本 1 构建的客户端尝试调用
A()
在版本为 3
的服务器上,该方法将被视为严格方法,
因为版本 1 的客户端认为该方法严格,而位于
版本 3 采纳客户端的意见,因为版本 3 无法识别该方法
时间是 时尚 来始终指定此修饰符。Fuchsia 项目通过 linter 检查。
我应该使用 open
、ajar
还是 closed
?
将协议标记为 open
后,
当不同的组件可能出现时,更容易处理移除方法或事件
具有不同版本,因此每个组件都有不同的
显示存在哪些方法和事件的视图因为不断发展的灵活性
但建议选择 open
除非有理由选择更封闭的协议。
应根据以下预期约束条件来决定使用 ajar
或 closed
:
协议的演变使用 closed
或 ajar
不会阻止协议
但这确实需要更长的发布期
其中存在方法和事件,但不使用它们以确保所有客户端
和服务器商定存在哪些方法。灵活使用 flexible
适用于添加和移除方法和事件,具体取决于
先更新客户端或服务器
对于使用前馈神经网络的协议,ajar
可能会很有用
Dataflow,但预计会得到进一步发展
仅限单向方法。例如,这可能适用于拆解
协议来代表事务,其中
方法是一项必须严格遵循的提交操作,
可能会发生变化。
ajar protocol BazTransaction {
flexible Add(resource struct {
bars vector<client_end:Bar>;
});
strict Commit() -> (struct {
args Args;
});
};
open protocol Foo3 {
flexible StartBazTransaction(resource struct {
transaction server_end:BazTransaction;
});
};
对于任何未知方法都是严重问题的关键协议而言,closed
非常有用
这应该会导致频道关闭而不是继续
状态可能不佳将它用于
或者至少可能涉及
还是很长的发布周期,因此
更改 strict
方法的预期已在发布周期内推出。
协议请求管道
一种最好、使用最广泛的设计模式是协议请求 流水线。不会返回支持某个协议的渠道, 客户端发送该通道并请求服务器绑定 信道的协议:
// GOOD:
protocol GoodFoo {
GetBar(resource struct {
name string;
bar server_end:Bar;
});
};
// BAD:
protocol BadFoo {
GetBar(struct {
name string;
}) -> (resource struct {
bar client_end:Bar;
});
};
此模式很有用,因为客户端不需要等待往返通信
然后才能开始使用 Bar
协议。相反,客户端可以将
Bar
条消息。内核会缓冲这些消息
并在 Bar
的实现绑定到协议后最终进行处理
请求。相比之下,如果服务器返回 Bar
协议的实例,
在将消息排入队列之前,客户端需要等待整个往返。
Bar
。
如果请求可能会失败,请考虑使用回复来扩展此模式 指示操作是否成功:
protocol CodecProvider {
TryToCreateCodec(resource struct {
params CodecParams;
codec server_end:Codec;
}) -> (struct {
succeed bool;
});
};
为了处理失败的情况,客户端会等待响应, 操作。另一种方法是让协议 事件:
protocol Codec2 {
-> OnReady();
};
protocol CodecProvider2 {
TryToCreateCodec(resource struct {
params CodecParams;
codec server_end:Codec2;
});
};
为了处理失败情况,客户端会等待 OnReady
事件,然后获取
如果 Codec2
频道在事件到来之前关闭,则会执行一些其他操作。
不过,如果请求很有可能成功,就会获得 信号可能有害,因为该信号可让客户端区分 不同故障模式之间的变化,通常应以相同的方式进行处理。 例如,客户端应该处理 建立与在区域内无法访问的服务相同的方式 排名第一在这两种情况下,服务都不可用,而客户端 应该生成错误或寻找其他方法来完成其任务。
流控制
内核会缓冲 FIDL 消息。如果一个端点产生 则消息会累积在 占用内存并使系统更难恢复。 相反,精心设计的协议应限制消息的生成, 与这些消息的使用速率保持一致,这个属性称为“流” 控制。
流控制是一个广泛而复杂的主题, 设计模式。本部分介绍了一些 模式,但并不详尽。模式按降序排列 偏好设置。如果其中某种模式对某个特定应用场景很有效, 。但如果不是,协议可随意使用替代流控制 其他机制。
更喜欢拉推
未经仔细设计,服务器将数据推送到客户端的协议 流量控制通常较差。提供更好的流程控制的一种方法是 让客户端从服务器中提取一个或多个数据范围拉取模型 因为客户端会自然地限制 服务器会生成数据,并避免从服务器推送的消息 服务器。
使用挂起获取延迟响应
实现基于拉取的协议的一种简单方法是“驻留回调”替换为 (使用挂起 get 模式)的服务器:
protocol FooProvider {
WatchFoo(struct {
args Args;
}) -> (resource struct {
foo client_end:Foo;
});
};
在此模式中,客户端发送 WatchFoo
消息,但服务器不会
回复,直到有新信息可以发送给客户。客户端使用
foo
并立即发送另一个挂起 get。客户端和服务器各自
每个数据项一个工作单元,这意味着它们不会先于另一个。
在传输一组数据项时,挂起的获取模式可以正常运行 大小是有限制的,服务器端状态很简单,但不太奏效 在客户端和服务器需要同步其工作的情况下使用。
例如,服务器可能会针对某些可变的
使用“dirty”来表示 foo
位。它会将此位初始化为
true,请在每次 WatchFoo
响应中清除该值,并在每次更改 foo
时设置该值。
只有在设置了脏位时,服务器才会响应 WatchFoo
消息。
使用确认限制推送
在采用推送的协议中提供流控制的一种方法是 确认模式,其中调用方提供确认 响应。例如,假设 通用监听器协议:
protocol Listener {
OnBar(struct {
args Args;
}) -> ();
};
监听器应立即发送空响应消息,
收到 OnBar
消息。该响应不会向
调用方。相反,响应可让调用方观察
被调用方使用消息。调用方应限制
生成消息的速率与被调用方接收消息的速率一致。对于
例如,调用方可能只安排一条(或固定数量的)消息
待执行(即等待确认)。
FIDL 配方:确认模式
确认模式是对方法进行流控制的一种简单方法 采用单向调用的方式不要将该方法保留为一个 而是会变成双向通话,并且没有响应, 俗称为确认。确认信息之所以出现的唯一原因是 表示已经收到邮件的发件人,发件人可以使用此信息 决定如何继续操作。
这种确认的成本会增加到通道上。此模式 也可能导致性能下降 在继续进行下一个调用之前确认。
来回发送不按流量计费的单向通话会产生简单的设计, 都存在潜在的隐患:如果服务器的处理速度 该怎么办呢?例如,客户端可能会加载绘图 由某个文本文件中的数千行信息构成, 全部按顺序显示我们该如何对客户施加背压,以防止 服务器是否因这波更新而应接不暇?
使用确认模式并进行单向调用 AddLine(...);
转换为双向AddLine(...) -> ();
,我们就可以向客户提供反馈。
这样,客户端就可以酌情限制其输出。在本课中,
我们只需让客户端先等待确认,然后再发送下一个
虽然更复杂的设计可以发送
乐观地调整,并且仅在降低收到异步 ACK 的频率时才进行节流
超出预期。
首先,我们需要定义接口定义和自动化测试框架。《FIDL》 CML 和领域接口定义设置一个基架, 实现可以使用:
FIDL
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. library examples.canvas.addlinemetered; /// A point in 2D space. type Point = struct { x int64; y int64; }; /// A line in 2D space. alias Line = array<Point, 2>; /// A bounding box in 2D space. This is the result of "drawing" operations on our canvas, and what /// the server reports back to the client. These bounds are sufficient to contain all of the /// lines (inclusive) on a canvas at a given time. type BoundingBox = struct { top_left Point; bottom_right Point; }; /// Manages a single instance of a canvas. Each session of this protocol is responsible for a new /// canvas. @discoverable open protocol Instance { /// Add a line to the canvas. /// /// This method can be considered an improvement over the one-way case from a flow control /// perspective, as it is now much more difficult for a well-behaved client to "get ahead" of /// the server and overwhelm. This is because the client now waits for each request to be acked /// by the server before proceeding. This change represents a trade-off: we get much greater /// synchronization of message flow between the client and the server, at the cost of worse /// performance at the limit due to the extra wait imposed by each ack. flexible AddLine(struct { line Line; }) -> (); /// Update the client with the latest drawing state. The server makes no guarantees about how /// often this event occurs - it could occur multiple times per board state, for example. flexible -> OnDrawn(BoundingBox); };
CML
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/client_bin", }, use: [ { protocol: "examples.canvas.addlinemetered.Instance" }, ], config: { // A script for the client to follow. Entries in the script may take one of two forms: a // pair of signed-integer coordinates like "-2,15:4,5", or the string "WAIT". The former // calls `AddLine(...)`, while the latter pauses execution until the next `->OnDrawn(...)` // event is received. // // TODO(https://fxbug.dev/42178362): It would absolve individual language implementations of a great // deal of string parsing if we were able to use a vector of `union { Point; WaitEnum}` // here. script: { type: "vector", max_count: 100, element: { type: "string", max_size: 64, }, }, }, }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/server_bin", }, capabilities: [ { protocol: "examples.canvas.addlinemetered.Instance" }, ], expose: [ { protocol: "examples.canvas.addlinemetered.Instance", from: "self", }, ], }
大区
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { children: [ { name: "client", url: "#meta/client.cm", }, { name: "server", url: "#meta/server.cm", }, ], offer: [ // Route the protocol under test from the server to the client. { protocol: "examples.canvas.addlinemetered.Instance", from: "#server", to: "#client", }, // Route diagnostics support to all children. { protocol: [ "fuchsia.inspect.InspectSink", "fuchsia.logger.LogSink", ], from: "parent", to: [ "#client", "#server", ], }, ], }
然后,可以使用任何受支持的语言编写客户端和服务器实现:
Rust
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{format_err, Context as _, Error}; use config::Config; use fidl_examples_canvas_addlinemetered::{InstanceEvent, InstanceMarker, Point}; use fuchsia_component::client::connect_to_protocol; use futures::TryStreamExt; use std::{thread, time}; #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Load the structured config values passed to this component at startup. let config = Config::take_from_startup_handle(); // Use the Component Framework runtime to connect to the newly spun up server component. We wrap // our retained client end in a proxy object that lets us asynchronously send Instance requests // across the channel. let instance = connect_to_protocol::<InstanceMarker>()?; println!("Outgoing connection enabled"); for action in config.script.into_iter() { // If the next action in the script is to "WAIT", block until an OnDrawn event is received // from the server. if action == "WAIT" { let mut event_stream = instance.take_event_stream(); loop { match event_stream .try_next() .await .context("Error getting event response from proxy")? .ok_or_else(|| format_err!("Proxy sent no events"))? { InstanceEvent::OnDrawn { top_left, bottom_right } => { println!( "OnDrawn event received: top_left: {:?}, bottom_right: {:?}", top_left, bottom_right ); break; } InstanceEvent::_UnknownEvent { ordinal, .. } => { println!("Received an unknown event with ordinal {ordinal}"); } } } continue; } // If the action is not a "WAIT", we need to draw a line instead. Parse the string input, // making two points out of it. let mut points = action .split(":") .map(|point| { let integers = point .split(",") .map(|integer| integer.parse::<i64>().unwrap()) .collect::<Vec<i64>>(); Point { x: integers[0], y: integers[1] } }) .collect::<Vec<Point>>(); // Assemble a line from the two points. let from = points.pop().ok_or(format_err!("line requires 2 points, but has 0"))?; let to = points.pop().ok_or(format_err!("line requires 2 points, but has 1"))?; let line = [from, to]; // Draw a line to the canvas by calling the server, using the two points we just parsed // above as arguments. println!("AddLine request sent: {:?}", line); // By awaiting on the reply, we prevent the client from sending another request before the // server is ready to handle, thereby syncing the flow rate between the two parties over // this method. instance.add_line(&line).await.context("Error sending request")?; println!("AddLine response received"); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. thread::sleep(time::Duration::from_secs(2)); Ok(()) }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{Context as _, Error}; use fidl::endpoints::RequestStream as _; use fidl_examples_canvas_addlinemetered::{ BoundingBox, InstanceRequest, InstanceRequestStream, Point, }; use fuchsia_async::{Time, Timer}; use fuchsia_component::server::ServiceFs; use fuchsia_zircon::{self as zx}; use futures::future::join; use futures::prelude::*; use std::sync::{Arc, Mutex}; // A struct that stores the two things we care about for this example: the bounding box the lines // that have been added thus far, and bit to track whether or not there have been changes since the // last `OnDrawn` event. #[derive(Debug)] struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. changed: bool, bounding_box: BoundingBox, } impl CanvasState { /// Handler for the `AddLine` method. fn add_line(&mut self, line: [Point; 2]) { // Update the bounding box to account for the new lines we've just "added" to the canvas. let bounds = &mut self.bounding_box; for point in line { if point.x < bounds.top_left.x { bounds.top_left.x = point.x; } if point.y > bounds.top_left.y { bounds.top_left.y = point.y; } if point.x > bounds.bottom_right.x { bounds.bottom_right.x = point.x; } if point.y < bounds.bottom_right.y { bounds.bottom_right.y = point.y; } } // Mark the state as "dirty", so that an update is sent back to the client on the next tick. self.changed = true } } /// Creates a new instance of the server, paired to a single client across a zircon channel. async fn run_server(stream: InstanceRequestStream) -> Result<(), Error> { // Create a new in-memory state store for the state of the canvas. The store will live for the // lifetime of the connection between the server and this particular client. let state = Arc::new(Mutex::new(CanvasState { changed: true, bounding_box: BoundingBox { top_left: Point { x: 0, y: 0 }, bottom_right: Point { x: 0, y: 0 }, }, })); // Take ownership of the control_handle from the stream, which will allow us to push events from // a different async task. let control_handle = stream.control_handle(); // A separate watcher task periodically "draws" the canvas, and notifies the client of the new // state. We'll need a cloned reference to the canvas state to be accessible from the new // task. let state_ref = state.clone(); let update_sender = || async move { loop { // Our server sends one update per second. Timer::new(Time::after(zx::Duration::from_seconds(1))).await; let mut state = state_ref.lock().unwrap(); if !state.changed { continue; } // After acquiring the lock, this is where we would draw the actual lines. Since this is // just an example, we'll avoid doing the actual rendering, and simply send the bounding // box to the client instead. let bounds = state.bounding_box; match control_handle.send_on_drawn(&bounds.top_left, &bounds.bottom_right) { Ok(_) => println!( "OnDrawn event sent: top_left: {:?}, bottom_right: {:?}", bounds.top_left, bounds.bottom_right ), Err(_) => return, } // Reset the change tracker. state.changed = false } }; // Handle requests on the protocol sequentially - a new request is not handled until its // predecessor has been processed. let state_ref = &state; let request_handler = stream.map(|result| result.context("failed request")).try_for_each(|request| async move { // Match based on the method being invoked. match request { InstanceRequest::AddLine { line, responder } => { println!("AddLine request received: {:?}", line); state_ref.lock().unwrap().add_line(line); // Because this is now a two-way method, we must use the generated `responder` // to send an in this case empty reply back to the client. This is the mechanic // which syncs the flow rate between the client and server on this method, // thereby preventing the client from "flooding" the server with unacknowledged // work. responder.send().context("Error responding")?; println!("AddLine response sent"); } // InstanceRequest::_UnknownMethod { ordinal, .. } => { println!("Received an unknown method with ordinal {ordinal}"); } } Ok(()) }); // This await does not complete, and thus the function does not return, unless the server errors // out. The stream will await indefinitely, thereby creating a long-lived server. Here, we first // wait for the updater task to realize the connection has died, then bubble up the error. join(request_handler, update_sender()).await.0 } // A helper enum that allows us to treat a `Instance` service instance as a value. enum IncomingService { Instance(InstanceRequestStream), } #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Add a discoverable instance of our `Instance` protocol - this will allow the client to see // the server and connect to it. let mut fs = ServiceFs::new_local(); fs.dir("svc").add_fidl_service(IncomingService::Instance); fs.take_and_serve_directory_handle()?; println!("Listening for incoming connections"); // The maximum number of concurrent clients that may be served by this process. const MAX_CONCURRENT: usize = 10; // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit. fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Instance(stream)| { run_server(stream).unwrap_or_else(|e| println!("{:?}", e)) }) .await; Ok(()) }
C++(自然)
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.addlinemetered/cpp/fidl.h> #include <lib/async-loop/cpp/loop.h> #include <lib/component/incoming/cpp/protocol.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/fidl/new/canvas/add_line_metered/cpp_natural/client/config.h> // The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming // events asynchronously. class EventHandler : public fidl::AsyncEventHandler<examples_canvas_addlinemetered::Instance> { public: // Handler for |OnDrawn| events sent from the server. void OnDrawn(fidl::Event<examples_canvas_addlinemetered::Instance::OnDrawn>& event) override { auto top_left = event.top_left(); auto bottom_right = event.bottom_right(); FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x() << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x() << ", y: " << bottom_right.y() << " }"; loop_.Quit(); } void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; } void handle_unknown_event( fidl::UnknownEventMetadata<examples_canvas_addlinemetered::Instance> metadata) override { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal; } explicit EventHandler(async::Loop& loop) : loop_(loop) {} private: async::Loop& loop_; }; // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples_canvas_addlinemetered::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples_canvas_addlinemetered::Point(x, y); } // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. ::std::array<::examples_canvas_addlinemetered::Point, 2> ParseLine(const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop and dispatcher. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a // |zx::result| and it must be checked for errors. zx::result client_end = component::Connect<examples_canvas_addlinemetered::Instance>(); if (!client_end.is_ok()) { FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: " << client_end.status_string(); return -1; } // Create an instance of the event handler. EventHandler event_handler(loop); // Create an asynchronous client using the newly-established connection. fidl::Client client(std::move(*client_end), dispatcher, &event_handler); FX_LOGS(INFO) << "Outgoing connection enabled"; for (const auto& action : conf.script()) { // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); continue; } // Draw a line to the canvas by calling the server, using the two points we just parsed // above as arguments. auto line = ParseLine(action); FX_LOGS(INFO) << "AddLine request sent: [Point { x: " << line[1].x() << ", y: " << line[1].y() << " }, Point { x: " << line[0].x() << ", y: " << line[0].y() << " }]"; client->AddLine(line).ThenExactlyOnce( [&](fidl::Result<examples_canvas_addlinemetered::Instance::AddLine>& result) { // Check if the FIDL call succeeded or not. if (!result.is_ok()) { // Check that our two-way call succeeded, and handle the error appropriately. In the // case of this example, there is nothing we can do to recover here, except to log an // error and exit the program. FX_LOGS(ERROR) << "Could not send AddLine request: " << result.error_value().FormatDescription(); } FX_LOGS(INFO) << "AddLine response received"; // Quit the loop, thereby handing control back to the outer loop of actions being iterated // over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.addlinemetered/cpp/fidl.h> #include <lib/async-loop/cpp/loop.h> #include <lib/async/cpp/task.h> #include <lib/component/outgoing/cpp/outgoing_directory.h> #include <lib/fidl/cpp/wire/channel.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; examples_canvas_addlinemetered::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public fidl::Server<examples_canvas_addlinemetered::Instance> { public: // Bind this implementation to a channel. InstanceImpl(async_dispatcher_t* dispatcher, fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) : binding_(fidl::BindServer( dispatcher, std::move(server_end), this, [this](InstanceImpl* impl, fidl::UnbindInfo info, fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) { if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; })), weak_factory_(this) { // Start the update timer on startup. Our server sends one update per second ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void AddLine(AddLineRequest& request, AddLineCompleter::Sync& completer) override { auto points = request.line(); FX_LOGS(INFO) << "AddLine request received: [Point { x: " << points[1].x() << ", y: " << points[1].y() << " }, Point { x: " << points[0].x() << ", y: " << points[0].y() << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : request.line()) { if (point.x() < bounds.top_left().x()) { bounds.top_left().x() = point.x(); } if (point.y() > bounds.top_left().y()) { bounds.top_left().y() = point.y(); } if (point.x() > bounds.bottom_right().x()) { bounds.bottom_right().x() = point.x(); } if (point.y() < bounds.bottom_right().y()) { bounds.bottom_right().y() = point.y(); } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; // Because this is now a two-way method, we must use the generated |completer| to send an in // this case empty reply back to the client. This is the mechanic which syncs the flow rate // between the client and server on this method, thereby preventing the client from "flooding" // the server with unacknowledged work. completer.Reply(); FX_LOGS(INFO) << "AddLine response sent"; } void handle_unknown_method( fidl::UnknownMethodMetadata<examples_canvas_addlinemetered::Instance> metadata, fidl::UnknownMethodCompleter::Sync& completer) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one. if (!weak->state_.changed) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto result = fidl::SendEvent(binding_)->OnDrawn(state_.bounding_box); if (!result.is_ok()) { return; } auto top_left = state_.bounding_box.top_left(); auto bottom_right = state_.bounding_box.bottom_right(); FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x() << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x() << ", y: " << bottom_right.y() << " }"; // Reset the change tracker. state_.changed = false; }, after); } fidl::ServerBindingRef<examples_canvas_addlinemetered::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This // directory is where the outgoing FIDL protocols are installed so that they can be provided to // other components. component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher); // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle. // The startup handle is a handle provided to every component by the system, so that they can // serve capabilities (e.g. FIDL protocols) to other components. zx::result result = outgoing.ServeFromStartupInfo(); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string(); return -1; } // Register a handler for components trying to connect to // |examples.canvas.addlinemetered.Instance|. result = outgoing.AddUnmanagedProtocol<examples_canvas_addlinemetered::Instance>( [dispatcher](fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) { // Create an instance of our InstanceImpl that destroys itself when the connection closes. new InstanceImpl(dispatcher, std::move(server_end)); }); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string(); return -1; } // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
C++(有线)
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.addlinemetered/cpp/wire.h> #include <lib/async-loop/cpp/loop.h> #include <lib/component/incoming/cpp/protocol.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/fidl/new/canvas/add_line_metered/cpp_wire/client/config.h> // The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming // events asynchronously. class EventHandler : public fidl::WireAsyncEventHandler<examples_canvas_addlinemetered::Instance> { public: // Handler for |OnDrawn| events sent from the server. void OnDrawn(fidl::WireEvent<examples_canvas_addlinemetered::Instance::OnDrawn>* event) override { auto top_left = event->top_left; auto bottom_right = event->bottom_right; FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; loop_.Quit(); } void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; } void handle_unknown_event( fidl::UnknownEventMetadata<examples_canvas_addlinemetered::Instance> metadata) override { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal; } explicit EventHandler(async::Loop& loop) : loop_(loop) {} private: async::Loop& loop_; }; // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples_canvas_addlinemetered::wire::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples_canvas_addlinemetered::wire::Point{.x = x, .y = y}; } // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. ::fidl::Array<::examples_canvas_addlinemetered::wire::Point, 2> ParseLine( const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop and dispatcher. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a // |zx::result| and it must be checked for errors. zx::result client_end = component::Connect<examples_canvas_addlinemetered::Instance>(); if (!client_end.is_ok()) { FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: " << client_end.status_string(); return -1; } // Create an instance of the event handler. EventHandler event_handler(loop); // Create an asynchronous client using the newly-established connection. fidl::WireClient client(std::move(*client_end), dispatcher, &event_handler); FX_LOGS(INFO) << "Outgoing connection enabled"; for (const auto& action : conf.script()) { // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); continue; } // Draw a line to the canvas by calling the server, using the two points we just parsed // above as arguments. auto line = ParseLine(action); FX_LOGS(INFO) << "AddLine request sent: [Point { x: " << line[1].x << ", y: " << line[1].y << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]"; client->AddLine(line).ThenExactlyOnce( [&](fidl::WireUnownedResult<examples_canvas_addlinemetered::Instance::AddLine>& result) { // Check if the FIDL call succeeded or not. if (!result.ok()) { // Check that our two-way call succeeded, and handle the error appropriately. In the // case of this example, there is nothing we can do to recover here, except to log an // error and exit the program. FX_LOGS(ERROR) << "Could not send AddLine request: " << result.status_string(); } FX_LOGS(INFO) << "AddLine response received"; // Quit the loop, thereby handing control back to the outer loop of actions being iterated // over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.addlinemetered/cpp/wire.h> #include <lib/async-loop/cpp/loop.h> #include <lib/async/cpp/task.h> #include <lib/component/outgoing/cpp/outgoing_directory.h> #include <lib/fidl/cpp/wire/channel.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; examples_canvas_addlinemetered::wire::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public fidl::WireServer<examples_canvas_addlinemetered::Instance> { public: // Bind this implementation to a channel. InstanceImpl(async_dispatcher_t* dispatcher, fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) : binding_(fidl::BindServer( dispatcher, std::move(server_end), this, [this](InstanceImpl* impl, fidl::UnbindInfo info, fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) { if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; })), weak_factory_(this) { // Start the update timer on startup. Our server sends one update per second ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void AddLine(AddLineRequestView request, AddLineCompleter::Sync& completer) override { auto points = request->line; FX_LOGS(INFO) << "AddLine request received: [Point { x: " << points[1].x << ", y: " << points[1].y << " }, Point { x: " << points[0].x << ", y: " << points[0].y << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : request->line) { if (point.x < bounds.top_left.x) { bounds.top_left.x = point.x; } if (point.y > bounds.top_left.y) { bounds.top_left.y = point.y; } if (point.x > bounds.bottom_right.x) { bounds.bottom_right.x = point.x; } if (point.y < bounds.bottom_right.y) { bounds.bottom_right.y = point.y; } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; // Because this is now a two-way method, we must use the generated |completer| to send an in // this case empty reply back to the client. This is the mechanic which syncs the flow rate // between the client and server on this method, thereby preventing the client from "flooding" // the server with unacknowledged work. completer.Reply(); FX_LOGS(INFO) << "AddLine response sent"; } void handle_unknown_method( fidl::UnknownMethodMetadata<examples_canvas_addlinemetered::Instance> metadata, fidl::UnknownMethodCompleter::Sync& completer) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one. if (!weak->state_.changed) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto top_left = weak->state_.bounding_box.top_left; auto bottom_right = weak->state_.bounding_box.bottom_right; fidl::Status status = fidl::WireSendEvent(weak->binding_)->OnDrawn(top_left, bottom_right); if (!status.ok()) { return; } FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; // Reset the change tracker. weak->state_.changed = false; }, after); } fidl::ServerBindingRef<examples_canvas_addlinemetered::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This // directory is where the outgoing FIDL protocols are installed so that they can be provided to // other components. component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher); // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle. // The startup handle is a handle provided to every component by the system, so that they can // serve capabilities (e.g. FIDL protocols) to other components. zx::result result = outgoing.ServeFromStartupInfo(); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string(); return -1; } // Register a handler for components trying to connect to // |examples.canvas.addlinemetered.Instance|. result = outgoing.AddUnmanagedProtocol<examples_canvas_addlinemetered::Instance>( [dispatcher](fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) { // Create an instance of our InstanceImpl that destroys itself when the connection closes. new InstanceImpl(dispatcher, std::move(server_end)); }); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string(); return -1; } // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
HLCPP
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <lib/async-loop/cpp/loop.h> #include <lib/sys/cpp/component_context.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/canvas/addlinemetered/cpp/fidl.h> #include <examples/fidl/new/canvas/add_line_metered/hlcpp/client/config.h> #include "lib/fpromise/result.h" // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples::canvas::addlinemetered::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples::canvas::addlinemetered::Point{.x = x, .y = y}; } // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. ::std::array<::examples::canvas::addlinemetered::Point, 2> ParseLine(const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace, then create an asynchronous client // using the newly-established connection. examples::canvas::addlinemetered::InstancePtr instance_proxy; auto context = sys::ComponentContext::Create(); context->svc()->Connect(instance_proxy.NewRequest(dispatcher)); FX_LOGS(INFO) << "Outgoing connection enabled"; instance_proxy.set_error_handler([&loop](zx_status_t status) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; loop.Quit(); }); // Provide a lambda to handle incoming |OnDrawn| events asynchronously. instance_proxy.events().OnDrawn = [&loop]( ::examples::canvas::addlinemetered::Point top_left, ::examples::canvas::addlinemetered::Point bottom_right) { FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; loop.Quit(); }; instance_proxy.events().handle_unknown_event = [](uint64_t ordinal) { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << ordinal; }; for (const auto& action : conf.script()) { // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); continue; } // Draw a line to the canvas by calling the server, using the two points we just parsed // above as arguments. auto line = ParseLine(action); FX_LOGS(INFO) << "AddLine request sent: [Point { x: " << line[1].x << ", y: " << line[1].y << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]"; instance_proxy->AddLine(line, [&](fpromise::result<void, fidl::FrameworkErr> result) { if (result.is_error()) { // Check that our flexible two-way call was known to the server and handle the case of an // unknown method appropriately. In the case of this example, there is nothing we can do to // recover here, except to log an error and exit the program. FX_LOGS(ERROR) << "Server does not implement AddLine"; } FX_LOGS(INFO) << "AddLine response received"; // Quit the loop, thereby handing control back to the outer loop of actions being iterated // over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <lib/async-loop/cpp/loop.h> #include <lib/async-loop/default.h> #include <lib/async/cpp/task.h> #include <lib/fidl/cpp/binding.h> #include <lib/sys/cpp/component_context.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <examples/canvas/addlinemetered/cpp/fidl.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; examples::canvas::addlinemetered::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public examples::canvas::addlinemetered::Instance { public: // Bind this implementation to an |InterfaceRequest|. InstanceImpl(async_dispatcher_t* dispatcher, fidl::InterfaceRequest<examples::canvas::addlinemetered::Instance> request) : binding_(fidl::Binding<examples::canvas::addlinemetered::Instance>(this)), weak_factory_(this) { binding_.Bind(std::move(request), dispatcher); // Gracefully handle abrupt shutdowns. binding_.set_error_handler([this](zx_status_t status) mutable { if (status != ZX_ERR_PEER_CLOSED) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; }); // Start the update timer on startup. Our server sends one update per second. ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void AddLine(::std::array<::examples::canvas::addlinemetered::Point, 2> line, AddLineCallback callback) override { FX_LOGS(INFO) << "AddLine request received: [Point { x: " << line[1].x << ", y: " << line[1].y << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : line) { if (point.x < bounds.top_left.x) { bounds.top_left.x = point.x; } if (point.y > bounds.top_left.y) { bounds.top_left.y = point.y; } if (point.x > bounds.bottom_right.x) { bounds.bottom_right.x = point.x; } if (point.y < bounds.bottom_right.y) { bounds.bottom_right.y = point.y; } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; // Because this is now a two-way method, we must use the generated |callback| to send an in // this case empty reply back to the client. This is the mechanic which syncs the flow rate // between the client and server on this method, thereby preventing the client from "flooding" // the server with unacknowledged work. callback(fpromise::ok()); FX_LOGS(INFO) << "AddLine response sent"; } void handle_unknown_method(uint64_t ordinal, bool method_has_response) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one. if (!weak->state_.changed) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto top_left = state_.bounding_box.top_left; auto bottom_right = state_.bounding_box.bottom_right; binding_.events().OnDrawn(top_left, bottom_right); FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; // Reset the change tracker. state_.changed = false; }, after); } fidl::Binding<examples::canvas::addlinemetered::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. // // Note that unlike the new C++ bindings, HLCPP bindings rely on the async loop being attached to // the current thread via the |kAsyncLoopConfigAttachToCurrentThread| configuration. async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. // This directory is where the outgoing FIDL protocols are installed so that they can be // provided to other components. auto context = sys::ComponentContext::CreateAndServeOutgoingDirectory(); // Register a handler for components trying to connect to // |examples.canvas.addlinemetered.Instance|. context->outgoing()->AddPublicService( fidl::InterfaceRequestHandler<examples::canvas::addlinemetered::Instance>( [dispatcher](fidl::InterfaceRequest<examples::canvas::addlinemetered::Instance> request) { // Create an instance of our |InstanceImpl| that destroys itself when the connection // closes. new InstanceImpl(dispatcher, std::move(request)); })); // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
使用事件推送有界限数据
在 FIDL 中,服务器可以向客户发送非请求消息,这种消息称为事件。 使用事件的协议需要特别注意流控制 因为事件机制本身不提供任何流控制。
适合使用事件的情况是,最多只有一个事件实例 在整个频道生命周期内发送的在此模式中,协议不 需要对事件进行任意流程控制:
protocol DeathWish {
-> OnFatalError(struct {
error_code zx.Status;
});
};
事件的另一个好用的情况是,客户端向服务器发出 以及服务器生成的事件总数 有界限此模式是挂起 get 的更复杂的版本 服务器可以响应“get”请求有限数量的 次(而不仅仅是一次):
protocol NetworkScanner {
ScanForNetworks();
-> OnNetworkDiscovered(struct {
network string;
});
-> OnScanFinished();
};
使用确认对事件进行限制
如果提前的事件数量没有已知的限制,请考虑 让客户端通过发送消息来确认事件此模式是 更为尴尬的确认 模式中,客户端的角色 和服务器与在另一种模式中一样,服务器应限制 事件制作,以与客户端处理事件的速率保持一致:
protocol View1 {
-> OnInputEvent(struct {
event InputEvent;
});
NotifyInputEventHandled();
};
与常规确认模式相比,这种模式有一个优势: 客户端可以使用一条消息更轻松地确认多个事件 因为确认会解除与被确认的事件的关联。 此模式可减少批量处理,从而提高批量处理效率 它非常适用于按顺序处理多个 事件类型:
protocol View2 {
-> OnInputEvent(struct {
event InputEvent;
seq uint64;
});
-> OnFocusChangedEvent(struct {
event FocusChangedEvent;
seq uint64;
});
NotifyEventsHandled(struct {
last_seq uint64;
});
};
与使用确认限制推送不同,此模式不会将 FIDL 语法中请求和响应之间的关系, 并且容易被滥用仅当客户端正确时,流控制才能正常运行 实现通知消息的发送。
FIDL 配方:受限事件模式
事件是从服务器发起的 FIDL 调用。由于这些调用 内置的客户端响应功能,因此不受流程控制:服务器可以 将大量此类调用加入队列,并使客户端过载。一种解决方案 是受限事件模式。此模式涉及向 FIDL 方法作为一个或多个消息的确认点, 活动。
服务器应避免发送更多被限制的事件(确切地 语义特定于实现协议),直到它们具有 收到来自客户端的下一次确认调用同样,客户端 如果服务器发送的节流事件数超过 在客户端确认之前被允许。这些限制 内置于 FIDL 运行时中,并且需要一些手动实现 客户端/服务器实现人员,以确保行为正确无误。
提高 Instance
协议性能的一种方法是允许
批量行,而不是每次发送一个 AddLine(...);
我们需要将新行添加到画布,等待回复,然后
再对下一行执行同样的操作,我们可以将多行批量处理为一个
调用新的 AddLines(...);
调用。客户现在可以决定如何
最好将一大群要绘制的线段分割出来。
如果单纯地实现,我们会发现自己处于服务器和
客户端完全不同步:客户端可能会使用大量的
无界限的 AddLines(...);
调用,服务器同样会用大量数据传送客户端
-> OnDrawn(...);
事件数超出其处理能力这两种问题的解决方法
这些问题是添加一个简单的 Ready() -> ();
方法来进行同步
目的。每当客户端准备好接收数据时,都会调用此方法。
下一次绘制更新,服务器响应表明客户端
可以继续处理更多请求
现在,我们在两个方向上都提供了一些流控制。现在,该协议
前馈模式,从而允许在某些
同步“提交”调用会在服务器上触发实际工作。这个
防止客户端超负荷运行服务器。同样,
服务器不再允许发送无界限 -> OnDrawn(...);
事件:
事件必须跟在来自客户端的信号(即 Ready() -> ();
调用)之后,
表明它已经可以做更多工作了。这称为受限的
事件模式。
具体实现必须手动应用其中一些规则:客户端
如果收到其未收到的 -> OnDrawn(...);
事件,则必须关闭连接
通过 Ready() -> ();
方法发出请求。
FIDL、CML 和 Realm 接口的定义如下所示:
FIDL
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. library examples.canvas.clientrequesteddraw; /// A point in 2D space. type Point = struct { x int64; y int64; }; /// A line in 2D space. alias Line = array<Point, 2>; /// A bounding box in 2D space. This is the result of "drawing" operations on our canvas, and what /// the server reports back to the client. These bounds are sufficient to contain all of the /// lines (inclusive) on a canvas at a given time. type BoundingBox = struct { top_left Point; bottom_right Point; }; /// Manages a single instance of a canvas. Each session of this protocol is responsible for a new /// canvas. @discoverable open protocol Instance { /// Add multiple lines to the canvas. We are able to reduce protocol chatter and the number of /// requests needed by batching instead of calling the simpler `AddLine(...)` one line at a /// time. flexible AddLines(struct { lines vector<Line>; }); /// Rather than the server randomly performing draws, or trying to guess when to do so, the /// client must explicitly ask for them. This creates a bit of extra chatter with the additional /// method invocation, but allows much greater client-side control of when the canvas is "ready" /// for a view update, thereby eliminating unnecessary draws. /// /// This method also has the benefit of "throttling" the `-> OnDrawn(...)` event - rather than /// allowing a potentially unlimited flood of `-> OnDrawn(...)` calls, we now have the runtime /// enforced semantic that each `-> OnDrawn(...)` call must follow a unique `Ready() -> ()` call /// from the client. An unprompted `-> OnDrawn(...)` is invalid, and should cause the channel to /// immediately close. flexible Ready() -> (); /// Update the client with the latest drawing state. The server makes no guarantees about how /// often this event occurs - it could occur multiple times per board state, for example. flexible -> OnDrawn(BoundingBox); };
CML
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/client_bin", }, use: [ { protocol: "examples.canvas.clientrequesteddraw.Instance" }, ], config: { // A script for the client to follow. Entries in the script may take one of two forms: a // pair of signed-integer coordinates like "-2,15:4,5", or the string "READY". The former // builds a local vector sent via a single `AddLines(...)` call, while the latter sends a // `Ready() -> ()` call pauses execution until the next `->OnDrawn(...)` event is received. // // TODO(https://fxbug.dev/42178362): It would absolve individual language implementations of a great // deal of string parsing if we were able to use a vector of `union { Point; Ready}` here. script: { type: "vector", max_count: 100, element: { type: "string", max_size: 64, }, }, }, }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/server_bin", }, capabilities: [ { protocol: "examples.canvas.clientrequesteddraw.Instance" }, ], expose: [ { protocol: "examples.canvas.clientrequesteddraw.Instance", from: "self", }, ], }
大区
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { children: [ { name: "client", url: "#meta/client.cm", }, { name: "server", url: "#meta/server.cm", }, ], offer: [ // Route the protocol under test from the server to the client. { protocol: "examples.canvas.clientrequesteddraw.Instance", from: "#server", to: "#client", }, // Route diagnostics support to all children. { protocol: [ "fuchsia.inspect.InspectSink", "fuchsia.logger.LogSink", ], from: "parent", to: [ "#client", "#server", ], }, ], }
然后,可以使用任何受支持的语言编写客户端和服务器实现:
Rust
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{format_err, Context as _, Error}; use config::Config; use fidl_examples_canvas_clientrequesteddraw::{InstanceEvent, InstanceMarker, Point}; use fuchsia_component::client::connect_to_protocol; use futures::TryStreamExt; use std::{thread, time}; #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Load the structured config values passed to this component at startup. let config = Config::take_from_startup_handle(); // Use the Component Framework runtime to connect to the newly spun up server component. We wrap // our retained client end in a proxy object that lets us asynchronously send Instance requests // across the channel. let instance = connect_to_protocol::<InstanceMarker>()?; println!("Outgoing connection enabled"); let mut batched_lines = Vec::<[Point; 2]>::new(); for action in config.script.into_iter() { // If the next action in the script is to "PUSH", send a batch of lines to the server. if action == "PUSH" { instance.add_lines(&batched_lines).context("Could not send lines")?; println!("AddLines request sent"); batched_lines.clear(); continue; } // If the next action in the script is to "WAIT", block until an OnDrawn event is received // from the server. if action == "WAIT" { let mut event_stream = instance.take_event_stream(); loop { match event_stream .try_next() .await .context("Error getting event response from proxy")? .ok_or_else(|| format_err!("Proxy sent no events"))? { InstanceEvent::OnDrawn { top_left, bottom_right } => { println!( "OnDrawn event received: top_left: {:?}, bottom_right: {:?}", top_left, bottom_right ); break; } InstanceEvent::_UnknownEvent { ordinal, .. } => { println!("Received an unknown event with ordinal {ordinal}"); } } } // Now, inform the server that we are ready to receive more updates whenever they are // ready for us. println!("Ready request sent"); instance.ready().await.context("Could not send ready call")?; println!("Ready success"); continue; } // Add a line to the next batch. Parse the string input, making two points out of it. let mut points = action .split(":") .map(|point| { let integers = point .split(",") .map(|integer| integer.parse::<i64>().unwrap()) .collect::<Vec<i64>>(); Point { x: integers[0], y: integers[1] } }) .collect::<Vec<Point>>(); // Assemble a line from the two points. let from = points.pop().ok_or(format_err!("line requires 2 points, but has 0"))?; let to = points.pop().ok_or(format_err!("line requires 2 points, but has 1"))?; let mut line: [Point; 2] = [from, to]; // Batch a line for drawing to the canvas using the two points provided. println!("AddLines batching line: {:?}", &mut line); batched_lines.push(line); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. thread::sleep(time::Duration::from_secs(2)); Ok(()) }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{anyhow, Context as _, Error}; use fidl::endpoints::RequestStream as _; use fidl_examples_canvas_clientrequesteddraw::{ BoundingBox, InstanceRequest, InstanceRequestStream, Point, }; use fuchsia_async::{Time, Timer}; use fuchsia_component::server::ServiceFs; use fuchsia_zircon::{self as zx}; use futures::future::join; use futures::prelude::*; use std::sync::{Arc, Mutex}; // A struct that stores the two things we care about for this example: the bounding box the lines // that have been added thus far, and bit to track whether or not there have been changes since the // last `OnDrawn` event. #[derive(Debug)] struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. changed: bool, // Tracks whether or not the client has declared itself ready to receive more updated. ready: bool, bounding_box: BoundingBox, } /// Handler for the `AddLines` method. fn add_lines(state: &mut CanvasState, lines: Vec<[Point; 2]>) { // Update the bounding box to account for the new lines we've just "added" to the canvas. let bounds = &mut state.bounding_box; for line in lines { println!("AddLines printing line: {:?}", line); for point in line { if point.x < bounds.top_left.x { bounds.top_left.x = point.x; } if point.y > bounds.top_left.y { bounds.top_left.y = point.y; } if point.x > bounds.bottom_right.x { bounds.bottom_right.x = point.x; } if point.y < bounds.bottom_right.y { bounds.bottom_right.y = point.y; } } } // Mark the state as "dirty", so that an update is sent back to the client on the next tick. state.changed = true } /// Creates a new instance of the server, paired to a single client across a zircon channel. async fn run_server(stream: InstanceRequestStream) -> Result<(), Error> { // Create a new in-memory state store for the state of the canvas. The store will live for the // lifetime of the connection between the server and this particular client. let state = Arc::new(Mutex::new(CanvasState { changed: true, ready: true, bounding_box: BoundingBox { top_left: Point { x: 0, y: 0 }, bottom_right: Point { x: 0, y: 0 }, }, })); // Take ownership of the control_handle from the stream, which will allow us to push events from // a different async task. let control_handle = stream.control_handle(); // A separate watcher task periodically "draws" the canvas, and notifies the client of the new // state. We'll need a cloned reference to the canvas state to be accessible from the new // task. let state_ref = state.clone(); let update_sender = || async move { loop { // Our server sends one update per second, but only if the client has declared that it // is ready to receive one. Timer::new(Time::after(zx::Duration::from_seconds(1))).await; let mut state = state_ref.lock().unwrap(); if !state.changed || !state.ready { continue; } // After acquiring the lock, this is where we would draw the actual lines. Since this is // just an example, we'll avoid doing the actual rendering, and simply send the bounding // box to the client instead. let bounds = state.bounding_box; match control_handle.send_on_drawn(&bounds.top_left, &bounds.bottom_right) { Ok(_) => println!( "OnDrawn event sent: top_left: {:?}, bottom_right: {:?}", bounds.top_left, bounds.bottom_right ), Err(_) => return, } // Reset the change and ready trackers. state.ready = false; state.changed = false; } }; // Handle requests on the protocol sequentially - a new request is not handled until its // predecessor has been processed. let state_ref = &state; let request_handler = stream.map(|result| result.context("failed request")).try_for_each(|request| async move { // Match based on the method being invoked. match request { InstanceRequest::AddLines { lines, .. } => { println!("AddLines request received"); add_lines(&mut state_ref.lock().unwrap(), lines); } InstanceRequest::Ready { responder, .. } => { println!("Ready request received"); // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` // event; if two "consecutive" `Ready() -> ();` calls are received, this // interaction has entered an invalid state, and should be aborted immediately. let mut state = state_ref.lock().unwrap(); if state.ready == true { return Err(anyhow!("Invalid back-to-back `Ready` requests received")); } state.ready = true; responder.send().context("Error responding")?; } // InstanceRequest::_UnknownMethod { ordinal, .. } => { println!("Received an unknown method with ordinal {ordinal}"); } } Ok(()) }); // This line will only be reached if the server errors out. The stream will await indefinitely, // thereby creating a long-lived server. Here, we first wait for the updater task to realize the // connection has died, then bubble up the error. join(request_handler, update_sender()).await.0 } // A helper enum that allows us to treat a `Instance` service instance as a value. enum IncomingService { Instance(InstanceRequestStream), } #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Add a discoverable instance of our `Instance` protocol - this will allow the client to see // the server and connect to it. let mut fs = ServiceFs::new_local(); fs.dir("svc").add_fidl_service(IncomingService::Instance); fs.take_and_serve_directory_handle()?; println!("Listening for incoming connections"); // The maximum number of concurrent clients that may be served by this process. const MAX_CONCURRENT: usize = 10; // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit. fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Instance(stream)| { run_server(stream).unwrap_or_else(|e| println!("{:?}", e)) }) .await; Ok(()) }
C++(自然)
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.clientrequesteddraw/cpp/fidl.h> #include <lib/async-loop/cpp/loop.h> #include <lib/component/incoming/cpp/protocol.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/fidl/new/canvas/client_requested_draw/cpp_natural/client/config.h> // The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming // events asynchronously. class EventHandler : public fidl::AsyncEventHandler<examples_canvas_clientrequesteddraw::Instance> { public: // Handler for |OnDrawn| events sent from the server. void OnDrawn( fidl::Event<examples_canvas_clientrequesteddraw::Instance::OnDrawn>& event) override { ::examples_canvas_clientrequesteddraw::Point top_left = event.top_left(); ::examples_canvas_clientrequesteddraw::Point bottom_right = event.bottom_right(); FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x() << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x() << ", y: " << bottom_right.y() << " }"; loop_.Quit(); } void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; } void handle_unknown_event( fidl::UnknownEventMetadata<examples_canvas_clientrequesteddraw::Instance> metadata) override { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal; } explicit EventHandler(async::Loop& loop) : loop_(loop) {} private: async::Loop& loop_; }; // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples_canvas_clientrequesteddraw::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples_canvas_clientrequesteddraw::Point(x, y); } using Line = ::std::array<::examples_canvas_clientrequesteddraw::Point, 2>; // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. Line ParseLine(const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop and dispatcher. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a // |zx::result| and it must be checked for errors. zx::result client_end = component::Connect<examples_canvas_clientrequesteddraw::Instance>(); if (!client_end.is_ok()) { FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: " << client_end.status_string(); return -1; } // Create an instance of the event handler. EventHandler event_handler(loop); // Create an asynchronous client using the newly-established connection. fidl::Client client(std::move(*client_end), dispatcher, &event_handler); FX_LOGS(INFO) << "Outgoing connection enabled"; std::vector<Line> batched_lines; for (const auto& action : conf.script()) { // If the next action in the script is to "PUSH", send a batch of lines to the server. if (action == "PUSH") { fit::result<fidl::Error> result = client->AddLines(batched_lines); if (!result.is_ok()) { // Check that our one-way call was enqueued successfully, and handle the error // appropriately. In the case of this example, there is nothing we can do to recover here, // except to log an error and exit the program. FX_LOGS(ERROR) << "Could not send AddLines request: " << result.error_value(); return -1; } batched_lines.clear(); FX_LOGS(INFO) << "AddLines request sent"; continue; } // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); // Now, inform the server that we are ready to receive more updates whenever they are // ready for us. FX_LOGS(INFO) << "Ready request sent"; client->Ready().ThenExactlyOnce( [&](fidl::Result<examples_canvas_clientrequesteddraw::Instance::Ready> result) { // Check if the FIDL call succeeded or not. if (result.is_ok()) { FX_LOGS(INFO) << "Ready success"; } else { FX_LOGS(ERROR) << "Could not send Ready request: " << result.error_value(); } // Quit the loop, thereby handing control back to the outer loop of actions being // iterated over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); continue; } // Batch a line for drawing to the canvas using the two points provided. Line line = ParseLine(action); batched_lines.push_back(line); FX_LOGS(INFO) << "AddLines batching line: [Point { x: " << line[1].x() << ", y: " << line[1].y() << " }, Point { x: " << line[0].x() << ", y: " << line[0].y() << " }]"; } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.clientrequesteddraw/cpp/fidl.h> #include <lib/async-loop/cpp/loop.h> #include <lib/async/cpp/task.h> #include <lib/component/outgoing/cpp/outgoing_directory.h> #include <lib/fidl/cpp/wire/channel.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; // Tracks whether or not the client has declared itself ready to receive more updated. bool ready = true; examples_canvas_clientrequesteddraw::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public fidl::Server<examples_canvas_clientrequesteddraw::Instance> { public: // Bind this implementation to a channel. InstanceImpl(async_dispatcher_t* dispatcher, fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) : binding_(dispatcher, std::move(server_end), this, std::mem_fn(&InstanceImpl::OnFidlClosed)), weak_factory_(this) { // Start the update timer on startup. Our server sends one update per second ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void OnFidlClosed(fidl::UnbindInfo info) { if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; } void AddLines(AddLinesRequest& request, AddLinesCompleter::Sync& completer) override { FX_LOGS(INFO) << "AddLines request received"; for (const auto& points : request.lines()) { FX_LOGS(INFO) << "AddLines printing line: [Point { x: " << points[1].x() << ", y: " << points[1].y() << " }, Point { x: " << points[0].x() << ", y: " << points[0].y() << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : points) { if (point.x() < bounds.top_left().x()) { bounds.top_left().x() = point.x(); } if (point.y() > bounds.top_left().y()) { bounds.top_left().y() = point.y(); } if (point.x() > bounds.bottom_right().x()) { bounds.bottom_right().x() = point.x(); } if (point.y() < bounds.bottom_right().y()) { bounds.bottom_right().y() = point.y(); } } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; } void Ready(ReadyCompleter::Sync& completer) override { FX_LOGS(INFO) << "Ready request received"; // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` event; if two // "consecutive" `Ready() -> ();` calls are received, this interaction has entered an invalid // state, and should be aborted immediately. if (state_.ready == true) { FX_LOGS(ERROR) << "Invalid back-to-back `Ready` requests received"; } state_.ready = true; completer.Reply(); } void handle_unknown_method( fidl::UnknownMethodMetadata<examples_canvas_clientrequesteddraw::Instance> metadata, fidl::UnknownMethodCompleter::Sync& completer) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one, or the client has // not yet informed us that it is ready for more updates. if (!weak->state_.changed || !weak->state_.ready) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto result = fidl::SendEvent(binding_)->OnDrawn(state_.bounding_box); if (!result.is_ok()) { return; } auto top_left = state_.bounding_box.top_left(); auto bottom_right = state_.bounding_box.bottom_right(); FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x() << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x() << ", y: " << bottom_right.y() << " }"; // Reset the change and ready trackers. state_.ready = false; state_.changed = false; }, after); } fidl::ServerBinding<examples_canvas_clientrequesteddraw::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This // directory is where the outgoing FIDL protocols are installed so that they can be provided to // other components. component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher); // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle. // The startup handle is a handle provided to every component by the system, so that they can // serve capabilities (e.g. FIDL protocols) to other components. zx::result result = outgoing.ServeFromStartupInfo(); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string(); return -1; } // Register a handler for components trying to connect to // |examples.canvas.clientrequesteddraw.Instance|. result = outgoing.AddUnmanagedProtocol<examples_canvas_clientrequesteddraw::Instance>( [dispatcher](fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) { // Create an instance of our InstanceImpl that destroys itself when the connection closes. new InstanceImpl(dispatcher, std::move(server_end)); }); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string(); return -1; } // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
C++(有线)
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.clientrequesteddraw/cpp/wire.h> #include <lib/async-loop/cpp/loop.h> #include <lib/component/incoming/cpp/protocol.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/fidl/new/canvas/client_requested_draw/cpp_wire/client/config.h> // The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming // events asynchronously. class EventHandler : public fidl::WireAsyncEventHandler<examples_canvas_clientrequesteddraw::Instance> { public: // Handler for |OnDrawn| events sent from the server. void OnDrawn( fidl::WireEvent<examples_canvas_clientrequesteddraw::Instance::OnDrawn>* event) override { ::examples_canvas_clientrequesteddraw::wire::Point top_left = event->top_left; ::examples_canvas_clientrequesteddraw::wire::Point bottom_right = event->bottom_right; FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; loop_.Quit(); } void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; } void handle_unknown_event( fidl::UnknownEventMetadata<examples_canvas_clientrequesteddraw::Instance> metadata) override { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal; } explicit EventHandler(async::Loop& loop) : loop_(loop) {} private: async::Loop& loop_; }; // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples_canvas_clientrequesteddraw::wire::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples_canvas_clientrequesteddraw::wire::Point{.x = x, .y = y}; } using Line = ::fidl::Array<::examples_canvas_clientrequesteddraw::wire::Point, 2>; // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. Line ParseLine(const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop and dispatcher. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a // |zx::result| and it must be checked for errors. zx::result client_end = component::Connect<examples_canvas_clientrequesteddraw::Instance>(); if (!client_end.is_ok()) { FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: " << client_end.status_string(); return -1; } // Create an instance of the event handler. EventHandler event_handler(loop); // Create an asynchronous client using the newly-established connection. fidl::WireClient client(std::move(*client_end), dispatcher, &event_handler); FX_LOGS(INFO) << "Outgoing connection enabled"; std::vector<Line> batched_lines; for (const auto& action : conf.script()) { // If the next action in the script is to "PUSH", send a batch of lines to the server. if (action == "PUSH") { fidl::Status status = client->AddLines(fidl::VectorView<Line>::FromExternal(batched_lines)); if (!status.ok()) { // Check that our one-way call was enqueued successfully, and handle the error // appropriately. In the case of this example, there is nothing we can do to recover here, // except to log an error and exit the program. FX_LOGS(ERROR) << "Could not send AddLines request: " << status.error(); return -1; } batched_lines.clear(); FX_LOGS(INFO) << "AddLines request sent"; continue; } // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); // Now, inform the server that we are ready to receive more updates whenever they are // ready for us. FX_LOGS(INFO) << "Ready request sent"; client->Ready().ThenExactlyOnce( [&](fidl::WireUnownedResult<examples_canvas_clientrequesteddraw::Instance::Ready>& result) { // Check if the FIDL call succeeded or not. if (result.ok()) { FX_LOGS(INFO) << "Ready success"; } else { FX_LOGS(ERROR) << "Could not send Ready request: " << result.error(); } // Quit the loop, thereby handing control back to the outer loop of actions being // iterated over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); continue; } // Batch a line for drawing to the canvas using the two points provided. Line line = ParseLine(action); batched_lines.push_back(line); FX_LOGS(INFO) << "AddLines batching line: [Point { x: " << line[1].x << ", y: " << line[1].y << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]"; } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.clientrequesteddraw/cpp/wire.h> #include <lib/async-loop/cpp/loop.h> #include <lib/async/cpp/task.h> #include <lib/component/outgoing/cpp/outgoing_directory.h> #include <lib/fidl/cpp/wire/channel.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; // Tracks whether or not the client has declared itself ready to receive more updated. bool ready = true; examples_canvas_clientrequesteddraw::wire::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public fidl::WireServer<examples_canvas_clientrequesteddraw::Instance> { public: // Bind this implementation to a channel. InstanceImpl(async_dispatcher_t* dispatcher, fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) : binding_(dispatcher, std::move(server_end), this, std::mem_fn(&InstanceImpl::OnFidlClosed)), weak_factory_(this) { // Start the update timer on startup. Our server sends one update per second ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void OnFidlClosed(fidl::UnbindInfo info) { if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; } void AddLines(AddLinesRequestView request, AddLinesCompleter::Sync& completer) override { FX_LOGS(INFO) << "AddLines request received"; for (const auto& points : request->lines) { FX_LOGS(INFO) << "AddLines printing line: [Point { x: " << points[1].x << ", y: " << points[1].y << " }, Point { x: " << points[0].x << ", y: " << points[0].y << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : points) { if (point.x < bounds.top_left.x) { bounds.top_left.x = point.x; } if (point.y > bounds.top_left.y) { bounds.top_left.y = point.y; } if (point.x > bounds.bottom_right.x) { bounds.bottom_right.x = point.x; } if (point.y < bounds.bottom_right.y) { bounds.bottom_right.y = point.y; } } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; } void Ready(ReadyCompleter::Sync& completer) override { FX_LOGS(INFO) << "Ready request received"; // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` event; if two // "consecutive" `Ready() -> ();` calls are received, this interaction has entered an invalid // state, and should be aborted immediately. if (state_.ready == true) { FX_LOGS(ERROR) << "Invalid back-to-back `Ready` requests received"; } state_.ready = true; completer.Reply(); } void handle_unknown_method( fidl::UnknownMethodMetadata<examples_canvas_clientrequesteddraw::Instance> metadata, fidl::UnknownMethodCompleter::Sync& completer) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one, or the client has // not yet informed us that it is ready for more updates. if (!weak->state_.changed || !weak->state_.ready) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto top_left = weak->state_.bounding_box.top_left; auto bottom_right = weak->state_.bounding_box.bottom_right; fidl::Status status = fidl::WireSendEvent(weak->binding_)->OnDrawn(top_left, bottom_right); if (!status.ok()) { return; } FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; // Reset the change and ready trackers. state_.ready = false; weak->state_.changed = false; }, after); } fidl::ServerBinding<examples_canvas_clientrequesteddraw::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This // directory is where the outgoing FIDL protocols are installed so that they can be provided to // other components. component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher); // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle. // The startup handle is a handle provided to every component by the system, so that they can // serve capabilities (e.g. FIDL protocols) to other components. zx::result result = outgoing.ServeFromStartupInfo(); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string(); return -1; } // Register a handler for components trying to connect to // |examples.canvas.clientrequesteddraw.Instance|. result = outgoing.AddUnmanagedProtocol<examples_canvas_clientrequesteddraw::Instance>( [dispatcher](fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) { // Create an instance of our InstanceImpl that destroys itself when the connection closes. new InstanceImpl(dispatcher, std::move(server_end)); }); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string(); return -1; } // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
HLCPP
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <lib/async-loop/cpp/loop.h> #include <lib/sys/cpp/component_context.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/canvas/clientrequesteddraw/cpp/fidl.h> #include <examples/fidl/new/canvas/client_requested_draw/hlcpp/client/config.h> // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples::canvas::clientrequesteddraw::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples::canvas::clientrequesteddraw::Point{.x = x, .y = y}; } using Line = ::std::array<::examples::canvas::clientrequesteddraw::Point, 2>; // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. Line ParseLine(const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace, then create an asynchronous client // using the newly-established connection. examples::canvas::clientrequesteddraw::InstancePtr instance_proxy; auto context = sys::ComponentContext::Create(); context->svc()->Connect(instance_proxy.NewRequest(dispatcher)); FX_LOGS(INFO) << "Outgoing connection enabled"; instance_proxy.set_error_handler([&loop](zx_status_t status) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; loop.Quit(); }); // Provide a lambda to handle incoming |OnDrawn| events asynchronously. instance_proxy.events().OnDrawn = [&loop](::examples::canvas::clientrequesteddraw::Point top_left, ::examples::canvas::clientrequesteddraw::Point bottom_right) { FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; loop.Quit(); }; instance_proxy.events().handle_unknown_event = [](uint64_t ordinal) { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << ordinal; }; std::vector<Line> batched_lines; for (const auto& action : conf.script()) { // If the next action in the script is to "PUSH", send a batch of lines to the server. if (action == "PUSH") { instance_proxy->AddLines(batched_lines); batched_lines.clear(); FX_LOGS(INFO) << "AddLines request sent"; continue; } // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); // Now, inform the server that we are ready to receive more updates whenever they are ready // for us. FX_LOGS(INFO) << "Ready request sent"; instance_proxy->Ready([&](fpromise::result<void, fidl::FrameworkErr> result) { if (result.is_error()) { // Check that our flexible two-way call was known to the server and handle the case of an // unknown method appropriately. In the case of this example, there is nothing we can do // to recover here, except to log an error and exit the program. FX_LOGS(ERROR) << "Server does not implement AddLine"; } FX_LOGS(INFO) << "Ready success"; // Quit the loop, thereby handing control back to the outer loop of actions being iterated // over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); continue; } // Batch a line for drawing to the canvas using the two points provided. Line line = ParseLine(action); batched_lines.push_back(line); FX_LOGS(INFO) << "AddLines batching line: [Point { x: " << line[1].x << ", y: " << line[1].y << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]"; } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <lib/async-loop/cpp/loop.h> #include <lib/async-loop/default.h> #include <lib/async/cpp/task.h> #include <lib/fidl/cpp/binding.h> #include <lib/sys/cpp/component_context.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <examples/canvas/clientrequesteddraw/cpp/fidl.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; // Tracks whether or not the client has declared itself ready to receive more updated. bool ready = true; examples::canvas::clientrequesteddraw::BoundingBox bounding_box; }; using Line = ::std::array<::examples::canvas::clientrequesteddraw::Point, 2>; // An implementation of the |Instance| protocol. class InstanceImpl final : public examples::canvas::clientrequesteddraw::Instance { public: // Bind this implementation to an |InterfaceRequest|. InstanceImpl(async_dispatcher_t* dispatcher, fidl::InterfaceRequest<examples::canvas::clientrequesteddraw::Instance> request) : binding_(fidl::Binding<examples::canvas::clientrequesteddraw::Instance>(this)), weak_factory_(this) { binding_.Bind(std::move(request), dispatcher); // Gracefully handle abrupt shutdowns. binding_.set_error_handler([this](zx_status_t status) mutable { if (status != ZX_ERR_PEER_CLOSED) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; }); // Start the update timer on startup. Our server sends one update per second. ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void AddLines(std::vector<Line> lines) override { FX_LOGS(INFO) << "AddLines request received"; for (const auto& points : lines) { FX_LOGS(INFO) << "AddLines printing line: [Point { x: " << points[1].x << ", y: " << points[1].y << " }, Point { x: " << points[0].x << ", y: " << points[0].y << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : points) { if (point.x < bounds.top_left.x) { bounds.top_left.x = point.x; } if (point.y > bounds.top_left.y) { bounds.top_left.y = point.y; } if (point.x > bounds.bottom_right.x) { bounds.bottom_right.x = point.x; } if (point.y < bounds.bottom_right.y) { bounds.bottom_right.y = point.y; } } } // Mark the state as "dirty", so that an update is sent back to the client on the next // |OnDrawn| event. state_.changed = true; } void Ready(ReadyCallback callback) override { FX_LOGS(INFO) << "Ready request received"; // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` event; if // two "consecutive" `Ready() -> ();` calls are received, this interaction has entered an // invalid state, and should be aborted immediately. if (state_.ready == true) { FX_LOGS(ERROR) << "Invalid back-to-back `Ready` requests received"; } state_.ready = true; callback(fpromise::ok()); } void handle_unknown_method(uint64_t ordinal, bool method_has_response) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something // has changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one, or the client // has not yet informed us that it is ready for more updates. if (!weak->state_.changed || !weak->state_.ready) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto top_left = state_.bounding_box.top_left; auto bottom_right = state_.bounding_box.bottom_right; binding_.events().OnDrawn(top_left, bottom_right); FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; // Reset the change and ready trackers. state_.ready = false; state_.changed = false; }, after); } fidl::Binding<examples::canvas::clientrequesteddraw::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from // the client. The following initializes the loop, and obtains the dispatcher, which will be // used when binding the server implementation to a channel. // // Note that unlike the new C++ bindings, HLCPP bindings rely on the async loop being attached // to the current thread via the |kAsyncLoopConfigAttachToCurrentThread| configuration. async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. // This directory is where the outgoing FIDL protocols are installed so that they can be // provided to other components. auto context = sys::ComponentContext::CreateAndServeOutgoingDirectory(); // Register a handler for components trying to connect to // |examples.canvas.clientrequesteddraw.Instance|. context->outgoing()->AddPublicService( fidl::InterfaceRequestHandler<examples::canvas::clientrequesteddraw::Instance>( [dispatcher]( fidl::InterfaceRequest<examples::canvas::clientrequesteddraw::Instance> request) { // Create an instance of our |InstanceImpl| that destroys itself when the connection // closes. new InstanceImpl(dispatcher, std::move(request)); })); // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
前馈 Dataflow
某些协议具有前馈数据流,可避免发生往返延迟,具体方法是 数据主要是单向流动,通常是从客户端到服务器。 该协议仅在必要时同步两个端点。前馈 Dataflow 还提高了吞吐量,因为 执行给定任务所需的全部资源。
前馈 Dataflow 的关键是消除了客户端等待 在发送后续消息之前获得之前的方法调用的结果。对于 例如,使用协议请求管道时,客户端无需等待 以便服务器使用协议进行回复,之后客户端才能使用 协议。同样,客户分配的标识符(见下文)可消除 让客户端等待服务器为 服务器
通常,前馈协议要求客户端向
而无需等待服务器响应。更新后
提交这些消息时,客户端会通过以下方式明确与服务器同步:
调用有回复的方法(例如 Commit
或 Flush
)。回复可能
是一条空消息,或者可能包含有关已提交
序列成功。在更复杂的协议中,单向消息
表示为命令对象的并集,而非单独的方法调用;
请参见下面的命令联合模式。
使用前馈数据流的协议可以很好地处理乐观错误 处理策略。不让服务器使用 状态值,该值鼓励客户端在两次访问之间 消息,而应仅在方法可能由于原因而失败时才包含状态回复 不受客户控制的数据如果客户端发送消息 应知道的无效(例如,引用的 客户端分配的标识符),请通过关闭连接来指示错误。如果 客户端发送的消息可能不知道该消息无效, 提供指示成功或失败的响应(这需要客户端 同步),或记住错误并忽略后续的从属请求 直到客户端以某种方式同步并从错误中恢复过来
示例:
protocol Canvas {
Flush() -> (struct {
code zx.Status;
});
Clear();
UploadImage(struct {
image_id uint32;
image Image;
});
PaintImage(struct {
image_id uint32;
x float32;
y float32;
});
DiscardImage(struct {
image_id uint32;
});
PaintSmileyFace(struct {
x float32;
y float32;
});
PaintMoustache(struct {
x float32;
y float32;
});
};
FIDL 配方:大小约束
FIDL 矢量和字符串可以带有大小限制,该约束条件可指定 该类型可以包含的成员数量。对于向量,指的是 向量中存储的元素数,而对于字符串,它是指 字符串包含的字节数。
强烈建议使用大小限制,因为它可以设置 原本将是无界限大型类型的内容
按顺序迭代是对键值存储的有用操作: 用于返回在 让它按顺序显示
推理
在 FIDL 中,最好使用迭代器来完成,迭代器通常以 可进行此迭代的单独协议。使用单独的 因此使用单独的频道具有诸多好处,包括 通过 主协议
协议 P
的通道连接的客户端和服务器端可以是
以 FIDL 数据类型表示,以 client_end:P
和 server_end:P
的形式表示,
。这些类型统称为“协议结束”,
代表另一种(非@discoverable
)将 FIDL 客户端连接到
相应的服务器:通过现有的 FIDL 连接!
协议终止是一般 FIDL 概念的具体实例:资源 type。资源类型旨在包含 FIDL 句柄,这必不可少 对类型使用方式的额外限制。类型必须始终为 唯一,因为底层资源由其他 capability Manager 调控 (通常是 Zircon 内核)。通过简单的内存中复制此类资源 不让经理参与,是不可行的。为避免此类重复, FIDL 中的所有资源类型始终只能移动。
最后,Iterator
协议本身的 Get()
方法使用
大小限制。这限制了
在单次拉取中传输,从而可以衡量资源使用情况
控制。它还创建了自然的分页边界:而不是一个巨大的转储。
所有结果,则服务器只需在
。
实现
FIDL、CML 和 Realm 接口的定义如下所示:
FIDL
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. library examples.keyvaluestore.additerator; /// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That /// is, it must start with a letter, end with a letter or number, contain only letters, numbers, /// periods, and slashes, and be between 4 and 64 characters long. type Item = struct { key string:128; value vector<byte>:64000; }; /// An enumeration of things that may go wrong when trying to write a value to our store. type WriteError = flexible enum { UNKNOWN = 0; INVALID_KEY = 1; INVALID_VALUE = 2; ALREADY_EXISTS = 3; }; /// An enumeration of things that may go wrong when trying to create an iterator. type IterateConnectionError = flexible enum { /// The starting key was not found. UNKNOWN_START_AT = 1; }; /// A key-value store which supports insertion and iteration. @discoverable open protocol Store { /// Writes an item to the store. flexible WriteItem(struct { attempt Item; }) -> () error WriteError; /// Iterates over the items in the store, using lexicographic ordering over the keys. /// /// The [`iterator`] is [pipelined][pipelining] to the server, such that the client can /// immediately send requests over the new connection. /// /// [pipelining]: https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#request-pipelining flexible Iterate(resource struct { /// If present, requests to start the iteration at this item. starting_at string:<128, optional>; /// The [`Iterator`] server endpoint. The client creates both ends of the channel and /// retains the `client_end` locally to use for pulling iteration pages, while sending the /// `server_end` off to be fulfilled by the server. iterator server_end:Iterator; }) -> () error IterateConnectionError; }; /// An iterator for the key-value store. Note that this protocol makes no guarantee of atomicity - /// the values may change between pulls from the iterator. Unlike the `Store` protocol above, this /// protocol is not `@discoverable`: it is not independently published by the component that /// implements it, but rather must have one of its two protocol ends transmitted over an existing /// FIDL connection. /// /// As is often the case with iterators, the client indicates that they are done with an instance of /// the iterator by simply closing their end of the connection. /// /// Since the iterator is associated only with the Iterate method, it is declared as closed rather /// than open. This is because changes to how iteration works are more likely to require replacing /// the Iterate method completely (which is fine because that method is flexible) rather than /// evolving the Iterator protocol. closed protocol Iterator { /// Gets the next batch of keys. /// /// The client pulls keys rather than having the server proactively push them, to implement /// [flow control][flow-control] over the messages. /// /// [flow-control]: /// https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#prefer_pull_to_push strict Get() -> (struct { /// A list of keys. If the iterator has reached the end of iteration, the list will be /// empty. The client is expected to then close the connection. entries vector<string:128>:10; }); };
CML
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/client_bin", }, use: [ { protocol: "examples.keyvaluestore.additerator.Store" }, ], config: { write_items: { type: "vector", max_count: 16, element: { type: "string", max_size: 64, }, }, // A key to iterate from, after all items in `write_items` have been written. iterate_from: { type: "string", max_size: 64, }, }, }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/server_bin", }, capabilities: [ { protocol: "examples.keyvaluestore.additerator.Store" }, ], expose: [ { protocol: "examples.keyvaluestore.additerator.Store", from: "self", }, ], }
大区
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { children: [ { name: "client", url: "#meta/client.cm", }, { name: "server", url: "#meta/server.cm", }, ], offer: [ // Route the protocol under test from the server to the client. { protocol: "examples.keyvaluestore.additerator.Store", from: "#server", to: "#client", }, // Route diagnostics support to all children. { protocol: [ "fuchsia.inspect.InspectSink", "fuchsia.logger.LogSink", ], from: "parent", to: [ "#client", "#server", ], }, ], }
然后,可以使用任何受支持的语言编写客户端和服务器实现:
Rust
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{Context as _, Error}; use config::Config; use fuchsia_component::client::connect_to_protocol; use std::{thread, time}; use fidl::endpoints::create_proxy; use fidl_examples_keyvaluestore_additerator::{Item, IteratorMarker, StoreMarker}; use futures::join; #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Load the structured config values passed to this component at startup. let config = Config::take_from_startup_handle(); // Use the Component Framework runtime to connect to the newly spun up server component. We wrap // our retained client end in a proxy object that lets us asynchronously send `Store` requests // across the channel. let store = connect_to_protocol::<StoreMarker>()?; println!("Outgoing connection enabled"); // This client's structured config has one parameter, a vector of strings. Each string is the // path to a resource file whose filename is a key and whose contents are a value. We iterate // over them and try to write each key-value pair to the remote store. for key in config.write_items.into_iter() { let path = format!("/pkg/data/{}.txt", key); let value = std::fs::read_to_string(path.clone()) .with_context(|| format!("Failed to load {path}"))?; match store.write_item(&Item { key: key, value: value.into_bytes() }).await? { Ok(_) => println!("WriteItem Success"), Err(err) => println!("WriteItem Error: {}", err.into_primitive()), } } if !config.iterate_from.is_empty() { // This helper creates a channel, and returns two protocol ends: the `client_end` is already // conveniently bound to the correct FIDL protocol, `Iterator`, while the `server_end` is // unbound and ready to be sent over the wire. let (iterator, server_end) = create_proxy::<IteratorMarker>()?; // There is no need to wait for the iterator to connect before sending the first `Get()` // request - since we already hold the `client_end` of the connection, we can start queuing // requests on it immediately. let connect_to_iterator = store.iterate(Some(config.iterate_from.as_str()), server_end); let first_get = iterator.get(); // Wait until both the connection and the first request resolve - an error in either case // triggers an immediate resolution of the combined future. let (connection, first_page) = join!(connect_to_iterator, first_get); // Handle any connection error. If this has occurred, it is impossible for the first `Get()` // call to have resolved successfully, so check this error first. if let Err(err) = connection.context("Could not connect to Iterator")? { println!("Iterator Connection Error: {}", err.into_primitive()); } else { println!("Iterator Connection Success"); // Consecutively repeat the `Get()` request if the previous response was not empty. let mut entries = first_page.context("Could not get page from Iterator")?; while !&entries.is_empty() { for entry in entries.iter() { println!("Iterator Entry: {}", entry); } entries = iterator.get().await.context("Could not get page from Iterator")?; } } } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. thread::sleep(time::Duration::from_secs(2)); Ok(()) }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{Context as _, Error}; use fuchsia_component::server::ServiceFs; use futures::prelude::*; use lazy_static::lazy_static; use regex::Regex; use fidl_examples_keyvaluestore_additerator::{ Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest, StoreRequestStream, WriteError, }; use fuchsia_async as fasync; use std::collections::btree_map::Entry; use std::collections::BTreeMap; use std::ops::Bound::*; use std::sync::{Arc, Mutex}; lazy_static! { static ref KEY_VALIDATION_REGEX: Regex = Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile"); } /// Handler for the `WriteItem` method. fn write_item(store: &mut BTreeMap<String, Vec<u8>>, attempt: Item) -> Result<(), WriteError> { // Validate the key. if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) { println!("Write error: INVALID_KEY, For key: {}", attempt.key); return Err(WriteError::InvalidKey); } // Validate the value. if attempt.value.is_empty() { println!("Write error: INVALID_VALUE, For key: {}", attempt.key); return Err(WriteError::InvalidValue); } // Write to the store, validating that the key did not already exist. match store.entry(attempt.key) { Entry::Occupied(entry) => { println!("Write error: ALREADY_EXISTS, For key: {}", entry.key()); Err(WriteError::AlreadyExists) } Entry::Vacant(entry) => { println!("Wrote value at key: {}", entry.key()); entry.insert(attempt.value); Ok(()) } } } /// Handler for the `Iterate` method, which deals with validating that the requested start position /// exists, and then sets up the asynchronous side channel for the actual iteration to occur over. fn iterate( store: Arc<Mutex<BTreeMap<String, Vec<u8>>>>, starting_at: Option<String>, stream: IteratorRequestStream, ) -> Result<(), IterateConnectionError> { // Validate that the starting key, if supplied, actually exists. if let Some(start_key) = starting_at.clone() { if !store.lock().unwrap().contains_key(&start_key) { return Err(IterateConnectionError::UnknownStartAt); } } // Spawn a detached task. This allows the method call to return while the iteration continues in // a separate, unawaited task. fasync::Task::spawn(async move { // Serve the iteration requests. Note that access to the underlying store is behind a // contended `Mutex`, meaning that the iteration is not atomic: page contents could shift, // change, or disappear entirely between `Get()` requests. stream .map(|result| result.context("failed request")) .try_fold( match starting_at { Some(start_key) => Included(start_key), None => Unbounded, }, |mut lower_bound, request| async { match request { IteratorRequest::Get { responder } => { println!("Iterator page request received"); // The `page_size` should be kept in sync with the size constraint on // the iterator's response, as defined in the FIDL protocol. static PAGE_SIZE: usize = 10; // An iterator, beginning at `lower_bound` and tracking the pagination's // progress through iteration as each page is pulled by a client-sent // `Get()` request. let held_store = store.lock().unwrap(); let mut entries = held_store.range((lower_bound.clone(), Unbounded)); let mut current_page = vec![]; for _ in 0..PAGE_SIZE { match entries.next() { Some(entry) => { current_page.push(entry.0.clone()); } None => break, } } // Update the `lower_bound` - either inclusive of the next item in the // iteration, or exclusive of the last seen item if the iteration has // finished. This `lower_bound` will be passed to the next request // handler as its starting point. lower_bound = match entries.next() { Some(next) => Included(next.0.clone()), None => match current_page.last() { Some(tail) => Excluded(tail.clone()), None => lower_bound, }, }; // Send the page. At the end of this scope, the `held_store` lock gets // dropped, and therefore released. responder.send(¤t_page).context("error sending reply")?; println!("Iterator page sent"); } } Ok(lower_bound) }, ) .await .ok(); }) .detach(); Ok(()) } /// Creates a new instance of the server. Each server has its own bespoke, per-connection instance /// of the key-value store. async fn run_server(stream: StoreRequestStream) -> Result<(), Error> { // Create a new in-memory key-value store. The store will live for the lifetime of the // connection between the server and this particular client. // // Note that we now use an `Arc<Mutex<BTreeMap>>`, replacing the previous `RefCell<HashMap>`. // The `BTreeMap` is used because we want an ordered map, to better facilitate iteration. The // `Arc<Mutex<...>>` is used because there are now multiple async tasks accessing the: one main // task which handles communication over the protocol, and one additional task per iterator // protocol. `Arc<Mutex<...>>` is the simplest way to synchronize concurrent access between // these racing tasks. let store = &Arc::new(Mutex::new(BTreeMap::<String, Vec<u8>>::new())); // Serve all requests on the protocol sequentially - a new request is not handled until its // predecessor has been processed. stream .map(|result| result.context("failed request")) .try_for_each(|request| async { // Match based on the method being invoked. match request { StoreRequest::WriteItem { attempt, responder } => { println!("WriteItem request received"); // The `responder` parameter is a special struct that manages the outgoing reply // to this method call. Calling `send` on the responder exactly once will send // the reply. responder .send(write_item(&mut store.clone().lock().unwrap(), attempt)) .context("error sending reply")?; println!("WriteItem response sent"); } StoreRequest::Iterate { starting_at, iterator, responder } => { println!("Iterate request received"); // The `iterate` handler does a quick check to see that the request is valid, // then spins up a separate worker task to serve the newly minted `Iterator` // protocol instance, allowing this call to return immediately and continue the // request stream with other work. responder .send(iterate(store.clone(), starting_at, iterator.into_stream()?)) .context("error sending reply")?; println!("Iterate response sent"); } // StoreRequest::_UnknownMethod { ordinal, .. } => { println!("Received an unknown method with ordinal {ordinal}"); } } Ok(()) }) .await } // A helper enum that allows us to treat a `Store` service instance as a value. enum IncomingService { Store(StoreRequestStream), } #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Add a discoverable instance of our `Store` protocol - this will allow the client to see the // server and connect to it. let mut fs = ServiceFs::new_local(); fs.dir("svc").add_fidl_service(IncomingService::Store); fs.take_and_serve_directory_handle()?; println!("Listening for incoming connections"); // The maximum number of concurrent clients that may be served by this process. const MAX_CONCURRENT: usize = 10; // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit. fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| { run_server(stream).unwrap_or_else(|e| println!("{:?}", e)) }) .await; Ok(()) }
C++(自然)
客户端
// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.
服务器
// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.
C++(有线)
客户端
// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.
服务器
// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.
HLCPP
客户端
// TODO(https://fxbug.dev/42060656): HLCPP implementation.
服务器
// TODO(https://fxbug.dev/42060656): HLCPP implementation.
从设计上保障用户隐私
协议中的客户端和服务器通常可以访问 敏感数据。隐私问题或安全问题可能是由无意中导致的 会造成不必要的数据泄露
在设计协议时,请特别注意协议中的字段 :
- 包含个人身份信息,例如姓名、电子邮件地址 或付款信息。
- 由用户提供,因此可能包含个人信息。示例 添加设备名称和注释字段
- 作为唯一标识符,可以在供应商、用户、 设备或设备例如序列号、MAC 地址、IP 地址和全局账号 ID。
我们会全面审核这些类型的字段,并检查协议是否可用 可能会受到限制请确保您的协议不包含 会比实际需要的信息多。
如果 API 的用例需要个人数据或可关联的数据,以及其他用例 不必考虑使用两种不同的协议, 敏感的使用情形可能会单独控制。
我们来看两个假设示例来说明 API 设计选择:
示例 1 - 外围设备控制 API 中的序列号
假设有一个包含 USB 序列号的外围设备控制 API 外围设备序列号不包含个人数据, 并且是易于关联的稳定标识符包括序列号 API 会导致许多隐私问题:
- 任何有权访问该 API 的客户都可以将不同的账号关联起来 使用相同的 Fuchsia 设备。
- 任何有权访问该 API 的客户端都可以将不同的角色 各个项目
- 不同的软件供应商可能会进行协作,以了解他们是否在使用 由同一用户或同一设备创建
- 如果在设备之间移动外围设备,则任何有权访问该 API 的客户端 可以将外围设备与共用外围设备的那组设备关联起来。
- 如果外围设备售出,则有权访问该 API 的客户可以将 外围设备的新所有者和新所有者。
- 一些制造商会在序列号中对信息进行编码。这可能会使 有权访问该 API 的客户可以推断出用户购买 API 的时间或地点 外设。
在此示例中,序列号的目的是允许客户端检测 当重新连接同一 USB 外围设备时触发。实现此意图需要 稳定的标识符,但不需要全局标识符。不同客户端 同一客户端无需接收相同的标识符 在不同 Fuchsia 设备上接收相同的标识符,而该标识符 不需要在恢复出厂设置事件期间保持不变。
在本例中,一个很好的替代方法是发送 保证对于单台设备上的单个客户端而言是稳定的。此标识符 可能是外围设备序列号的哈希值(即 Fuchsia) 设备标识符以及连接的别名。
示例 2 - 设备设置 API 中的设备名称
假设有一个设备设置 API,其中包含用于 协助您完成设备设置。在大多数情况下,手机的型号字符串由 但有些手机会报告用户提供的设备名称作为其型号。这个 可生成许多包含模型字符串的真实名称或假名的模型字符串 用户。因此,此 API 存在将用户跨身份或跨身份的关联风险。 设备。罕见或预发布的模型字符串可能会泄露敏感信息 即使它并非由用户提供
在某些情况下,可能可以使用模型字符串, 哪些客户端可以访问 API或者,API 可以使用 (如制造商字符串)。另一个 另一种方法是将模型字符串与 热门手机型号,并将罕见的型号字符串替换为通用字符串。
客户端分配的标识符
协议通常会允许客户端操控由 服务器在设计对象系统时,通常采用的方法 问题在于,要为每个连续的状态部分创建单独的对象, 服务器不过,在设计协议时,为每个协议使用单独的对象 状态具有几个缺点。
为每个逻辑对象创建单独的协议实例会消耗内核 因为每个实例都需要单独的渠道对象。 每个实例维护一个单独的 FIFO 消息队列。使用 对每个逻辑对象使用单独的实例意味着 可以相对于不同的对象进行重新排序,从而 客户端与服务器之间的无序交互。
客户端分配的标识符模式可避免这些问题,方法是
客户端为服务器保留的对象分配 uint32
或 uint64
标识符。
客户端和服务器之间交换的所有消息都通过漏斗
它通过单个协议实例提供一致的 FIFO 顺序
整个互动过程
让客户端(而不是服务器)分配标识符 前馈数据流,因为客户端可以为对象分配标识符 然后立即对该对象执行操作,而不必等待服务器 使用对象的标识符进行回复。在此格式中,标识符有效 而且通常只在当前连接范围内 作为标记。安全注意事项:客户端不得使用 用作标识符的标识符 可能会泄露地址空间的布局
客户端分配的标识符模式有一些缺点。例如: 撰写客户时更加困难,因为客户需要自行管理 标识符。开发者通常希望创建一个客户端库, 为服务提供面向对象的 Facade,以隐藏管理工作的复杂性 标识符,它本身是一种反模式(请参阅客户端 库)。
明确说明您应创建单独的协议实例 而不是使用客户端分配的标识符, 希望使用内核的对象功能系统来保护对该资源的访问 对象。例如,如果您希望客户端能够与 但您不希望客户端能够与其他对象进行交互, 创建单独的协议实例意味着您可以使用 一项功能来控制对该对象的访问权限。
命令并集
在使用前馈 Dataflow 的协议中,客户端通常会 在发送双向同步邮件之前将消息发送到服务器。如果 涉及到特别大量的消息, 可能更容易被注意到在这些情况下,可以考虑使用 命令联合模式将多个命令批量处理为一条消息。
在此模式中,客户端会发送命令 vector
,而不是发送
分别显示一条消息该向量包含
命令,而服务器会使用联合标记作为命令的选择器,
分派。
type PokeCmd = struct {
x int32;
y int32;
};
type ProdCmd = struct {
message string:64;
};
type MyCommand = strict union {
1: poke PokeCmd;
2: prod ProdCmd;
};
protocol HighVolumeSink {
Enqueue(struct {
commands vector<MyCommand>;
});
Commit() -> (struct {
result MyStatus;
});
};
通常,客户端会在其地址空间本地缓冲命令,并发送 将它们分批传送到服务器客户端应将批次刷新到服务器 之后才会达到以字节数和句柄数表示的通道容量限制。
对于具有更高消息量的协议,请考虑在
zx.Handle:VMO
表示数据平面,zx.Handle:FIFO
表示
控制平面此类协议会给客户端造成更大的实施负担
但适合需要最佳性能的情况。例如:
块设备协议就利用这种方法来优化性能。
分页
FIDL 消息通常通过通道发送,这些通道有消息数量上限 。在许多情况下,最大消息大小足以传输 但也有一些应用场景需要传输大量(或 数据量。一种用于传输大型或无界限数据的方法 使用分页模式。
将写入操作分页
对服务器的写入操作进行分页的一种简单方法是让客户端发送 将数据放在多条消息中,然后执行“finalize”指令方法,引发 服务器处理发送的数据:
protocol Foo1 {
AddBars(resource struct {
bars vector<client_end:Bar>;
});
UseTheBars() -> (struct {
args Args;
});
};
例如,fuchsia.process.Launcher
使用此模式可让
客户端发送任意数量的环境变量。
此模式的更复杂的版本会创建一个协议, 表示事务,通常称为拆解协议:
protocol BarTransaction {
Add(resource struct {
bars vector<client_end:Bar>;
});
Commit() -> (struct {
args Args;
});
};
protocol Foo2 {
StartBarTransaction(resource struct {
transaction server_end:BarTransaction;
});
};
当客户端可能正在执行许多操作时,此方法非常有用
同时将写入操作拆分为多个单独的消息会失去原子性。
请注意,BarTransaction
不需要 Abort
方法。越多越好
中止事务的方法是客户端关闭
BarTransaction
协议。
将读取操作分页
对来自服务器的读取操作进行分页的简单方法是让服务器将 使用事件为单个请求提供多个响应:
protocol EventBasedGetter {
GetBars();
-> OnBars(resource struct {
bars vector<client_end:Bar>;
});
-> OnBarsDone();
};
根据特定领域的语义,此模式可能还需要 第二个事件,在服务器完成数据发送时发出信号。这种方法 非常适合简单的情况,但具有许多扩缩问题。例如: 协议缺少流控制,如果存在以下情况,客户端将无法停止服务器: 客户不再需要其他数据(除非关闭整个 协议)。
一种更可靠的方法使用拆解协议来创建迭代器:
protocol BarIterator {
GetNext() -> (resource struct {
bars vector<client_end:Bar>;
});
};
protocol ChannelBasedGetter {
GetBars(resource struct {
iterator server_end:BarIterator;
});
};
调用 GetBars
后,客户端使用协议请求管道排队
第一个 GetNext
调用。此后,客户端会反复调用
GetNext
,用于从服务器读取其他数据,
未完成的 GetNext
消息,以提供流控制。请注意,
迭代器不需要“完成”因为服务器可以使用
然后关闭迭代器。
对读取操作进行分页的另一种方法是使用令牌。在此方法中, 服务器以不透明令牌的形式在客户端上存储迭代器状态, 客户端每次部分读取都会将令牌返回给服务器:
type Token = struct {
opaque array<uint8, 16>;
};
protocol TokenBasedGetter {
/// If token is null, fetch the first N entries. If token is not null,
/// return the N items starting at token. Returns as many entries as it can
/// in results and populates next_token if more entries are available.
GetEntries(struct {
token box<Token>;
}) -> (struct {
entries vector<Entry>;
next_token box<Token>;
});
};
当服务器可以托管其所有 分页状态传递给客户端,因此不再需要保留 分页状态。服务器应记录客户端能否 保留令牌并在协议实例之间重复使用。安全 注意:无论是哪种情况,服务器都必须验证客户端提供的令牌 以确保客户端的访问权限仅限于其自身的分页结果, 不包含针对其他客户端的结果。
事件对相关性
使用客户端分配的标识符时,客户端会识别由
使用仅在其自身环境中有意义的标识符,
连接到服务器。但是,某些使用场景需要将对象
。例如,在 fuchsia.ui.scenic
中,客户端主要与
场景图中的节点。不过,
如果要从其他进程导入节点,则需要将引用
跨进程边界。
事件对关联模式使用前馈神经网络来解决这个问题。
Dataflow 依赖内核来提供必要的安全性。首先,
希望导出对象的客户端创建一个 zx::eventpair
并发送一个
发送给服务器的事件及其客户端分配的标识符
对象。然后,客户端将另一个被纠结的事件发送给
该客户端会将事件转发到服务器,并由其自己的客户端分配
当前共享对象的标识符:
protocol Exporter {
ExportThing(resource struct {
client_assigned_id uint32;
export_token zx.Handle:EVENTPAIR;
});
};
protocol Importer {
ImportThing(resource struct {
some_other_client_assigned_id uint32;
import_token zx.Handle:EVENTPAIR;
});
};
要关联这些对象,服务器会调用 zx_object_get_info
和
ZX_INFO_HANDLE_BASIC
,并与以下位置中的 koid
和 related_koid
属性匹配:
相互纠缠的事件对象。
取消活动对
使用拆解协议事务时,客户端可以取消长时间运行的操作
关闭协议的客户端。服务器应该监听
ZX_CHANNEL_PEER_CLOSED
并中止事务以避免浪费资源。
没有专用渠道的操作也有类似的用例。
例如,fuchsia.net.http.Loader
协议有一个 Fetch
方法,
发起 HTTP 请求。服务器使用 HTTP 响应请求
一旦 HTTP 事务完成后返回响应,这可能需要
。除了
关闭整个 Loader
协议,这可能会取消许多其他未完成
请求。
事件对取消模式可解决此问题,让客户端
将 zx::eventpair
中的一个纠纷事件作为参数添加到
方法。然后,服务器会监听 ZX_EVENTPAIR_PEER_CLOSED
并取消
操作。使用 zx::eventpair
的效果要好于
使用 zx::event
或其他一些信号,因为 zx::eventpair
方法
隐式处理客户端崩溃或崩溃的情况。
当发生纠纷事件时,内核会生成 ZX_EVENTPAIR_PEER_CLOSED
将被销毁
空协议
有时,空协议可以提供值。例如,一个方法
创建的对象也可能会接收 server_end:FooController
参数。
调用方会提供此空协议的实现:
protocol FooController {};
FooController
不包含任何用于控制已创建的
对象,但服务器可以使用 ZX_CHANNEL_PEER_CLOSED
信号,
触发对象销毁。将来,协议
可能会通过控制所创建对象的方法进行扩展。
控制数据等设置
通常,服务器会公开可供客户端修改的设置。最好使用
table
来表示此类设置。例如,fuchsia.accessibility
库定义了:
type Settings = table {
1: magnification_enabled bool;
2: magnification_zoom_factor float32;
3: screen_reader_enabled bool;
4: color_inversion_enabled bool;
5: color_correction ColorCorrection;
6: color_adjustment_matrix array<float32, 9>;
};
(为了便于阅读,省略了注释。)
您可以通过多种方式让客户更改这些设置。
部分更新方法公开了接受部分数据的 Update
方法,
设置值,并仅在部分
值。
protocol TheManagerOfSomeSorts {
/// Description how the update modifies the behavior.
///
/// Only fields present in the settings value will be changed.
Update(struct {
settings Settings;
}) -> (struct {
args Args;
});
};
替换方法公开了 Replace
方法,用于完成
settings 值,并将设置更改为新提供的值。
protocol TheManagerOfOtherSorts {
/// Description how the override modifies the behavior.
///
/// This replaces the setting.
Replace(struct {
settings Settings;
}) -> (struct {
args Args;
});
};
需要避免的事项:
避免将动词
Set
或Override
用于部分更新或 替换方法,因为所提供的语义并不明确。避免通过单个方法来更新设置例如
SetMagnificationEnabled
。这种个人的方法对 而且调用方很少希望更新单个值。请避免移除具有特殊值的设置,例如
-1
。而应移除 缺少对应的设置字段。
引用联合变体和表字段
引用类型的字段通常很有用,例如引用一个或 多个字段或引用特定的联合变体。
假设某个 API 以包含许多字段的 table
形式提供元数据。如果
由于元数据可能会变得非常大,因此建立一种机制
以便收件人向发件人指明此元数据中的哪些字段
从而避免发送不必要的字段
收件人。在这种情况下,有一个其成员匹配的并行 bits
与 table
的字段进行一对一通信是构建
您的 API:
type MetadataFields = flexible bits {
VERSION = 0b1;
COUNT = 0b10;
};
type Metadata = table {
1: version string;
2: count uint32;
// ...
};
protocol MetadataGetter {
Get(struct {
fields MetadataFields;
}) -> (struct {
metadata Metadata;
});
};
现在,请考虑命令联合。在复杂的场景中,
服务器可能需要能够描述其支持的命令。在这种情况下,
有一个并行 enum
,其成员与
union
可以为构建 API 奠定坚实基础:
type MyCommandVariant = strict enum {
POKE = 1;
PROD = 2;
// ...
};
protocol HighVolumeSinkContinued {
SupportedCommands() -> (struct {
supported_commands vector<MyCommandVariant>;
});
};
请注意,尽管您可能很想使用 bits
值来表示集合
这会导致后续的选择更加困难,如果你
API 不断发展,使您需要引用特定命令,并具有 enum
这一点非常重要如果您开始时使用的是 bits
值,则现在面临
有两种错误选择之一:
引入了
enum
,这意味着现在可以通过两种方式引用 字段,还可能会在客户端代码中 相互表示);或继续使用
bits
,但只能在 然后映射回已设置的哪个特定位 太麻烦了
总的来说,对于 table
:
使用
table
的名称和后缀Fields
为bits
命名 (复数)。每个成员值都应该是序号索引处的位,即1 << (ordinal - 1)
。与针对
union
的建议类似,您希望实现灵活性bits
和table
之间,也就是说,因为 FIDL 仅支持灵活的 则bits
必须为flexible
。
对于 union
:
按
union
的名称为enum
命名列出所有变体 后缀Variant
(单数)。每个成员值都应为序数 其描述的变体的名称。匹配
union
和enum
之间的灵活性,例如,如果union
为strict
,则enum
也必须为strict
。
反模式
本部分介绍了几种反模式,即通常提供 负值。学习识别这些规律是 避免以错误的方式使用它们
已推送设置:请尽可能避免
Fuchsia 平台通常首选拉取语义。 组件提供了一个重要的示例;功能 假定是从组件中提取的,这使得组件启动可以延迟 从功能有向图推断组件关闭顺序 路由。
使用 FIDL 协议从 这是因为它们明显的简单性。这个 当组件 A 将政策推送到组件 B 以处理 B 的业务时,就会出现这种问题 逻辑。这会导致平台误解依赖关系: 为了支持 B 的功能而自动启动,A 可能会关闭 在 B 完成其业务逻辑执行之前。这进而会引领 涉及弱依赖项和幻象反向的解决方法 产生期望的行为;所有这一切都比政策 与推送相比
如果可能,最好采用可拉取(而非推送)政策的设计。
客户端库:请谨慎使用
理想情况下,客户端会使用 FIDL 中定义的协议 FIDL 编译器生成的特定语言的客户端库。 尽管这种方法使 Fuchsia 能够为大型应用提供高质量的支持, 目标语言的数量,有时该协议级别太低,无法直接进行编程。 在这种情况下,您可以提供一个手写客户端库, 同一底层协议的接口,但更易于使用。
例如,fuchsia.io
有一个客户端库 libfdio.so
,该库提供
类似于 POSIX 的协议前端。需要 POSIX 样式的客户端
open
/close
/read
/write
接口可以链接到 libfdio.so
并说话
fuchsia.io
协议,只需极少的修改。此客户端库
之所以能够提供价值,是因为该库可在现有的库接口之间进行调整
和底层 FIDL 协议。
框架也是另一种提供正值的客户端库。答
framework 是一个庞大的客户端库,为大型企业
部分。通常情况下,框架可以提供
可基于各种协议进行抽象化处理。例如,Flutter 是
一个框架,可视为适用于 Google Cloud 的
fuchsia.ui
协议。
无论 FIDL 协议是否具有 关联的客户端库一个独立的软件工程师小组应该 能够直接了解并正确使用协议, 定义,而无需对客户端库进行逆向工程。当 具有客户端库,而协议的底层和部分 足以激励您创建客户端库。 。
客户端库的主要问题是,需要针对 这往往意味着缺少客户端库(或者 降低质量)。客户端库也往往会导致 因为它们会导致每个客户端 创建服务器服务器会不断增长, 当客户偏离目标区域时,应用无法正常工作 客户端库使用的格式
要将客户端库添加到 Fuchsia SDK 中,我们应提供 至少以两种语言实现该库。
服务中心:请谨慎使用
服务中心是一种 Discoverable
协议,可让您轻松发现
其他协议的数量,通常具有明确的名称:
// BAD
@discoverable
protocol ServiceHub {
GetFoo(resource struct {
foo server_end:Foo;
});
GetBar(resource struct {
bar server_end:Bar;
});
GetBaz(resource struct {
baz server_end:Baz;
});
GetQux(resource struct {
qux server_end:Qux;
});
};
ServiceHub
协议不提供太多服务,尤其是在无状态的情况下。
而不是简单地将单个协议服务直接检测出来,这才是重点:
@discoverable
protocol Foo {};
@discoverable
protocol Bar {};
@discoverable
protocol Baz {};
@discoverable
protocol Qux {};
无论采用哪种方式,客户端都可以与枚举服务建立连接。 在后一种情况下,客户端可以通过常规 在整个系统中用于发现服务的机制。使用常规 机制可让核心平台对发现应用适当的政策。
不过,服务中心在某些情况下可能很有用。例如,如果 都是有状态的,或者是通过更细致的流程获得的, 那么协议就可以通过 状态更改为获取的服务。再举一个例子,如果 获取服务需要额外的参数,那么协议 通过在连接到 服务。
过度面向对象的设计:否
有些库会为 但这种方法也有一些缺点:
不同协议实例之间的消息排序未定义。 通过单个协议发送的消息按 FIFO 顺序(每个 方向),但是在不同通道上发送的消息会混淆。当 客户端与服务器之间的交互通过多个渠道进行, 而当消息出现意外情况时, 已重新排序。
每个协议实例都有内核资源方面的成本,等待 队列和调度。尽管 Fuchsia 设计为可扩容到 那么,整个系统的成本都会累加 对象的急剧扩增,以便为系统中的每个逻辑对象建模 给系统带来巨大的负担。
错误处理和拆解要复杂得多, 错误和拆解状态会随着协议数量的 所涉及的实例数使用单个协议时 客户端和服务器都可以彻底关闭交互 关闭协议使用多个协议实例时, 互动可能会进入部分关闭状态或 两方对关闭状态的看法不一致。
- 跨协议边界的协调比在单个 因为多个协议需要允许 不同的协议 客户,他们可能不会完全信任彼此。
不过,也有一些应用场景会将功能拆分为多个 协议:
提供单独的协议有助于提高安全性, 客户端可能只能访问其中一个协议 限制用户与服务器互动。
此外,从不同的线程中更轻松地使用单独的协议。对于 例如,一个协议可能绑定到一个线程,而另一个协议 可能绑定到另一个线程
客户端和服务器会为协议中的每种方法支付(少量)费用。 拥有一个包含所有可能方法的大型协议可以减少 比使用多个较小的协议更高效 每次需要较小的协议
有时,服务器保持的状态会按照方法进行彻底考虑 边界。在这些情况下,请考虑将该协议分解为更小的 同时沿这些相同的边界提供单独的协议, 与单独的状态交互。
避免过度面向对象的一个好方法是使用客户端分配的 来模拟协议中的逻辑对象。这种模式可让客户端 通过单个命令与可能很大的逻辑对象进行交互 协议。
使用魔法值指定缺失:no
我们通常希望指示服务器设置某种状态,但允许 移除状态以下代码使用魔法值来指示移除:
// BAD
protocol View3 {
ReplaceHitRegion(struct {
id uint64;
// Set to { -1, -1 } to remove.
size fuchsia.math.Rect;
});
ReplaceName(struct {
// Set to "" to remove.
name string;
});
};
不过,FIDL 针对许多数据类型提供了可选性。使用可选性会产生结果 更符合编程习惯的界面:
protocol View4 {
ReplaceHitRegion(struct {
id uint64;
size box<fuchsia.math.Rect>;
});
ReplaceName(struct {
name string:optional;
});
};
-
虽然 FIDL 类型系统是一种结构类型系统, 与 ABI 有关,也就是说,名称没有作用,只有类型的结构 很重要的一点是,FIDL 类型系统具有命名的类型语义, API。 ↩