如何用FRB(flutter_rust_bridge)快速搭建Flutter+Rust混编项目
无惧阻塞!Flutter从Rust丝滑接收Stream数据流
关注专栏,一起学习。
00. 前言
RPC(Remote Procedure Call),即远程过程调用,是一种分布式通信协议,旨在将远程服务调用抽象为本地函数调用,屏蔽网络通信细节。
RPC 通过标准化接口定义和序列化协议,让开发者以直观的方式调用跨进程甚至跨主机的服务。
这种抽象极大降低了分布式系统开发的复杂度,提升了代码的可维护性和扩展性。
QUIC(Quick UDP Internet Connections)即快速 UDP 互联网连接,是谷歌公司研发的一种基于 UDP 的低时延的互联网传输层协议,后来被标准化为 RFC 9000。它融合了 TCP、TLS 和 HTTP/2 等协议的优势,旨在解决传统网络传输协议在性能、安全等方面的不足。
QUIC相比传统TCP有诸多优势和特性:
连接建立快速:QUIC 协议基于 UDP,使用 TLS 1.3 进行加密,能在首次连接时就完成密钥协商和数据传输,实现 0-RTT(0 往返时间)连接建立
抗网络抖动能力强:QUIC 协议内置了强大的拥塞控制和快速重传机制。
多路复用性能优:和 HTTP/2 类似,QUIC 支持多路复用,且由于在 UDP 上实现,避免了 TCP 队头阻塞问题。
灵活的流量控制:QUIC 的流量控制基于连接和流级别,能更精细地管理数据传输速度。
更好的移动性支持:当设备在不同网络(如从 WiFi 切换到 4G)间切换时,QUIC 能通过连接迁移特性,基于设备唯一标识快速恢复连接,而无需重新建立连接
安全性增强:将加密和认证功能作为协议的一部分,相比传统的在传输层之上叠加安全协议(如 HTTPS 是在 TCP 上叠加 TLS),减少了安全漏洞的风险,提供了更可靠的安全保障。
简化网络栈:将 TCP、TLS 和 HTTP/2 等协议的功能整合到一个协议中,减少了协议层之间的交互和适配工作,简化了网络栈的实现和管理。
01. QUIC-RPC初识
quic-rpc 是基于 QUIC 协议构建的 RPC(远程过程调用)框架,结合了 QUIC 协议特性与 RPC 的分布式调用优势,在网络通信和服务交互上有独特表现。
quic-rpc是一个基于quic的流式RPC系统,提供不仅仅是请求/响应式的RPC,还支持双向流式传输,类似于gRPC。
quic-rpc支持以下请求响应模式(1 req -> 1 res):客户端发送一个请求,服务端返回一个响应。这是最基本的RPC交互模式,一次请求对应一次响应。
(1 req, update stream -> 1 res):客户端发送一个请求,并且可以更新一个流,服务端返回一个响应。这种模式允许在请求过程中动态更新数据,但最终仍然只返回一个响应。
(1 req -> res stream):客户端发送一个请求,服务端返回一个响应流。这种模式适用于服务端需要持续发送数据给客户端的场景,例如实时数据推送。
(1 req, update stream -> res stream):客户端发送一个请求,并且可以更新一个流,服务端返回一个响应流。这种模式结合了动态更新和流式响应,适用于复杂的交互场景。
详细的介绍可以阅读:quic-rpc官方文档
02. 配置Cargo依赖
quic-rpc为主要依赖项,其他为常用开发辅助功能依赖项,有兴趣的可以查阅对应库文档了解。
[dependencies]
anyhow = "1.0.98"
async-stream = "0.3.6"
derive_more = { version = "1", features = ["from", "try_into", "display"] }
futures-buffered = "0.2.11"
futures-lite = "2.6.0"
futures-util = { version = "0.3.31"}
quic-rpc = { version = "0.20.0", features = ["quinn-transport", "macros", "test-utils"] }
serde = { version = "1.0.219", features = ["derive"] }
serde-error = "0.1.3"
tokio = { version = "1.45", features = ["full"] }
tokio-util = "0.7.15"
03. 定义proto模块
这个模块主要定义RPC协议的请求Request和响应Response结构对象的映射关系,以及RPC服务对象Service。
Request和Response都是
enum,服务端接收请求时可以方便的使用模式匹配处理具体的逻辑。
1 Req -> 1 Res模式为
VersionRequest请求结构实现RpcMsg<RpcService>特性
1 Req -> Stream Res模式为
ListArticlesRequest结构实现Msg<RpcService>,指定Pattern为ServerStreaming模式同时实现
ServerStreamingMsg<RpcService>特性,指定Stream的Response为ArticleResponse
proto.rs
use derive_more::{From, TryInto};
use quic_rpc::message::{Msg, RpcMsg, ServerStreaming, ServerStreamingMsg};
use quic_rpc::Service;
use serde::{Deserialize, Serialize};
// `1 Req -> 1 Res`
#[derive(Debug, Serialize, Deserialize)]
pub struct VersionRequest;
#[derive(Debug, Serialize, Deserialize)]
pub struct VersionResponse(pub String);
impl RpcMsg<RpcService> for VersionRequest {
type Response = VersionResponse;
}
// `1 Req -> Stream Res`
#[derive(Debug, Serialize, Deserialize)]
pub struct ListArticlesRequest;
#[derive(Debug, Serialize, Deserialize)]
pub struct ArticleResponse {
pub title: String,
}
impl Msg<RpcService> for ListArticlesRequest {
type Pattern = ServerStreaming;
}
impl ServerStreamingMsg<RpcService> for ListArticlesRequest {
type Response = ArticleResponse;
}
#[derive(Debug, Serialize, Deserialize, TryInto, From)]
pub enum Request {
Version(VersionRequest),
ListArticles(ListArticlesRequest),
}
#[derive(Debug, Serialize, Deserialize, TryInto, From)]
pub enum Response {
Version(VersionResponse),
Article(ArticleResponse),
}
#[derive(Debug, Clone)]
pub struct RpcService;
impl Service for RpcService{
type Req = Request;
type Res = Response;
}
/// Error type for RPC operations
pub type RpcError = serde_error::Error;
/// Result type for RPC operations
pub type RpcResult<T> = Result<T, RpcError>;
04. 实现RPC请求处理逻辑rpc模块
这个模块主要实现proto里对应定义的每个请求的处理逻辑:
Handler结构协调组织请求和处理逻辑分发
handle_rpc_request对接收到的msg请求进行模式匹配,并判断使用rpc还是server_streaming模式调用RPC处理过程。
quic-rpc一共实现了四种RPC处理模式,分别对应前边介绍的四种请求响应模式:
.rpc(1 req -> 1 res)
.client_streaming(1 req, update stream -> 1 res)
.server_streaming(1 req -> res stream)
.bidi_streaming(1 req, update stream -> res stream)
.rpc模式比较简单,get_version方法只需要按照proto里的定义返回指定结构对象即可.server_streaming复杂一些,list_articles方法按照定义需要返回一个泛型参数Item为ArticleResponse的Stream。
rpc.rs
use async_stream::stream;
use derive_more::{From};
use futures_lite::Stream;
use quic_rpc::server::{ChannelTypes, RpcChannel, RpcServerError};
use crate::proto::{ArticleResponse, ListArticlesRequest, Request, RpcService, VersionRequest, VersionResponse};
#[derive(Clone)]
pub struct Handler;
impl Handler {
pub async fn handle_rpc_request<C>(self, msg: Request, chan: RpcChannel<RpcService, C>) -> Result<(), RpcServerError<C>>
where
C: ChannelTypes<RpcService>
{
println!("Handling RPC request: {:?}", msg);
match msg {
Request::Version(ver)=> chan.rpc(ver, self, Self::get_version).await,
Request::ListArticles(req) => chan.server_streaming(req, self, Self::list_articles).await,
}
}
pub async fn get_version(self, _msg: VersionRequest) -> VersionResponse{
VersionResponse("0.0.1".to_string())
}
pub fn list_articles(self, _msg: ListArticlesRequest) -> impl Stream<Item = ArticleResponse> + Send + 'static {
let mut articles = vec![
String::from("Article 1"),
String::from("Article 2"),
String::from("Article 3"),
];
let articles_len = articles.len();
stream! {
for i in 0..articles_len {
yield ArticleResponse {
title: articles[i].clone(),
}
}
}
}
}
05. 服务端server模块和客户端client模块
a. 服务端server模块
server模块里除了启动服务循环接收请求的逻辑,还有通过CancellationToken以及Ctrl-C键盘事件触发退出服务的实现。
start_rpc_server函数通过在loop循环里轮询检测新请求到来以及退出事件实现可控的服务状态控制。
server.rs
use std::net::SocketAddr;
use std::time::Duration;
use quic_rpc::RpcServer;
use quic_rpc::transport::quinn::{make_server_endpoint, QuinnListener};
use tokio::time::sleep;
use tokio_util::sync::CancellationToken;
use crate::proto::{Request, Response, RpcService};
use crate::rpc::{Handler};
pub async fn check_cancellation_token(token: CancellationToken) -> bool {
token.is_cancelled()
}
pub async fn check_ctrl_c() -> bool {
match tokio::signal::ctrl_c().await {
Ok(_) => {
println!("catch ctrl-c");
true
},
Err(_) => {
false
}
}
}
pub async fn start_rpc_server(port: Option<usize>, cancellation_token: CancellationToken) -> anyhow::Result<()> {
let server_addr: SocketAddr = format!("127.0.0.1:{}", port.unwrap_or(12345)).parse()?;
let (server, _server_certs) = make_server_endpoint(server_addr)?;
let channel = QuinnListener::new(server)?;
println!("Starting RPC server on {}", server_addr);
let server = RpcServer::<RpcService, QuinnListener<Request, Response>>::new(channel.clone());
loop {
let ctrl_c_token = cancellation_token.clone();
tokio::select! {
biased;
res = server.accept() => {
let (req, chan) = res?.read_first().await?;
println!("Check accept request: {:?}", req);
let _ = Handler.handle_rpc_request(req, chan).await;
}
ctrl_c_triggered = check_ctrl_c() => {
println!("ctrl_c_triggered {:?}", ctrl_c_triggered);
if ctrl_c_triggered {
println!("Ctrl-C");
ctrl_c_token.cancel();
println!("Cancelled by ctrl-C");
break;
}
}
_ = sleep(Duration::from_secs(1)) => {}
}
let cancellation_token_cloned = cancellation_token.clone();
let cancelled = check_cancellation_token(cancellation_token_cloned).await;
if cancelled {
println!("Cancelled");
break;
}
}
Ok(())
}
b. 客户端client模块
client模块主要实现了创建RPC请求客户端对象的函数,通过指定端口建立一个连接到对应RPC服务器。
需要注意的地方是,初始化
QuinnConnector以及RpcClient的时候要加上类型声明
RpcService,Response和Request是proto模块里定义的结构,同时泛型参数的顺序不要写反了。
client.rs
use std::net::SocketAddr;
use quic_rpc::RpcClient;
use quic_rpc::transport::quinn::{make_insecure_client_endpoint, QuinnConnector};
use crate::proto::{Request, Response, RpcService};
use anyhow::Result;
pub async fn make_rpc_client(port: Option<usize>) -> Result<RpcClient<RpcService, QuinnConnector<Response, Request>>>{
let server_addr: SocketAddr = format!("127.0.0.1:{}", port.unwrap_or(12345)).parse()?;
let endpoint = make_insecure_client_endpoint("0.0.0.0:0".parse()?)?;
println!("Connecting to {}", server_addr);
let conn: QuinnConnector<Response, Request> = QuinnConnector::new(endpoint, server_addr, "localhost".to_string());
let client: RpcClient<RpcService, QuinnConnector<Response, Request>> = RpcClient::new(conn);
Ok(client)
}
06. 运行测试
测试程序把服务端启动和客户端发起请求写到了一个程序里,分别通过一个异步线程执行。 分离到两个程序里单独运行也是可以的,因为实际是通过网络端口通信的。
quic-rpc也提供了基于内存的服务端和客户端通信模式,性能更高延时更低,适用于单进程内通信场景。
main.rs
use std::time::Duration;
use tokio::time::sleep;
use tokio_util::sync::CancellationToken;
use crate::server::start_rpc_server;
use futures_buffered::BufferedStreamExt;
use futures_lite::StreamExt;
use futures_util::{FutureExt, Stream};
use crate::client::make_rpc_client;
use crate::proto::ListArticlesRequest;
#[tokio::main]
pub async fn main() -> anyhow::Result<()> {
let cancellation_token = CancellationToken::new();
let cancellation_token_cloned = cancellation_token.clone();
tokio::spawn(async move {
start_rpc_server(Some(23456), cancellation_token_cloned).await.expect("Failed to start RPC server");
});
sleep(Duration::from_secs(5)).await;
let cancellation_token_for_client = cancellation_token.clone();
tokio::spawn(async move {
let client = make_rpc_client(Some(23456)).await.unwrap();
let mut articles_stream = client.server_streaming(ListArticlesRequest).await.expect("Failed to get articles stream");
while let Some(res) = articles_stream.next().await {
match res {
Ok(article) => {
println!("Received article: {}", article.title);
},
Err(err) => {
eprintln!("Error receiving article: {}", err);
}
}
}
sleep(Duration::from_secs(15)).await;
cancellation_token_for_client.cancel();
});
let main_cancellation_token = cancellation_token.clone();
loop {
if main_cancellation_token.is_cancelled() {
println!("Main thread received cancellation signal");
break;
}
}
Ok(())
}
运行效果
Starting RPC server on 127.0.0.1:23456
Connecting to 127.0.0.1:23456
Check accept request: ListArticles(ListArticlesRequest)
Handling RPC request: ListArticles(ListArticlesRequest)
Received article: Article 1
Received article: Article 2
Received article: Article 3
Main thread received cancellation signal
07. 总结
RPC 作为分布式系统的核心通信技术,显著降低了跨进程调用的复杂性。 对于开发者而言,深入理解 RPC 原理与实践,将成为构建高性能分布式系统的必备技能。
本专栏专注Rust和Flutter深度协作实践,欢迎关注和交流。