在Rust中使用RabbitMQ的方法

1,604 阅读5分钟

在这篇文章中,我们将看看如何使用warp与 RabbitMQ 集成一个 Rust Web 应用程序。为此,我们将使用lapin库以及用于池化连接的deadpool

我们将构建的示例非常简单。有一个端点,您可以发送消息,然后将其发送到 RabbitMQ 实例。同时,该服务会监听新的事件,在它们从队列中取出时对其进行记录。

示例

让我们从一些类型开始。

type WebResult<T> = StdResult<T, Rejection>;
type RMQResult<T> = StdResult<T, PoolError>;
type Result<T> = StdResult<T, Error>;

type Connection = deadpool::managed::Object<deadpool_lapin::Manager>;

#[derive(ThisError, Debug)]
enum Error {
    #[error("rmq error: {0}")]
    RMQError(#[from] lapin::Error),
    #[error("rmq pool error: {0}")]
    RMQPoolError(#[from] PoolError),
}

impl warp::reject::Reject for Error {}

这些只是一些辅助类型,所以我们不必输入那么多,还有一个自定义的 Error 类型,它实现了Reject ,所以我们可以用它来返回来自warp 端点的错误。

接下来让我们看看main

#[tokio::main]
async fn main() -> Result<()> {
    let addr = std::env::var("AMQP_ADDR")
        .unwrap_or_else(|_| "amqp://rmq:rmq@127.0.0.1:5672/%2f".into());
    let manager = Manager::new(addr, ConnectionProperties::default().with_tokio());
    let pool: Pool = deadpool::managed::Pool::builder(manager)
        .max_size(10)
        .build()
        .expect("can create pool");

    let health_route = warp::path!("health").and_then(health_handler);
    let add_msg_route = warp::path!("msg")
        .and(warp::post())
        .and(with_rmq(pool.clone()))
        .and_then(add_msg_handler);

    let routes = health_route
        .or(add_msg_route);

    println!("Started server at localhost:8000");
    let _ = join!(
        warp::serve(routes).run(([0, 0, 0, 0], 8000)),
        rmq_listen(pool.clone())
    );
    Ok(())
}

在这个片段中,我们已经可以看到应用程序的基本结构。首先,我们使用deadpool 创建一个 RabbitMQ 连接池。然后,我们定义一些路由 - 一个作为/health 的端点,另一个用于将消息发送到/msg 的队列。

main 的末尾,我们启动warp 网络服务器,同时,我们使用futures::join! 来启动rmq_listen 函数。这两个期货将一直运行,直到其中一个完成,正如我们将看到的,只有在出现错误时才会出现这种情况。

接下来让我们看看rmq_listen

async fn rmq_listen(pool: Pool) -> Result<()> {
    let mut retry_interval = tokio::time::interval(Duration::from_secs(5));
    loop {
        retry_interval.tick().await;
        println!("connecting rmq consumer...");
        match init_rmq_listen(pool.clone()).await {
            Ok(_) => println!("rmq listen returned"),
            Err(e) => eprintln!("rmq listen had an error: {}", e),
        };
    }
}

从高层次来看,我们希望rmq_listen 在网络服务运行时一直运行。因此,例如,如果 RabbitMQ 连接死亡,或者在连接运行期间发生其他错误,我们希望重新连接并继续运行。出于这个原因,实际逻辑被上述重试逻辑所包裹。

init_rmq_listen 中,我们实际上听取了 RabbitMQ 事件。

async fn init_rmq_listen(pool: Pool) -> Result<()> {
    let rmq_con = get_rmq_con(pool).await.map_err(|e| {
        eprintln!("could not get rmq con: {}", e);
        e
    })?;
    let channel = rmq_con.create_channel().await?;

    let queue = channel
        .queue_declare(
            "hello",
            QueueDeclareOptions::default(),
            FieldTable::default(),
        )
        .await?;
    println!("Declared queue {:?}", queue);

    let mut consumer = channel
        .basic_consume(
            "hello",
            "my_consumer",
            BasicConsumeOptions::default(),
            FieldTable::default(),
        )
        .await?;

    println!("rmq consumer connected, waiting for messages");
    while let Some(delivery) = consumer.next().await {
        if let Ok((channel, delivery)) = delivery {
            println!("received msg: {:?}", delivery);
            channel
                .basic_ack(delivery.delivery_tag, BasicAckOptions::default())
                .await?
        }
    }
    Ok(())
}

首先,我们从池中获得一个 RabbitMQ 连接,创建一个通道并确保我们想要监听的队列存在。然后我们为这个队列创建一个消费者。

这个消费者实际上是一个Stream ,所以在函数的最后,我们简单地迭代流。如果任何操作失败,或者没有任何东西可以从流中得到,外部的rmq_listen 函数将重新启动这个过程。如果我们收到一条消息,我们只需记录它并确认它。

好了,现在我们有了一种从队列中接收消息的方法。唯一剩下的就是使用我们的/msg 处理程序向 RabbitMQ 发送事件的方法。为了做到这一点,我们首先需要一种方法来将 RabbitMQ 池传递给处理程序。

fn with_rmq(pool: Pool) -> impl Filter<Extract = (Pool,), Error = Infallible> + Clone {
    warp::any().map(move || pool.clone())
}

上述过滤器简单地克隆了该池,并使这个克隆的引用对任何拥有此过滤器的处理程序可用。实际的/msg 处理程序看起来像这样。

async fn add_msg_handler(pool: Pool) -> WebResult<impl Reply> {
    let payload = b"Hello world!";

    let rmq_con = get_rmq_con(pool).await.map_err(|e| {
        eprintln!("can't connect to rmq, {}", e);
        warp::reject::custom(Error::RMQPoolError(e))
    })?;

    let channel = rmq_con.create_channel().await.map_err(|e| {
        eprintln!("can't create channel, {}", e);
        warp::reject::custom(Error::RMQError(e))
    })?;

    channel
        .basic_publish(
            "",
            "hello",
            BasicPublishOptions::default(),
            payload.to_vec(),
            BasicProperties::default(),
        )
        .await
        .map_err(|e| {
            eprintln!("can't publish: {}", e);
            warp::reject::custom(Error::RMQError(e))
        })?
        .await
        .map_err(|e| {
            eprintln!("can't publish: {}", e);
            warp::reject::custom(Error::RMQError(e))
        })?;
    Ok("OK")
}

在这个简单的例子中,我们向每个人发送Hello World ,但这很容易被调整为发送,例如,POST 请求的有效载荷。

同样,我们从池中获得一个连接,然后创建一个通道,并在该通道上发布一个消息,将其发送到hello 队列中。

RabbitMQ 中连接池方法的一个问题是,我们仍然必须每次都创建一个通道,为了获得最佳性能,连接和通道都必须被池化。

如果您现在与 RabbitMQ 服务器一起运行此示例(例如使用 docker),您可以调用POST /msg 端点并观察来自监听器中队列的传入消息。

要使用上述凭据启动本地 RabbitMQ 实例,您可以使用以下命令。

docker run -p 15672:15672 -p 5672:5672 -e RABBITMQ_DEFAULT_USER=rmq -e RABBITMQ_DEFAULT_PASS=rmq  rabbitmq:3.8.4-management

完整的示例代码可以在这里找到

结论

自从async/await稳定下来后,围绕分布式系统的生态系统已经发展和成熟了很多,并且还在继续发展。

随着lapin的1.0发布,使用RabbitMQ的系统现在似乎可以用Rust以一种方便和连贯的方式构建,这对网络生态系统来说也是一个很大的进步。

按照这个速度,缺乏生态系统很快就不会成为反对在网络服务中使用Rust的理由了!:):)

资源