如何在同一个监听器上复用Axum与Tonic服务?
如何在同一个监听器上复用Axum与Tonic服务?
哈哈,我之前刚好折腾过这个需求!Axum和Tonic都是基于Hyper构建的,所以咱们可以利用Hyper的服务能力做请求分发,直接在同一个端口上跑两者,不用分开监听两个地址。咱们一步步来实现:
首先,核心思路就是用Hyper的service_fn写一个“请求路由器”,判断每个进来的请求该交给Axum处理还是Tonic处理。下面是完整的可运行示例代码,我给你加了注释,一看就懂:
use axum::{Router, Server as AxumServer}; use tonic::transport::Server as TonicServer; use hyper::{Request, Response, Body, service::service_fn}; use std::convert::Infallible; use std::sync::Arc; use std::time::Duration; // 先把你的gRPC服务定义好(示例用了官方的helloworld proto) mod grpc { tonic::include_proto!("helloworld"); #[derive(Default)] pub struct MyGreeter; #[tonic::async_trait] impl super::grpc::greeter_server::Greeter for MyGreeter { async fn say_hello( &self, request: tonic::Request<super::grpc::HelloRequest>, ) -> Result<tonic::Response<super::grpc::HelloReply>, tonic::Status> { let reply = super::grpc::HelloReply { message: format!("Hello {}!", request.into_inner().name).into(), }; Ok(tonic::Response::new(reply)) } } } // 你的应用状态,和原来的定义保持一致 #[derive(Clone)] struct AppState { rate_limiter: Arc<RateLimiter>, } // 简化的限流器实现,你可以替换成自己的业务逻辑 struct RateLimiter { max_requests: u32, window: Duration, } impl RateLimiter { fn new(max_requests: u32, window: Duration) -> Self { Self { max_requests, window } } } #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { // 初始化应用状态 let state = AppState { rate_limiter: Arc::new(RateLimiter::new(10, Duration::from_secs(60))), }; // 1. 构建Tonic的gRPC服务,并转换为Hyper兼容的Service let greeter_service = grpc::MyGreeter::default(); let grpc_service = TonicServer::builder() .add_service(grpc::greeter_server::GreeterServer::new(greeter_service)) .into_service(); // 2. 构建Axum的HTTP服务,同样转换为Hyper兼容的Service let axum_app = Router::new() .route("/health", axum::routing::get(|| async { "OK" })) // 示例Axum路由 .with_state(state); let axum_service = axum_app.into_make_service(); // 3. 核心:实现请求分发逻辑 let shared_service = service_fn(move |req: Request<Body>| { // 克隆服务实例,因为service_fn会被并发调用 let axum_svc = axum_service.clone(); let grpc_svc = grpc_service.clone(); async move { // 两种判断方式选一种即可: // 方式1:通过Content-Type判断(标准gRPC请求的Content-Type是application/grpc) let is_grpc = req.headers() .get(hyper::header::CONTENT_TYPE) .map(|ct| ct.as_bytes().starts_with(b"application/grpc")) .unwrap_or(false); // 方式2:通过请求路径判断(gRPC方法的固定路径格式是/包名.服务名/方法名) // let is_grpc = req.uri().path().starts_with("/helloworld.Greeter/"); // 根据判断结果分发请求 let response = if is_grpc { // 交给Tonic处理gRPC请求 grpc_svc.call(req).await.map_err(|e| { eprintln!("gRPC服务出错: {}", e); Infallible::default() }) } else { // 交给Axum处理HTTP请求 axum_svc.call(req).await }; response } }); // 4. 绑定地址并启动服务 let addr = ([127, 0, 0, 1], 50051).into(); println!("服务已启动,监听地址: http://{}", addr); AxumServer::bind(&addr) .serve(shared_service) .await?; Ok(()) }
再给你补充几个关键注意点:
- 版本兼容性:一定要确保Axum、Tonic和Hyper的版本匹配,比如Axum 0.7对应Hyper 1.x,Tonic 0.11也是基于Hyper 1.x,版本不兼容会出现各种奇怪的编译或运行错误。
- 分发逻辑选择:如果你的gRPC服务有固定的路径前缀,用路径判断会比Content-Type更可靠——毕竟有些客户端可能会在Content-Type里加额外参数(比如
application/grpc+json)。 - 状态共享:Axum用
with_state传递全局状态,Tonic的服务如果需要共享状态,直接在你的服务结构体(比如示例里的MyGreeter)中添加字段,初始化时把状态传进去就行。
这样配置好之后,你就可以在http://127.0.0.1:50051/health访问Axum的健康检查接口,同时用gRPC客户端调用helloworld.Greeter/SayHello方法,两者完全共用同一个端口~
内容来源于stack exchange
相关产品推荐
相关产品推荐

