QUIC-RPC:Rust分布式通信利器

454 阅读8分钟

Flutter+Rust的最强组合实践之路(专栏)

如何用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>,指定PatternServerStreaming模式

同时实现ServerStreamingMsg<RpcService>特性,指定StreamResponseArticleResponse

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方法按照定义需要返回一个泛型参数ItemArticleResponseStream

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的时候要加上类型声明

RpcServiceResponseRequestproto模块里定义的结构,同时泛型参数的顺序不要写反了。

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深度协作实践,欢迎关注和交流。