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

37次阅读

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

上一篇我写的文章 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…

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

正文完
 0