大家好,我是砸锅。一个摸鱼八年的后端开发。熟悉 Go、Lua。第十四天还是继续和大家一起学习 Rust😊
今天来设计一个图片处理程序,由于图片转换处理的功能很多,例如 /hamc/trim/smart/filter 这种在接口设计上如果按照一定格式、一定顺序将参数排列在 URL 里面的话,这样的话不好扩展,而且解析不方便,也很容易和别的参数冲突,形成破坏性更新。使用 querystring 的话,在图片处理步骤很多的时候就容易造成 querystring 很長
所以需要找一个更简洁而且可扩展的方式,例如使用 protobuf 。因为 protobuf 可以描述数据结构,并且几乎所有语言都对 protobuf 支持。使用 protobuf 生成一个 image spec 之后,然后将其序列化成字节流,再用 base64 转码放在 URL 里
syntax = "proto3";
package abi; // 编译结果的名字:abi.rs
// a ImageSpec is sort array
message ImageSpec { repeated Spec specs = 1; }
// spec
message Spec {
oneof data {
Resize resize = 1;
Crop crop = 2;
Flipv flipv = 3;
Fliph fliph = 4;
Contrast contrast = 5;
Filter filter = 6;
Watermark watermark = 7;
}
}
将 protobuf 生成 base64 字符串嵌进去 URL 里:
http://localhost:3000/image/CgoKCAjYBBCgBiADCgY6BAgUEBQKBDICCAM/<encoded origin url>
main.rs 的代码在这:
use anyhow::Result;
use axum::{
extract::{Extension, Path},
handler::get,
http::{HeaderMap, HeaderValue, StatusCode},
Router,
};
use bytes::Bytes;
use image::ImageOutputFormat;
use lru::LruCache;
use percent_encoding::{percent_decode_str, percent_encode, NON_ALPHANUMERIC};
use serde::Deserialize;
use std::{
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
sync::Arc,
time::Duration,
};
use tokio::sync::Mutex;
use tower::ServiceBuilder;
use tower_http::{
add_extension::AddExtensionLayer, compression::CompressionLayer, trace::TraceLayer,
};
use tracing::{info, instrument};
mod engine;
mod pb;
use engine::Photon;
use pb::*;
use crate::engine::Engine;
// 参数使用 serde 做 Deserialize,axum 会自动识别并解析
#[derive(Deserialize)]
struct Params {
spec: String,
url: String,
}
type Cache = Arc<Mutex<LruCache<u64, Bytes>>>;
#[tokio::main]
async fn main() {
// 初始化 tracing
tracing_subscriber::fmt::init();
let cache: Cache = Arc::new(Mutex::new(LruCache::new(1024)));
// 构建路由
let app = Router::new()
// `GET /` 会执行
.route("/image/:spec/:url", get(generate))
.layer(
ServiceBuilder::new()
.load_shed()
.concurrency_limit(1024)
.timeout(Duration::from_secs(10))
.layer(TraceLayer::new_for_http())
.layer(AddExtensionLayer::new(cache))
.layer(CompressionLayer::new())
.into_inner(),
);
// 运行 web 服务器
let addr = "127.0.0.1:3000".parse().unwrap();
print_test_url("https://images.pexels.com/photos/1562477/pexels-photo-1562477.jpeg?auto=compress&cs=tinysrgb&dpr=3&h=750&w=1260");
info!("Listening on {}", addr);
axum::Server::bind(&addr)
.serve(app.into_make_service())
.await
.unwrap();
}
// basic handler that responds with a static string
async fn generate(
Path(Params { spec, url }): Path<Params>,
Extension(cache): Extension<Cache>,
) -> Result<(HeaderMap, Vec<u8>), StatusCode> {
let spec: ImageSpec = spec
.as_str()
.try_into()
.map_err(|_| StatusCode::BAD_REQUEST)?;
let url: &str = &percent_decode_str(&url).decode_utf8_lossy();
let data = retrieve_image(url, cache)
.await
.map_err(|_| StatusCode::BAD_REQUEST)?;
// 使用 image engine 处理
let mut engine: Photon = data
.try_into()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
engine.apply(&spec.specs);
// TODO: 这里目前类型写死了,应该使用 content negotiation
let image = engine.generate(ImageOutputFormat::Jpeg(85));
info!("Finished processing: image size {}", image.len());
let mut headers = HeaderMap::new();
headers.insert("content-type", HeaderValue::from_static("image/jpeg"));
Ok((headers, image))
}
#[instrument(level = "info", skip(cache))]
async fn retrieve_image(url: &str, cache: Cache) -> Result<Bytes> {
let mut hasher = DefaultHasher::new();
url.hash(&mut hasher);
let key = hasher.finish();
let g = &mut cache.lock().await;
let data = match g.get(&key) {
Some(v) => {
info!("Match cache {}", key);
v.to_owned()
}
None => {
info!("Retrieve url");
let resp = reqwest::get(url).await?;
let data = resp.bytes().await?;
g.put(key, data.clone());
data
}
};
Ok(data)
}
// 调试辅助函数
fn print_test_url(url: &str) {
use std::borrow::Borrow;
let spec1 = Spec::new_resize(500, 800, resize::SampleFilter::CatmullRom);
let spec2 = Spec::new_watermark(20, 20);
let spec3 = Spec::new_filter(filter::Filter::Marine);
let image_spec = ImageSpec::new(vec![spec1, spec2, spec3]);
let s: String = image_spec.borrow().into();
let test_image = percent_encode(url.as_bytes(), NON_ALPHANUMERIC).to_string();
println!("test url: http://localhost:3000/image/{}/{}", s, test_image);
}
这个项目还是挺有学习意义的,由于代码量比较多,所以就不贴出来了。 完整的项目代码:github.com/tyrchen/gee…