本页译自英文,由模型翻译。若与英文原文存在差异,以英文原文为准。
快速开始
本指南安装 ruststream-sea-file,在流文件上运行第一个服务,再把同一个处理器放进 shell 管道。
两种传输都不需要服务器。
安装
[dependencies]
ruststream = { version = "0.7", features = ["macros", "json", "logging"] }
ruststream-sea-file = "0.7"
serde = { version = "1", features = ["derive"] }
tracing = "0.1"
[dev-dependencies]
ruststream-sea-file = { version = "0.7", features = ["testing"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
ruststream 是框架本身:macros 提供属性宏,json 提供默认编解码器,logging 把服务的日志打印到终端。
ruststream-sea-file 包含两种传输。
它的 testing feature 加入测试所用的进程内传输,asyncapi 加入本 crate 为 AsyncAPI 文档提供的内容。
这两个 feature 默认都不启用。
流文件客户端只能在 Unix 系统上构建,因此本 crate 无法在 Windows 上构建。
第一个服务
- 宏
- 手写
use ruststream_sea_file::file::prelude::*;
use serde::Deserialize;
#[derive(Debug, PartialEq, Deserialize)]
struct Order {
id: u64,
amount_cents: u64,
}
#[subscriber(FileStream::new("orders"), start_at(FilePosition::beginning()))]
async fn take_order(order: &Order) -> HandlerOutcome {
if order.amount_cents == 0 {
tracing::warn!(order.id, "order without an amount dropped");
return HandlerOutcome::drop();
}
tracing::info!(order.id, order.amount_cents, "order taken");
HandlerOutcome::ack()
}
#[ruststream::app]
fn app() -> impl App {
RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
FileBroker::new("/tmp/orders.ss"),
|b| {
b.include(take_order);
},
)
}
use ruststream_sea_file::file::prelude::*;
use serde::Deserialize;
#[derive(Debug, PartialEq, Deserialize, JsonSchema)]
#[schemars(crate = "ruststream::schemars")]
struct Order {
id: u64,
amount_cents: u64,
}
struct TakeOrder;
impl Handle<Order> for TakeOrder {
fn handle(
&self,
order: &Order,
_outs: &(),
_ctx: &mut Context<'_>,
) -> impl Future<Output = Result<(), HandlerOutcome>> {
if order.amount_cents == 0 {
tracing::warn!(order.id, "order without an amount dropped");
return ready(Err(HandlerOutcome::drop()));
}
tracing::info!(order.id, order.amount_cents, "order taken");
ready(Ok(()))
}
}
fn app() -> impl App {
RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(
FileBroker::new("/tmp/orders.ss"),
|b| {
b.include(
subscriber(FileStream::new("orders"), TakeOrder)
.start_at(FilePosition::beginning())
.build(),
);
},
)
}
fn main() -> ExitCode {
run_main(app)
}
take_order 读取文件 /tmp/orders.ss 中流键 orders 下的 JSON 订单。
带金额的订单被 ack,没有金额的订单被丢弃。
FileBroker::new 只记录路径。
文件在服务启动时才打开,文件不存在时,启动过程会创建它。
FileStream::new 指定流键,start_at(FilePosition::beginning()) 让订阅从文件开头而不是末尾读取该键。
#[ruststream::app] 把 app 变成 main;在手写形式中,run_main 做同样的事。
运行服务:
cargo run -- run
服务读取文件中已有的每个订单,然后等待下一个。
另一个服务在同一路径上以 orders 发布订单,就会把它追加到文件中,磁盘上的事件日志展示了这一点。
Ctrl+C 停止服务。
文件内部
文件客户端自带一个解码器,它把流文件按每行一条消息打印出来:
cargo install sea-streamer-file --features=executables --bin ss-decode
ss-decode --file /tmp/orders.ss
每一行的形式是 [timestamp | stream key | sequence | shard] payload,例如 [2026-10-18T09:12:03.120 | orders | 1 | 0] {"id":1,"amount_cents":1250}。
一个文件容纳所有流键,每个键的序号从一开始。
新文件中还有一条 ruststream-internal 键下的消息,由本 crate 在创建文件时写入。
第一个测试
示例文件里还有两个测试。
每个测试都用 TestApp::start 启动同一个 app,发布一个订单,然后检查 take_order 如何结算它。
在测试中,FileBroker 在进程内运行,所以测试不接触任何文件:
cargo test
测试文件或管道服务逐一讲解测试套件,以及进程内传输模拟了什么。
管道中的同一个处理器
另一种传输读取标准输入,写入标准输出。 处理器保持不变,改变的是 prelude、Broker 和订阅。
- 宏
- 手写
use ruststream_sea_file::stdio::prelude::*;
use serde::Deserialize;
#[derive(Debug, PartialEq, Deserialize)]
struct Order {
id: u64,
amount_cents: u64,
}
#[subscriber("orders")]
async fn take_order(order: &Order) -> HandlerOutcome {
if order.amount_cents == 0 {
tracing::warn!(order.id, "order without an amount dropped");
return HandlerOutcome::drop();
}
tracing::info!(order.id, order.amount_cents, "order taken");
HandlerOutcome::ack()
}
#[ruststream::app]
fn app() -> impl App {
RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(StdioBroker::new(), |b| {
b.include(take_order).out_retry(Publish).to("orders.retry");
})
}
use ruststream_sea_file::stdio::prelude::*;
use serde::Deserialize;
#[derive(Debug, PartialEq, Deserialize, JsonSchema)]
#[schemars(crate = "ruststream::schemars")]
struct Order {
id: u64,
amount_cents: u64,
}
struct TakeOrder;
impl Handle<Order> for TakeOrder {
fn handle(
&self,
order: &Order,
_outs: &(),
_ctx: &mut Context<'_>,
) -> impl Future<Output = Result<(), HandlerOutcome>> {
if order.amount_cents == 0 {
tracing::warn!(order.id, "order without an amount dropped");
return ready(Err(HandlerOutcome::drop()));
}
tracing::info!(order.id, order.amount_cents, "order taken");
ready(Ok(()))
}
}
fn app() -> impl App {
RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker(StdioBroker::new(), |b| {
b.include(subscriber("orders", TakeOrder).build())
.out_retry(Publish)
.to("orders.retry");
})
}
fn main() -> ExitCode {
run_main(app)
}
StdioBroker::new 不接收任何设置,订阅只指定流键。
管道的一级用 out_retry(Publish).to(..) 指定延迟副本发往的键,没有它就不会启动。
两个 prelude 中的发布策略都叫 Publish,所以负责发布的代码在两种传输上写法相同。
给这一级输入一行:
echo '[orders] {"id":1,"amount_cents":1250}' | cargo run -- run
方括号指定流键。 日志写到标准错误,因为标准输出把这一级的消息送往下一个进程。 shell 管道讲解行格式和多级串联。
从 sea-streamer 迁移
基于 sea-streamer-file 的服务要自己编写消费者循环和生产者。
下表列出每一部分在 RustStream 中的位置:
| sea-streamer | RustStream |
|---|---|
FileStreamer::connect 加上 FileConnectOptions | FileBroker::new 及其设置,在启动时生效 |
create_consumer 加一个 next() 循环 | FileStream 上的 #[subscriber] 处理器 |
| 解码消息载荷 | 处理器的参数类型,由编解码器解码 |
create_producer 和 send_to | 回复或 Out 参数,经由 Publish 发送 |
在消费者选项中调用 set_auto_stream_reset,再调用 seek_to | 挂载点上的 start_at,或处理器中的定位句柄(恢复与定位) |
StdioStreamer | StdioBroker |