乐趣区

Tensorflow-Rust实战下篇整合actixweb提供http服务

上一篇我写的文章 how to use tensorflow with rust. 这一次我们看看使用 tensorflow 建立了什么,并通过 http 接口提供服务。随着 Actix Web1.0 版本发布,我认为用它构建一些东西将是一个很好的时机。

本文假设您对 Futures 及其运作方式有一定的了解。我将尽量用更简单的术语解释,但理解 Futures 生态系统将非常有效地帮助阅读本文。为此,我建议你从 tokio 开始。

有些人建议在深入 Futures 之前等待 async/await 和 friends 功能发布。我认为你现在应该亲自动手:异步编程总是很有挑战性。

再一次为了不耐烦的人,您可以在 actix-web 分支上找到参考代码:
https://github.com/cetra3/mtc…

一、API 定义

这里的 API 非常简单。我们想模仿我们在命令行上所做的事情:提交一张图片,返回结果是一张图片。为了使事情变得有趣,我们将提供一种方式: 将边界框以 JSON 数组返回。

关于通过 http 协议提交二进制数据,我首先想到了几种选择:

  • 只需提交原始数据即可
  • 使用 multipart/form-data
  • 序列化为 JSON 格式提交

我认为最简单是原始数据,所以让我们这样做!multipart/form-data 可能 ok,但是你必须处理多个图像的时候呢?JSON 格式似乎有点浪费,因为您不可避免地必须使用 base64 或类似的方式转换二进制数据。

所以我们的 API 是这样的:

  • 提交 POST 请求作为原始文件提交
  • 运行会话,通过 MTCNN 算法以提取人脸
  • 将边界框作以 JSON 格式返回;或者命令行示例一样,将图像叠加以 JPEG 格式返回。

二、MTCNN 的结构体(struct)

在我们上一篇博客中,我们只是简单地使用 main 函数来执行所有操作,但我们必须一些重构才能与 actix 一起使用。我们希望将 MTCNN 行为封装为结构,可以传递和转移。最终目标是在应用程序状态下使用它。

2.1 结构体 (struct) 定义

让我们将结构包含我们想要的一切:

  • 图版
  • 会话
  • 一些多个请求中共用的 Tensor 框架的输入参数

首先,我们创建一个新文件 mtcnn.rs 并加上结构体定义。

use tensorflow::{Graph, Session, Tensor};

pub struct Mtcnn {
    graph: Graph,
    session: Session,
    min_size: Tensor<f32>,
    thresholds: Tensor<f32>,
    factor: Tensor<f32>
}

然后,现在我们只是用 new 方法填充初始化内容。由于其中一些值的初始化并非绝对可靠,我们将返回 Result:

pub fn new() -> Result<Self, Box<dyn Error>> {let model = include_bytes!("mtcnn.pb");

    let mut graph = Graph::new();
    graph.import_graph_def(&*model, &ImportGraphDefOptions::new())?;

    let session = Session::new(&SessionOptions::new(), &graph)?;

    let min_size = Tensor::new(&[]).with_values(&[40f32])?;
    let thresholds = Tensor::new(&[3]).with_values(&[0.6f32, 0.7f32, 0.7f32])?;
    let factor = Tensor::new(&[]).with_values(&[0.709f32])?;

    Ok(Self {
        graph,
        session,
        min_size,
        thresholds,
        factor
    })

}

2.2Run 方法

我将在这里开始加快节奏,所以如果你遇到困难或不确定发生了什么,请查看 face-detection-with-tensorflow-rust,以解释这里发生的事情。

我们已经添加了所有需要跑一个会话的东西。让我们创建一个需要 API 做什么的方法:提交一张图片,响应一些边界框(框出人脸的位置):

pub fn run(&self, img: &DynamicImage) -> Result<Vec<BBoxes>, Status> {...}

再一次,我们响应了一个 Result 类型,因为在某些情况下 run 方法会失败。我们使用 Status 类型来表示响应错误的类型。

像我们先前的 main 方法,我们需要压平图片的输入:

let input = {let mut flattened: Vec<f32> = Vec::new();

    for (_x, _y, rgb) in img.pixels() {flattened.push(rgb[2] as f32);
        flattened.push(rgb[1] as f32);
        flattened.push(rgb[0] as f32);
    }

    Tensor::new(&[img.height() as u64, img.width() as u64, 3])
        .with_values(&flattened)?
};

然后我们将提供所有相关输入。这与我们之前的 main 方法相同,但我们只是从 self 中借用值,而不是为每次运行创建它们:

let mut args = SessionRunArgs::new();

args.add_feed(&self.graph.operation_by_name_required("min_size")?,
    0,
    &self.min_size,
);
args.add_feed(&self.graph.operation_by_name_required("thresholds")?,
    0,
    &self.thresholds,
);
args.add_feed(&self.graph.operation_by_name_required("factor")?,
    0,
    &self.factor,
);
args.add_feed(&self.graph.operation_by_name_required("input")?, 0, &input);

接下来,我们抓住我们想要的输出:

let bbox = args.request_fetch(&self.graph.operation_by_name_required("box")?, 0);
let prob = args.request_fetch(&self.graph.operation_by_name_required("prob")?, 0);

2.3 会话(running in session)

现在我们设置了所有参数,我们可以跑 session 了:

&self.session.run(&mut args)?;

噢哦!我们得到一个编译器错误:

error[E0596]: cannot borrow `self.session` as mutable, as it is behind a `&` reference
  --> src/mtcnn.rs:68:10
   |
36 |     pub fn run(&self, img: &DynamicImage) -> Result<DynamicImage, Box<dyn Error>> {
   |                ----- help: consider changing this to be a mutable reference: `&mut self`
...
68 |         &self.session.run(&mut args)?;
   |          ^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable

事实证明,Session::run()方法采用&mut self。我们可以做些什么来解决这个问题:

  • 使我们的 run 方法拿到 &mut self
  • 做一些棘手的内部可变性
  • 提交 issue 给 tensorflow-rust crate,看看 Session 是否真的需要 &mut self

我们选择了第三种方式!
更新你的 Cargo.toml,指定 git 而不是 cargo 里的 crate 版本号:

tensorflow = {git = "https://github.com/tensorflow/rust"}

2.4 获取边界框(人脸位置)

自从我们的 main 方法以来,这一点都没有改变。我们获取边界框,将它们放入我们的 BBox 结构中:

//Our bounding box extents
let bbox_res: Tensor<f32> = args.fetch(bbox)?;
//Our facial probability
let prob_res: Tensor<f32> = args.fetch(prob)?;

//Let's store the results as a Vec<BBox>
let mut bboxes = Vec::new();

let mut i = 0;
let mut j = 0;

//While we have responses, iterate through
while i < bbox_res.len() {
    //Add in the 4 floats from the `bbox_res` array.
    //Notice the y1, x1, etc.. is ordered differently to our struct definition.
    bboxes.push(BBox {y1: bbox_res[i],
        x1: bbox_res[i + 1],
        y2: bbox_res[i + 2],
        x2: bbox_res[i + 3],
        prob: prob_res[j], // Add in the facial probability
    });

    //Step `i` ahead by 4.
    i += 4;
    //Step `i` ahead by 1.
    j += 1;
}

debug!("BBox Length: {}, BBoxes:{:#?}", bboxes.len(), bboxes);

Ok(bboxes)

到此,我们的 run 方法完成了。

2.5BBox 结构的 JSON 格式

我们打算响应代表 BBox 结构体的 JSON,所以添加 serde_derive 中的 Serialize(序列化相关模块):

use serde_derive::Serialize;

#[derive(Copy, Clone, Debug, Serialize)]
pub struct BBox {
    pub x1: f32,
    pub y1: f32,
    pub x2: f32,
    pub y2: f32,
    pub prob: f32,
}

2.6 绘制输出的图片

我们将要添加一个方法,输入一张图片和一个边界框数组,响应输出的图片:

pub fn overlay(img: &DynamicImage, bboxes: &Vec<BBox>) -> DynamicImage

这里也没有多大的变化,只是响应了一张图片而不是保存一个文件:

//Let's clone the input image
let mut output_image = img.clone();

//Iterate through all bounding boxes
for bbox in bboxes {
    //Create a `Rect` from the bounding box.
    let rect = Rect::at(bbox.x1 as i32, bbox.y1 as i32)
        .of_size((bbox.x2 - bbox.x1) as u32, (bbox.y2 - bbox.y1) as u32);

    //Draw a green line around the bounding box
    draw_hollow_rect_mut(&mut output_image, rect, LINE_COLOUR);
}

output_image

好的,我们已经完成了我们的 Mtcnn 结构体和方法!我们可以进一步吗?是的,绝对可以!但就目前而言,我认为这就是我们所需要的。我们已经封装了行为并创建了一个很好用的几个函数。

三、新 main 方法

我们不再将它用作命令行程序,而是用作自托管的 Web 应用程序。因为我们不再有输入和输出文件,所以我们需要更改应用程序所需的参数。
我认为我们最初应该拿到的唯一参数是监听地址,即使这样我们也应该使用合理的默认值。所以让我们通过 structopt 的帮助来制作这个非常小的 demo:

#[derive(StructOpt)]
struct Opt {
    #[structopt(
        short = "l",
        long = "listen",
        help = "Listen Address",
        default_value = "127.0.0.1:8000"
    )]
    listen: String,
}

3.1 日志框架

Actix Web 使用 log crate 来显示 errors 和 debug message。
让我们使用 log 替代 println!。我喜欢使用 pretty_env_logger,因为它将不同的级别打印为不同的颜色,并且我们可以使用有用的时间戳。
pretty_env_logger 仍然使用环境变量。那就让我们设置环境变量 RUST_LOG,然后启动我们的 logger。

//Set the `RUST_LOG` var if none is provided
if env::var("RUST_LOG").is_err() {env::set_var("RUST_LOG", "mtcnn=DEBUG,actix_web=DEBUG");
}

//Create a timestamped logger
pretty_env_logger::init_timed();

这为我们的 app 和 actix web 设置了 DEBUG 级别日志,但允许我们通过环境变量更改日志级别。

四、Actix and 状态(State)

我们需要将一些状态传递给 actix 使用:Mtcnn 结构体和 run 方法。你可以通过多种方式传递状态提供 actix,但最简单的方法应该是 App::data 方法。当我们正在进入一个多线程世界时,我们将不得不考虑 Send/Sync。

好的,那么我们如何在线程之间分享数据呢?好吧,作为第一步,我会看看 std::sync。由于我们知道 mtcnn 的 run 函数不需要可变引用,只需要不可变 self 引用,我们可以将它包装在 Arc 中。如果我们不得不使用可变引用,那么可能也需要 Mutex,但是如果我们使用 tensorflow-rust 的主分支,可以避免这种情况。

那么让我们创建一个 Arc:

let mtcnn = Arc::new(Mtcnn::new()?);

现在可以实例化服务:

HttpServer::new(move || {App::new()
        //Add in our mtcnn struct, we clone the reference for each worker thread
        .data(mtcnn.clone())
        //Add in a logger to see the requests coming through
        .wrap(middleware::Logger::default())
        // Add in some routes here
        .service(...)
})
.bind(&opt.listen)? // Use the listener from the command arguments
.run()

总结一下我们已完成的事情:

  • 首先构建一个 HttpServer
  • 这需要一个返回 App 的闭包。此 App 是为每个 http 服务器运行的线程实例化的
  • 使用 data 方法添加 Arc<Mtcnn>,并为每个线程侦听器 clone 它
  • 添加了一个日志框架
  • 用 service 方法设置了一些 route
  • bind 到一个监听地址并运行

五、处理请求

Actix Web 是一个异步框架,使用 tokio。我们的 function 是同步,需要一些时间才能处理完成。换句话说,我们的请求是阻塞的。我们可以混合使用同步和异步,当然,处理起来有点麻烦。

5.1 方法定义与提取器(Extractors)

Actix 1.0 大量使用 Extractors,Extractors 为方法定义提供完全不同形式。您指定希望接口接收的内容,actix 将为您进行串联起来。请注意:这确实意味着在运行之前不能发现错误。我在 web::Data 参数中使用了错误的类型签名时的一个示例。

那么我们需要从我们的请求中提取什么?request body 的 bytes 和 mtcnn:

fn handle_request(
    stream: web::Payload,
    mtcnn: web::Data<Arc<Mtcnn>>,
) -> impl Future<Item = HttpResponse, Error = ActixError> {...}

我们将在 mtcnn 中使用这种类型(web::Data<Arc<Mtcnn>>),因此让我们为它创建一个类型别名:

type WebMtcnn = web::Data<Arc<Mtcnn>>;

六、从 Payload 中获取图像

注:这里的 payload 指的是 http 请求中 header 后面的部分。

我们需要一种从 payload 中检索图像并返回 Future 的方法。web::Payload 结构体实现了 Stream 将 Item 设置为 Bytes。

从流中获得单个字节是没有意义的,我们想要获得整个批次并对图像进行解码!因此,让我们将 Stream 转换为 Future,并将我们将要获得的所有单个字节合并到一个大的字节桶中。听起来很复杂,但幸运的是 Stream 有一个方法:concat2。

concat2 是一个非常强大的组合器,它允许我们将单个 Stream 轮询的结果加入到一个集合中,如果该项实现了 Extend(以及一些其它的 trait),Bytes 就会支持扩展。

因此就像这样:

stream.concat2().and_then(....)

6.1 图像解码 和 web::block

我们需要解决的第二件事是:如果我们要解码出图像,那么会阻止线程直到解码完成。如果它是一个巨大的图像,它可能需要几毫秒!因此,我们希望确保在发生这种情况时我们不会发生阻塞。幸运的是,actix web 有一种方法可以将阻塞代码包装为 future:

stream.concat2().and_then(move |bytes| {
    web::block(move || {image::load_from_memory(&bytes)
    })
})

我们采用 stream,将其转换为 future 和 bytes,然后使用 web::block 将字节解码为后台线程中的图像并返回结果。load_from_memory 函数返回了一个 Result,这意味着我们可以将其用作返回类型。

6.2 平衡错误类型

因此,我们的 Item 被转换为 Bytes 再到 DynamicImage,但我们还没有处理错误类型,无法编译通过。我们的错误类型应该是什么?让我们使用 actix_web::Error 作为 ActixError:

use actix_web::{Error as ActixError}

fn get_image(stream: web::Payload) -> impl Future<Item = DynamicImage, Error = ActixError> {stream.concat2().and_then(move |bytes| {
        web::block(move || {image::load_from_memory(&bytes)
        })
    })
}

好吧,当我们尝试编译时,出现了错误:

error[E0271]: type mismatch resolving `<impl futures::future::Future as futures::future::IntoFuture>::Error == actix_http::error::PayloadError`
  --> src/main.rs:67:22
   |
67 |     stream.concat2().and_then(move |bytes| {
   |                      ^^^^^^^^ expected enum `actix_threadpool::BlockingError`, found enum `actix_http::error::PayloadError`
   |
   = note: expected type `actix_threadpool::BlockingError<image::image::ImageError>`
              found type `actix_http::error::PayloadError`
              
还有一些未列出的内容...

当您组合 stream 时,将它们映射为 future,以及尝试从这些组合器获得一些输出时,您实际上处理的是 Item 类型 和 Error 类型。
处理多种类型的响应结果会使代码变得丑陋,这里不像 Result 类型可以使用问号 (?) 自动调整到正确的错误。当 ops::Try 和 async/await 语法变得稳定的时候,事情可能变得简单,但是现在,我们必须想办法处理这些错误类型。

我们可以使用 from_err() 方法。作用跟问号 (?) 基本相同,区别是 from_err 作用于 future。我们有两个正在处理的 future:来自 stream 的字节数组 和 来自阻塞闭包的图像。我们有 3 种错误类型:the Payload error, the Image load from memory error, and the blocking error:

fn get_image(stream: web::Payload)
  -> impl Future<Item = DynamicImage, Error = ActixError> {stream.concat2().from_err().and_then(move |bytes| {
        web::block(move || {image::load_from_memory(&bytes)
        }).from_err()})
}

七、从图像中获得边界框

最重要的是,我们需要 run 起来:

mtcnn.run(&img)

但是我们想要在一个线程池里跑起来:

web::block(|| mtcnn.run(&img))

让我们看看函数声明。至少我们需要图像和 mtcnn 结构体。然后我们想要返回 BBox 的 Vec。我们保持错误类型相同,因此我们将使用 ActixError 类型。

函数声明如下:

fn get_bboxes(img: DynamicImage, mtcnn: WebMtcnn) 
  -> impl Future<Item = Vec<BBox>, Error = ActixError> 

我们需要在 web::block 上使用 from_err() 来转换错误类型,使用 move 来将图像提供给闭包:

fn get_bboxes(img: DynamicImage, mtcnn: WebMtcnn) -> impl Future<Item = Vec<BBox>, Error = ActixError> {web::block(move || mtcnn.run(&img)).from_err()}

但还是会发生了编译错误:

error[E0277]: `*mut tensorflow_sys::TF_Status` cannot be sent between threads safely
  --> src/main.rs:75:5
   |
75 |     web::block(move || mtcnn.run(&img)).from_err()
   |     ^^^^^^^^^^ `*mut tensorflow_sys::TF_Status` cannot be sent between threads safely
   |
   = help: within `tensorflow::Status`, the trait `std::marker::Send` is not implemented for `*mut tensorflow_sys::TF_Status`
   = note: required because it appears within the type `tensorflow::Status`
   = note: required by `actix_web::web::block`

tensorflow::Status,它是错误类型,不能在线程之间发送。

快捷方式是将 error 转换成 String:

fn get_bboxes(img: DynamicImage, mtcnn: WebMtcnn) -> impl Future<Item = Vec<BBox>, Error = ActixError> {web::block(move || mtcnn.run(&img).map_err(|e| e.to_string())).from_err()}

因为 String 实现了 Send, 因此允许跨越线程间发送 Result。

八、返回 JSON 对象 BBoxes

好的,我们有 2 个函数,一个用于从请求中获取图像,另一个用于获取边界框。我们要返回回 json HttpResponse:

fn return_bboxes(
    stream: web::Payload,
    mtcnn: WebMtcnn,
) -> impl Future<Item = HttpResponse, Error = ActixError> {
    // Get the image from the input stream
    get_image(stream) 
        // Get the bounding boxes from the image
        .and_then(move |img| get_bboxes(img, mtcnn)) 
        // Map the bounding boxes to a json HttpResponse
        .map(|bboxes| HttpResponse::Ok().json(bboxes))
}

接着,在 App 里添接口定义:

HttpServer::new(move || {App::new()
        .data(mtcnn.clone()) 
        .wrap(middleware::Logger::default()) 
        // our new API service
        .service(web::resource("/api/v1/bboxes").to_async(return_bboxes))
})
.bind(&opt.listen)?
.run()

run 起来,用 curl 来提交一个请求:

$ curl --data-binary @rustfest.jpg  http://localhost:8000/api/v1/bboxes

[{"x1":471.4591,"y1":287.59888,"x2":495.3053,"y2":317.25327,"prob":0.9999908}....

使用 jmespath 来获取 120 张脸:

$ curl -s --data-binary @rustfest.jpg  http://localhost:8000/api/v1/bboxes | jp "length(@)"
120

九、返回叠加图像

我们想要的另一个 API 调用是返回一个覆盖了边界框的图像。这不是一个很大的延伸,但在图像上绘制框肯定是一个阻塞动作,所以我们将其发送到线程池中运行。
让我们包装叠加函数,将其转换为 future:

fn get_overlay(img: DynamicImage, bboxes: Vec<BBox>)
   -> impl Future<Item = Vec<u8>, Error = ActixError> {
    web::block(move || {let output_img = overlay(&img, &bboxes);
        
        ...

    }).from_err()}

我们想要返回一个 u8 字节的 Vec,这样我们就可以在返回体中使用它。所以我们需要分配缓冲区并以 JPEG 格式写入:

let mut buffer = vec![];

output_img.write_to(&mut buffer, JPEG)?; // write out our buffer

Ok(buffer)

将目前为止的函数尝试编译一次:

fn get_overlay(img: DynamicImage, bboxes: Vec<BBox>)
  -> impl Future<Item = Vec<u8>, Error = ActixError> {
    web::block(move || {let output_img = overlay(&img, &bboxes);

        let mut buffer = Vec::new();

        output_img.write_to(&mut buffer, JPEG)?; // write out our buffer

        Ok(buffer)
    }).from_err()}

还差一点,我们缺少一个类型注解:

error[E0282]: type annotations needed
  --> src/main.rs:82:5
   |
82 |     web::block(move || {|     ^^^^^^^^^^ cannot infer type for `E`

为什么这里是类型问题?关联到这一行:

Ok(buffer) // What's the `Error` type here?

目前,唯一的错误类型来自 write_to 方法,即 ImageError。但是这一行没有错误类型,可能是任何东西。
我想到三种方法处理这个问题:

方法一:在 web::block 中声明错误

web::block::<_,_,ImageError>

这看上去有点凌乱,但可以编译通过。

方法二:使用 as 声明 Result 类型:

Ok(buffer) as Result<_, ImageError>

方法三:使用 map 在成功时返回一个 buffer:

output_img.write_to(&mut buffer, JPEG).map(|_| buffer)

我认为为了可读性,#2 可能是最简单的。web::block 函数需要 3 个类型的参数,这些参数在第一次阅读代码时可能会引起混淆。#3 也不错,但我觉得它看起来有点奇怪。

最终我的选择:

fn get_overlay(img: DynamicImage, bboxes: Vec<BBox>)
   -> impl Future<Item = Vec<u8>, Error = ActixError> {
    web::block(move || {let output_img = overlay(&img, &bboxes);

        let mut buffer = Vec::new();

        output_img.write_to(&mut buffer, JPEG)?;

        // Type annotations required for the `web::block`
        Ok(buffer) as Result<_, ImageError> 
    }).from_err()}

9.1API 调用

好的,我们拥有了一些返回 future 的方法,future 返回边界框和叠加图像。让我们将它们拼接在一起并返回一个 HttpResponse:

fn return_overlay(
    stream: web::Payload,
    mtcnn: WebMtcnn,
) -> impl Future<Item = HttpResponse, Error = ActixError> {//... magic happens here}

第一步是从字节流中获取图像:

get_image(stream)

然后我们想要获取边界框:

get_image(stream).and_then(move |img| {get_bboxes(img, mtcnn)
})

9.2 如何使用 image 对象

现在我们想要获得叠加图像。我们有一个问题,如何使用 image?get_bboxes 返回 future 的图像,然后计算 image 上的人脸返回一个边界框数组。这里有几个选择。当我们将 image 传递给 get_bboxes 时,我们可以克隆 image,但这会发生内存拷贝。我们可以等待 Pin 和 async/await 语法完成,然后可能更容易处理它。
或者我们可以调整我们的 get_bboxes 方法:

fn get_bboxes(
    img: DynamicImage,
    mtcnn: WebMtcnn,
) -> impl Future<Item = (DynamicImage, Vec<BBox>), Error = ActixError> {
    web::block(move || {
        mtcnn
            .run(&img)
            .map_err(|e| e.to_string())
            //Return both the image and the bounding boxes
            .map(|bboxes| (img, bboxes))
    })
    .from_err()}

记录把 return_bboxes 方法也修改了:

fn return_bboxes(
    stream: web::Payload,
    mtcnn: WebMtcnn,
) -> impl Future<Item = HttpResponse, Error = ActixError> {get_image(stream)
        .and_then(move |img| get_bboxes(img, mtcnn))
        .map(|(_img, bboxes)| HttpResponse::Ok().json(bboxes))
}

9.3 获取叠加层

如果 rust 可以将元组变成命令参数,那就太好了。不幸的是不适合我们,所以我们需要创建一个闭包:

//Create our image overlay
.and_then(|(img, bbox)| get_overlay(img, bbox))
.map(|buffer| {// Return a `HttpResponse` here})

9.4 创建响应

我们的 HttpResponse 需要将 buffer 包装到一个 body:

HttpResponse::with_body(StatusCode::OK, buffer.into())

将 Content-Type 设置为 jpeg:

let mut response = HttpResponse::with_body(StatusCode::OK, buffer.into());

response
    .headers_mut()
    .insert(CONTENT_TYPE, HeaderValue::from_static("image/jpeg"));

获取叠加层的最终实现:

fn return_overlay(
    stream: web::Payload,
    mtcnn: WebMtcnn,
) -> impl Future<Item = HttpResponse, Error = ActixError> {get_image(stream)
        .and_then(move |img| {get_bboxes(img, mtcnn)
        })
        .and_then(|(img, bbox) | get_overlay(img, bbox))
        .map(|buffer| {let mut response = HttpResponse::with_body(StatusCode::OK, buffer.into());
            response
                .headers_mut()
                .insert(CONTENT_TYPE, HeaderValue::from_static("image/jpeg"));
            response
        })
}

在 App 注册此接口:

HttpServer::new(move || {App::new()
        .data(mtcnn.clone()) //Add in our data handler
        //Add in a logger to see the requets coming through
        .wrap(middleware::Logger::default()) 
        //JSON bounding boxes
        .service(web::resource("/api/v1/bboxes").to_async(return_bboxes))
        //Image overlay
        .service(web::resource("/api/v1/overlay").to_async(return_overlay))
}

run 一下:

$ curl --data-binary @rustfest.jpg  http://localhost:8000/api/v1/bboxes > output.jpg

结果:

十、总结

我们逐步将 CLI 应用程序转换为 HTTP 服务,并尝试了异步编程。如您所见,actix web 是一个非常通用的 Web 框架。我对它的兴趣来自于拥有构建 Web 应用程序所需的所有功能:多组件,线程池,高效率。虽然 actix 写异步还不是很优雅,但未来可期,因为我认为很多开发人员都在努力解决这个问题。

如果您正在寻找更多的 actix 示例,这个示例仓库是您最好的选择:https://github.com/actix/exam…

我期待看到社区未来的建设!

退出移动版