关于后端:译集成Axum-Hyper-Tonic-and-Tower-做webgRPC混合应用04

37次阅读

共计 6775 个字符,预计需要花费 17 分钟才能阅读完成。

这是系列的第四篇,总的四个局部如下:

  • Tower 概览
  • Hyper 原理和 Axum 初试
  • Tonic gRPC 客户端和服务端示范
  • 如何把 Axum 和 Tonic 服务集成到一个利用中
单端口,两个协定

下面题目开个玩笑。Axum 站点利用和 gRPC 服务用的都是同一个 HTTP/ 2 协定。可能更正当的是他们说不同的方言。然而,重要的是,查看申请和查看是否与 gPRC 通信通常是比较简单的。gRPC 申请头都包含 Content-Type: application/grpc; 所以咱们明天要实现的是写一些货色承受 gPRC 申请和一般 Service,返回对立的服务。让咱们开始吧!源码可参考

然咱们从 main 函数开始,来展现咱们最终要实现的样子:

#[tokio::main]
async fn main() {let addr = SocketAddr::from(([0, 0, 0, 0], 3000));

    let axum_make_service = axum::Router::new()
        .route("/", axum::handler::get(|| async { "Hello world!"}))
        .into_make_service();

    let grpc_service = tonic::transport::Server::builder()
        .add_service(EchoServer::new(MyEcho))
        .into_service();

    let hybrid_make_service = hybrid(axum_make_service, grpc_service);

    let server = hyper::Server::bind(&addr).serve(hybrid_make_service);

    if let Err(e) = server.await {eprintln!("server error: {}", e);
    }
}

咱们创立了一个繁难的 axum_make_service 和 grpc_service 值,而后用 hybrid 函数组合成一个服务。留神不同的办法名称,实际上咱们开始用的是 into_make_service,起初用的是 into_service,不可否认,这将给咱们带来很多苦楚。

无论如何,只管 hybrid 函数有待解释,启动一个 hybrid 服务是小菜一碟;然而,细节决定成败。

当然:有更简略的形式应用 trait 对象解决上面代码。我防止任何类型代替技术:1. 我认为这样能够使代码逻辑更清晰。2. 在我心中,这样能够使教程更丰盛。惟一例外的是,我正在应用谬误 trait 对象,因为 Hyper 本身也这样做的。这样更简化了跨服务以雷同的谬误示意的代码。

定义 hybrid

咱们的 hybrid 函数返回 HybridMakeService:

fn hybrid<MakeWeb, Grpc>(make_web: MakeWeb, grpc: Grpc) -> HybridMakeService<MakeWeb, Grpc> {HybridMakeService { make_web, grpc}
}

struct HybridMakeService<MakeWeb, Grpc> {
    make_web: MakeWeb,
    grpc: Grpc,
}

整个过程中变量名称将保持一致和具体。这里,咱们有 MakeWeb 和 Grpc 类型。这反映了 Axum 和 Tonic 提供 API 的区别。咱们须要提供链接信息给 Axum’s MakeWeb 来取得申请解决的 Service.gRPC, 则不须要这么做。

在任何状况下,咱们筹备实现咱们 `HybridMakeService 的 Service:

impl<ConnInfo, MakeWeb, Grpc> Service<ConnInfo> for HybridMakeService<MakeWeb, Grpc>
where
    MakeWeb: Service<ConnInfo>,
    Grpc: Clone,
{// ...}

咱们有两个预期的类型变量 MakeWebGrpc,以及 ConnInfo,来示意咱们给出的任何连贯信息。Grpc 基本不在乎,但 ConnInfo 必须与 MakeWeb 接管的内容相匹配。因而,咱们绑定MakeWeb: Service<ConnInfo> 和Grpc: Clone` 很快就会有意义。

当咱们收到传入的连贯时,咱们须要做两件事:

  • 获取新 ServiceMakeWeb。这样做可能是异步产生,也可能会呈现一些谬误。

    • 注:如果您还记得 Axum 的理论实现,咱们就会晓得这些都不是真的。Service从 Axum 获取 aIntoMakeService总是会胜利,并且永远不会做任何异步工作。然而 Axum 中没有 API 公开这个事实,所以咱们被困在ServiceAPI 前面。
  • 克隆咱们曾经领有的 gRPC。

一旦咱们有了新的 Web Service 和克隆的 Grpc,咱们将把它们包装成一个新的 HybridService 构造体。咱们还须要一些帮忙来执行必要的异步操作,因而咱们将创立一个新的Future 类型助手。将看起来像:

type Response = HybridService<MakeWeb::Response, Grpc>;
type Error = MakeWeb::Error;
type Future = HybridMakeServiceFuture<MakeWeb::Future, Grpc>;

fn poll_ready(
    &mut self,
    cx: &mut std::task::Context,
) -> std::task::Poll<Result<(), Self::Error>> {self.make_web.poll_ready(cx)
}

fn call(&mut self, conn_info: ConnInfo) -> Self::Future {
    HybridMakeServiceFuture {web_future: self.make_web.call(conn_info),
        grpc: Some(self.grpc.clone()),
    }
}

请留神,咱们推延 self.make_web 准备就绪并传递其谬误。让咱们通过查看 HybridMakeServiceFuture`:

#[pin_project]
struct HybridMakeServiceFuture<WebFuture, Grpc> {#[pin]
    web_future: WebFuture,
    grpc: Option<Grpc>,
}

impl<WebFuture, Web, WebError, Grpc> Future for HybridMakeServiceFuture<WebFuture, Grpc>
where
    WebFuture: Future<Output = Result<Web, WebError>>,
{
    type Output = Result<HybridService<Web, Grpc>, WebError>;

    fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context) -> Poll<Self::Output> {let this = self.project();
        match this.web_future.poll(cx) {
            Poll::Pending => Poll::Pending,
            Poll::Ready(Err(e)) => Poll::Ready(Err(e)),
            Poll::Ready(Ok(web)) => Poll::Ready(Ok(HybridService {
                web,
                grpc: this.grpc.take().expect("Cannot poll twice!"),
            })),
        }
    }
}

咱们须要将 pin_project 引入以容许咱们在异步轮询外部实现中将我的项目中 future 固定。(如果你不相熟pin_project,请不要放心,咱们稍后会在 HybridFuture 中形容)当咱们轮询时web_future,咱们可能会处于以下三种状态之一:

  • Pending:MakeWeb还没筹备好,所以咱们也没筹备好
  • Ready(Err(e))MakeWeb失败,所以咱们传递谬误
  • Ready(Ok(web)):MakeWeb胜利了,所以把新 web 值和 grpc 值打包

this.grpc.take()从 Option 类型值中失去 gRPC 值过程中有一些乏味的事件. Futures 有一个不变量,一旦它们返回 Ready,就不能再次轮询。因而,能够平安地假如它take 只会被调用一次。然而,如果 Axum 公开一种 into_service 办法,则能够防止这种苦楚。

HybridService

后面最终会产生一个 HybridService 类型. 让咱们看看:

struct HybridService<Web, Grpc> {
    web: Web,
    grpc: Grpc,
}

impl<Web, Grpc, WebBody, GrpcBody> Service<Request<Body>> for HybridService<Web, Grpc>
where
    Web: Service<Request<Body>, Response = Response<WebBody>>,
    Grpc: Service<Request<Body>, Response = Response<GrpcBody>>,
    Web::Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
    Grpc::Error: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
{// ...}

HybridServiceRequest<Body> 作为参数。WebGrpc 也将 Request<Body> 作为参数,但他们返回值略有不同:前者 Response<WebBody> 后者Response<GrpcBody>。咱们须要以某种形式对立返回值。如上所述,咱们将应用 trait 对象进行错误处理,因而不须要对立。

type Response = Response<HybridBody<WebBody, GrpcBody>>;
type Error = Box<dyn std::error::Error + Send + Sync + 'static>;
type Future = HybridFuture<Web::Future, Grpc::Future>;

关联 Response 类型也是 Response<...>,但它的主体将是 HybridBody<WebBody, GrpcBody> 类型。咱们稍后再谈。相似地,咱们有两个不同的 Futures 可能会被调用,具体取决于申请的类型。咱们须要用一个HybridFuture 类型来对立它。

接下来,咱们来看看 poll_ready。咱们须要查看两者WebGrpc为新申请做好筹备。而且每个查看可能会导致以下三种状况之一:PendingReady(Err)Ready(Ok)。这个函数是对于模式匹配和应用.into() 以下办法对立谬误示意:

fn poll_ready(
    &mut self,
    cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {match self.web.poll_ready(cx) {Poll::Ready(Ok(())) => match self.grpc.poll_ready(cx) {Poll::Ready(Ok(())) => Poll::Ready(Ok(())),
            Poll::Ready(Err(e)) => Poll::Ready(Err(e.into())),
            Poll::Pending => Poll::Pending,
        },
        Poll::Ready(Err(e)) => Poll::Ready(Err(e.into())),
        Poll::Pending => Poll::Pending,
    }
}

最初,咱们看看 call 实现,咱们试图实现的真正逻辑。这是咱们查看申请并确定将其路由到何处的中央:

fn call(&mut self, req: Request<Body>) -> Self::Future {if req.headers().get("content-type").map(|x| x.as_bytes()) == Some(b"application/grpc") {HybridFuture::Grpc(self.grpc.call(req))
    } else {HybridFuture::Web(self.web.call(req))
    }
}

惊人的。所有这些工作基本上只须要 5 行有意义的代码!

HybridFuture

就这样,咱们到了最初!咱们将在本系列中剖析的最初一种类型是 HybridFuture.(还有一种HybridBody 类型,和 HybridFuture 很类似,咱们就不解释了。)struct的定义是:

#[pin_project(project = HybridFutureProj)]
enum HybridFuture<WebFuture, GrpcFuture> {Web(#[pin] WebFuture),
    Grpc(#[pin] GrpcFuture),
}

像以前一样,咱们应用 pin_project. 这一次,让咱们来探索一下起因。Futuretrait 接口须要内存中的固定指针。具体来说,第一个参数pollself: Pin<&mut Self>。Rust 自身从不提供任何对于对象持久性的保障,但这对于编写异步运行时零碎相对至关重要。

HybridFuture 的 poll 办法上因而将接管 Pin<&mut HybridFuture> 类型的参数。问题是 WebBody 或 GrpcBody 须要调用 poll。假如咱们有Web 变体,咱们面临的问题是模式匹配 HybridFuture 会给咱们一个&WebFutureor &mut WebFuture。它不会给咱们一个Pin<&mut WebFuture>,这正是咱们所须要的!

pin_project生成固定数据类型并用 .project() 在原始数据类型上提供一种办法,为咱们提供固定的可变援用。这使咱们可能正确地实现Futuretrait HybridFuture,如下所示:

impl<WebFuture, GrpcFuture, WebBody, GrpcBody, WebError, GrpcError> Future
    for HybridFuture<WebFuture, GrpcFuture>
where
    WebFuture: Future<Output = Result<Response<WebBody>, WebError>>,
    GrpcFuture: Future<Output = Result<Response<GrpcBody>, GrpcError>>,
    WebError: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
    GrpcError: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
{
    type Output = Result<
        Response<HybridBody<WebBody, GrpcBody>>,
        Box<dyn std::error::Error + Send + Sync + 'static>,
    >;

    fn poll(self: Pin<&mut Self>, cx: &mut std::task::Context) -> Poll<Self::Output> {match self.project() {HybridFutureProj::Web(a) => match a.poll(cx) {Poll::Ready(Ok(res)) => Poll::Ready(Ok(res.map(HybridBody::Web))),
                Poll::Ready(Err(e)) => Poll::Ready(Err(e.into())),
                Poll::Pending => Poll::Pending,
            },
            HybridFutureProj::Grpc(b) => match b.poll(cx) {Poll::Ready(Ok(res)) => Poll::Ready(Ok(res.map(HybridBody::Grpc))),
                Poll::Ready(Err(e)) => Poll::Ready(Err(e.into())),
                Poll::Pending => Poll::Pending,
            },
        }
    }
}

咱们用 HybridBody enum 和 trait 错误处理对象胜利的对立了申请响应。当初咱们为这两种类型的申请提供一个对立的类型。🎉庆贺!
浏览原文

正文完
 0