1

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

  • 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 错误处理对象成功的统一了请求响应。现在我们为这两种类型的请求提供一个统一的类型。🎉庆祝!
阅读原文


道之
10 声望1 粉丝