6 个版本
新 0.1.5 | 2024年8月20日 |
---|---|
0.1.4 | 2024年8月20日 |
#452 in 网络编程
768 每月下载量
用于 lrcall-macro
255KB
4.5K SLoC
lrcall
lrcall
是一个兼容本地和远程过程调用的 Rust 过程调用框架。lrcall 专注于易用性。定义一个服务只需几行代码,并且服务器编写的大部分模板代码都由您处理。
基于 google/tarpc 的二次开发
什么是LPC?
"LPC" 代表 "本地过程调用",这是一个在本地执行返回值生成的工作的函数调用。
什么是RPC?
"RPC" 代表 "远程过程调用",这是一个在别处执行返回值生成的工作的函数调用。当调用 rpc 函数时,在幕后,该函数会联系某个其他进程并请求它们评估该函数。然后原始函数返回由其他进程生成的值。
lrcall的一些特性
- 客户端同时支持本地调用和远程调用,这意味着用户可以根据上下文动态切换调用方法。
- 在代码中定义模式,而不是使用如 .proto 这样的单独语言。
- 可插拔的传输:任何实现
Stream<Item = Request> + Sink<Response>
的类型都可以用作传输来连接客户端和服务器。 Send + 'static
可选:如果传输不需要它,lrcall 也不需要!- 级联取消:取消请求将向服务器发送取消消息。服务器将停止请求上的任何未完成的工作,随后取消其自身的任何请求,此过程会递归地对整个传递依赖链重复。
- 可配置的截止日期和截止日期传播:如果未指定,则请求截止日期默认为10秒。当截止日期过去后,服务器将自动停止工作。服务器发送的任何使用请求上下文的请求将传播请求截止日期。例如,如果服务器正在处理一个10秒的截止日期的请求,完成了2秒的工作,然后向另一个服务器发送请求,那个服务器将看到8秒的截止日期。
- 分布式跟踪:lrcall 使用 tracing 基本工具和扩展了 OpenTelemetry 跟踪的跟踪。使用兼容的跟踪订阅者,例如 OTLP,每个 RPC 都可以通过客户端、服务器以及服务器下游的其他依赖进行跟踪。即使对于未连接到分布式跟踪收集器的应用程序,跟踪工具也可以被常规的日志记录器,如 env_logger,所消费。
- Serde 序列化:启用
serde1
Cargo 功能将使服务请求和响应支持Serialize + Deserialize
。这是完全可选的:也可以使用内存传输,因此不需要时不需要支付序列化的代价。
使用方法
将依赖添加到您的 Cargo.toml
中
lrcall = "0.1"
lrcall::service
属性展开为一个形成 RPC 服务的项目集合。这些生成的类型使得编写服务器更加容易和高效。只需实现生成的服务特质,您就可以开始比赛了!
示例
此示例使用 tokio,因此请将以下依赖项添加到您的 Cargo.toml
anyhow = "1.0"
futures = "0.3"
lrcall = { version = "0.1", features = ["tokio1"] }
tokio = { version = "1.0", features = ["macros"] }
在以下示例中,我们使用进程内通道进行客户端和服务器之间的通信。在实际代码中,您很可能会通过网络进行通信。对于更真实的示例,请参阅 lrcall-example。
首先,让我们设置依赖关系和服务定义。
# extern crate futures;
use futures::{
prelude::*,
};
use lrcall::{
client, context,
server::{self, incoming::Incoming, Channel},
};
// This is the service definition. It looks a lot like a trait definition.
// It defines one RPC, hello, which takes one arg, name, and returns a String.
#[lrcall::service]
trait World {
/// Returns a greeting for name.
async fn hello(name: String) -> String;
}
此服务定义生成一个名为 World
的特质。接下来,我们需要为我们的 Server 结构体实现它。
# extern crate futures;
# use futures::{
# prelude::*,
# };
# use lrcall::{
# client, context,
# server::{self, incoming::Incoming},
# };
# // This is the service definition. It looks a lot like a trait definition.
# // It defines one RPC, hello, which takes one arg, name, and returns a String.
# #[lrcall::service]
# trait World {
# /// Returns a greeting for name.
# async fn hello(name: String) -> String;
# }
// This is the type that implements the generated World trait. It is the business logic
// and is used to start the server.
#[derive(Clone)]
struct HelloService;
impl World for HelloService {
// Each defined rpc generates an async fn that serves the RPC
async fn hello(self, _: context::Context, name: String) -> String {
format!("Hello, {name}!")
}
}
最后,让我们编写我们的 main
,该 main
将启动服务器。虽然此示例使用进程内通道,但 lrcall 还在 serde-transport
功能后面提供了一个通用的 serde_transport
,在 tcp
功能后面提供了额外的 TCP 功能。
# extern crate futures;
# use futures::{
# prelude::*,
# };
# use lrcall::{
# client, context,
# server::{self, Channel},
# };
# // This is the service definition. It looks a lot like a trait definition.
# // It defines one RPC, hello, which takes one arg, name, and returns a String.
# #[lrcall::service]
# trait World {
# /// Returns a greeting for name.
# async fn hello(name: String) -> String;
# }
# // This is the type that implements the generated World trait. It is the business logic
# // and is used to start the server.
# #[derive(Clone)]
# struct HelloService;
# impl World for HelloService {
// Each defined rpc generates an async fn that serves the RPC
# async fn hello(self, _: context::Context, name: String) -> String {
# format!("Hello, {name}!")
# }
# }
# #[cfg(not(feature = "tokio1"))]
# fn main() {}
# #[cfg(feature = "tokio1")]
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let (client_transport, server_transport) = lrcall::transport::channel::unbounded();
let server = server::BaseChannel::with_defaults(server_transport);
tokio::spawn(
server.execute(HelloService.serve())
// Handle all requests concurrently.
.for_each(|response| async move {
tokio::spawn(response);
}));
// WorldClient is generated by the #[lrcall::service] attribute. It has a constructor `new`
// that takes a config and any Transport as input.
let mut client = WorldClient::<HelloService>::rpc_client(WorldChannel::spawn(client::Config::default(), client_transport));
// The client has an RPC method for each RPC defined in the annotated trait. It takes the same
// args as defined, with the addition of a Context, which is always the first arg. The Context
// specifies a deadline and trace information which can be helpful in debugging requests.
let hello = client.hello(context::rpc_current(), "Andeya".to_string()).await?;
println!("{hello}");
Ok(())
}
依赖项
~7–17MB
~198K SLoC