编辑
2024-01-19
网络
00
请注意,本文编写于 296 天前,最后修改于 296 天前,其中某些信息可能已经过时。

目录

wmproxy
项目地址
nginx中的try_files
实现方案
源码实现
小结

wmproxy

wmproxy已用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现websocket代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子

项目地址

国内: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

nginx中的try_files

  • 语法:try_files file … uri;try_files file … = code;
  • 作用域:server location
    • 首先:按照指定的顺序检查文件是否存在,并使用第一个找到的文件进行请求处理
    • 其次:处理是在当前上下文中执行的。根据 root 和 alias 指令从 file 参数构造文件路径。
    • 然后:可以通过在名称末尾指定一个斜杠来检查目录的存在,例如"$uri/"
    • 最后:如果没有找到任何文件,则进行内部重定向到最后一个参数中指定的 uri。

注:只有最后一个参数可以引起一个内部重定向,之前的参数只设置内部的 URL 的指向。最后一个参数是回退 URL 且必须存在,否则会出现内部 500 错误。命名的 location 也可以使用在最后一个参数中。

应用场景

1、前端路由处理:

NGINX
location / { try_files $uri $uri/ /index.html; # $uri指请求的uri路径,$uri/表示请求的uri路径加上一个/,例如访问example.com/path,则会依次尝试访问/path,/path/index.html,/index.html # /index.html表示如果仍未匹配到则重定向到index.html }

这种场景多用于单页应用,例如vue.js等前端框架的路由管理。当用户在浏览器中访问一个非根路径的路径时,由于这些路径都是由前端路由管理的,nginx无法直接返回正确的静态文件,因此需要将请求重定向到统一的路径,这里是/index.html,由前端路由控制页面的展示。 2、图片服务器:

NGINX
location /images/ { root /data/www; error_page 404 = /fetch_image.php; try_files $uri $uri/ =404; } location /fetch_image.php { fastcgi_pass 127.0.0.1:9000; set $path_info ""; fastcgi_param PATH_INFO $path_info; fastcgi_param SCRIPT_FILENAME /scripts/fetch_image.php; include fastcgi_params; }

这种场景多用于图片服务器,当用户访问图片时,先尝试在本地文件系统中查找是否有该文件,如果找到就返回;如果没有找到则会转发到fetch_image.php进行处理,从远程资源服务器拉取图片并返回给用户。

实现方案

当前nginx方案的实现,是基于文件的重试,也就是所谓的伪静态,如果跨目录的服务器就很麻烦了,比如:

NGINX
location /images/ { root /data/upload; try_files $uri $uri/ =404; } location /images2/ { root /data/www; try_files $uri $uri/ =404; }

上面的我们无法同时索引两个目录下的结构。即我假设我访问/images/logo.png无法同时查找/data/upload/logo.png/data/www/logo.png是否存在。

当前实现方案从try_files变成try_paths也就是当碰到该选项时,将当前的几个访问地址重新进入路由

例:

TOML
[[http.server.location]] rate_limit = "4m/s" rule = "/root/logo.png" file_server = { browse = true } proxy_pass = "" try_paths = "/data/upload/logo.png /data/www/logo.png /root/README.md" [[http.server.location]] rule = "/data/upload" file_server = { browse = true } [[http.server.location]] rule = "/data/www" file_server = { browse = true }

除非碰到返回100或者200状态码的,否则将执行到最后一个匹配路由。

源码实现

    1. 要能循环遍历路由
    1. 当try_paths时要避免递归死循环
    1. 当try_paths时可能会调用自己本身,需要能重复调用

以下主要源码均在reverse/http.rs

  • 实现循环 主要的处理函数为deal_match_location,函数的参数为
RUST
#[async_recursion] async fn deal_match_location( req: &mut Request<Body>, // 缓存客户端请求 cache: &mut HashMap< LocationConfig, (Sender<Request<Body>>, Receiver<ProtResult<Response<Body>>>), >, // 该Server的配置选项 server: Arc<ServerConfig>, // 已处理的匹配路由 deals: &mut HashSet<usize>, // 已处理的TryPath匹配路由 try_deals: &mut HashSet<usize>, ) -> ProtResult<Response<Body>>

当前在Rust中的异步递归会报如下错误

recursion in an `async fn` requires boxing a recursive `async fn` must be rewritten to return a boxed `dyn Future` consider using the `async_recursion` crate: https://crates.io/crates/async_recursion

所以需要添加#[async_recursion]或者改成Box返回。

参数其中多定义了两组HashSet用来存储已处理的路由及已处理的TryPath路由。

将循环获取合适的location,如果未找到直接返回503错误。

RUST
let path = req.path().clone(); let mut l = None; let mut now = usize::MAX; for idx in 0..server.location.len() { if deals.contains(&idx) { continue; } if server.location[idx].is_match_rule(&path, req.method()) { l = Some(&server.location[idx]); now = idx; break; } } if l.is_none() { return Ok(Response::status503() .body("unknow location to deal") .unwrap() .into_type()); }

当该路由存在try_paths的情况时:

RUST
// 判定该try是否处理过, 防止死循环 if !try_deals.contains(&now) && l.try_paths.is_some() { let try_paths = l.try_paths.as_ref().unwrap(); try_deals.insert(now); let ori_path = req.path().clone(); for val in try_paths.list.iter() { deals.clear(); // 重写path好方便做数据格式化 req.set_path(ori_path.clone()); let new_path = Helper::format_req(req, &**val); // 重写path好方便后续处理无感 req.set_path(new_path); if let Ok(res) = Self::deal_match_location( req, cache, server.clone(), deals, try_deals, ) .await { if !res.status().is_client_error() && !res.status().is_server_error() { return Ok(res); } } } return Ok(Response::builder() .status(try_paths.fail_status) .body("not valid to try") .unwrap() .into_type()); }

其中会将req中的path进行格式化的重写以方便处理:

rust
// 重写path好方便做数据格式化 req.set_path(ori_path.clone()); let new_path = Helper::format_req(req, &**val); // 重写path好方便后续处理无感 req.set_path(new_path);

如果不存在try_paths将正常的按照路由的处理逻辑,该文件服务器或者反向代理,并标记该路由已处理。

RUST
deals.insert(now); let clone = l.clone_only_hash(); if cache.contains_key(&clone) { let mut cache_client = cache.remove(&clone).unwrap(); if !cache_client.0.is_closed() { println!("do req data by cache"); let _send = cache_client.0.send(req.replace_clone(Body::empty())).await; match cache_client.1.recv().await { Some(res) => { if res.is_ok() { log::trace!("cache client receive response"); cache.insert(clone, cache_client); } return res; } None => { log::trace!("cache client close response"); return Ok(Response::status503() .body("already lose connection") .unwrap() .into_type()); } } } } else { log::trace!("do req data by new"); let (res, sender, receiver) = l.deal_request(req).await?; if sender.is_some() && receiver.is_some() { cache.insert(clone, (sender.unwrap(), receiver.unwrap())); } return Ok(res); }

小结

try_files在nginx中提供了更多的可能,也方便了伪静态文件服务器的处理。我们在其中的基础上稍微改造成try_paths来适应处理提供多路由映射的可能性。

点击 [关注][在看][点赞] 是对作者最大的支持

本文作者:问蒙服务框架

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!