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、前端路由处理:

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

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

2、图片服务器:

  1. location /images/ {
  2. root /data/www;
  3. error_page 404 = /fetch_image.php;
  4. try_files $uri $uri/ =404;
  5. }
  6. location /fetch_image.php {
  7. fastcgi_pass 127.0.0.1:9000;
  8. set $path_info "";
  9. fastcgi_param PATH_INFO $path_info;
  10. fastcgi_param SCRIPT_FILENAME /scripts/fetch_image.php;
  11. include fastcgi_params;
  12. }

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

实现方案

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

  1. location /images/ {
  2. root /data/upload;
  3. try_files $uri $uri/ =404;
  4. }
  5. location /images2/ {
  6. root /data/www;
  7. try_files $uri $uri/ =404;
  8. }

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

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

例:

  1. [[http.server.location]]
  2. rate_limit = "4m/s"
  3. rule = "/root/logo.png"
  4. file_server = { browse = true }
  5. proxy_pass = ""
  6. try_paths = "/data/upload/logo.png /data/www/logo.png /root/README.md"
  7. [[http.server.location]]
  8. rule = "/data/upload"
  9. file_server = { browse = true }
  10. [[http.server.location]]
  11. rule = "/data/www"
  12. file_server = { browse = true }

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

源码实现

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

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

  • 实现循环

    主要的处理函数为deal_match_location,函数的参数为
  1. #[async_recursion]
  2. async fn deal_match_location(
  3. req: &mut Request<Body>,
  4. // 缓存客户端请求
  5. cache: &mut HashMap<
  6. LocationConfig,
  7. (Sender<Request<Body>>, Receiver<ProtResult<Response<Body>>>),
  8. >,
  9. // 该Server的配置选项
  10. server: Arc<ServerConfig>,
  11. // 已处理的匹配路由
  12. deals: &mut HashSet<usize>,
  13. // 已处理的TryPath匹配路由
  14. try_deals: &mut HashSet<usize>,
  15. ) -> ProtResult<Response<Body>>

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

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

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

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

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

  1. let path = req.path().clone();
  2. let mut l = None;
  3. let mut now = usize::MAX;
  4. for idx in 0..server.location.len() {
  5. if deals.contains(&idx) {
  6. continue;
  7. }
  8. if server.location[idx].is_match_rule(&path, req.method()) {
  9. l = Some(&server.location[idx]);
  10. now = idx;
  11. break;
  12. }
  13. }
  14. if l.is_none() {
  15. return Ok(Response::status503()
  16. .body("unknow location to deal")
  17. .unwrap()
  18. .into_type());
  19. }

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

  1. // 判定该try是否处理过, 防止死循环
  2. if !try_deals.contains(&now) && l.try_paths.is_some() {
  3. let try_paths = l.try_paths.as_ref().unwrap();
  4. try_deals.insert(now);
  5. let ori_path = req.path().clone();
  6. for val in try_paths.list.iter() {
  7. deals.clear();
  8. // 重写path好方便做数据格式化
  9. req.set_path(ori_path.clone());
  10. let new_path = Helper::format_req(req, &**val);
  11. // 重写path好方便后续处理无感
  12. req.set_path(new_path);
  13. if let Ok(res) = Self::deal_match_location(
  14. req,
  15. cache,
  16. server.clone(),
  17. deals,
  18. try_deals,
  19. )
  20. .await
  21. {
  22. if !res.status().is_client_error() && !res.status().is_server_error() {
  23. return Ok(res);
  24. }
  25. }
  26. }
  27. return Ok(Response::builder()
  28. .status(try_paths.fail_status)
  29. .body("not valid to try")
  30. .unwrap()
  31. .into_type());
  32. }

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

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

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

  1. deals.insert(now);
  2. let clone = l.clone_only_hash();
  3. if cache.contains_key(&clone) {
  4. let mut cache_client = cache.remove(&clone).unwrap();
  5. if !cache_client.0.is_closed() {
  6. println!("do req data by cache");
  7. let _send = cache_client.0.send(req.replace_clone(Body::empty())).await;
  8. match cache_client.1.recv().await {
  9. Some(res) => {
  10. if res.is_ok() {
  11. log::trace!("cache client receive response");
  12. cache.insert(clone, cache_client);
  13. }
  14. return res;
  15. }
  16. None => {
  17. log::trace!("cache client close response");
  18. return Ok(Response::status503()
  19. .body("already lose connection")
  20. .unwrap()
  21. .into_type());
  22. }
  23. }
  24. }
  25. } else {
  26. log::trace!("do req data by new");
  27. let (res, sender, receiver) = l.deal_request(req).await?;
  28. if sender.is_some() && receiver.is_some() {
  29. cache.insert(clone, (sender.unwrap(), receiver.unwrap()));
  30. }
  31. return Ok(res);
  32. }

小结

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

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

37. 干货系列从零用Rust编写负载均衡及代理,负载均衡中try_files实现的更多相关文章

  1. 【转】Spring Boot干货系列:(一)优雅的入门篇

    转自Spring Boot干货系列:(一)优雅的入门篇 前言 Spring一直是很火的一个开源框架,在过去的一段时间里,Spring Boot在社区中热度一直很高,所以决定花时间来了解和学习,为自己做 ...

  2. Spring Boot干货系列:(八)数据存储篇-SQL关系型数据库之JdbcTemplate的使用

    Spring Boot干货系列:(八)数据存储篇-SQL关系型数据库之JdbcTemplate的使用 原创 2017-04-13 嘟嘟MD 嘟爷java超神学堂 前言 前面几章介绍了一些基础,但都是静 ...

  3. Spring Boot干货系列:(七)默认日志框架配置

    Spring Boot干货系列:(七)默认日志框架配置 原创 2017-04-05 嘟嘟MD 嘟爷java超神学堂 前言 今天来介绍下Spring Boot如何配置日志logback,我刚学习的时候, ...

  4. Spring Boot干货系列:(五)开发Web应用JSP篇

    Spring Boot干货系列:(五)开发Web应用JSP篇 原创 2017-04-05 嘟嘟MD 嘟爷java超神学堂 前言 上一篇介绍了Spring Boot中使用Thymeleaf模板引擎,今天 ...

  5. Spring Boot干货系列:(四)Thymeleaf篇

    Spring Boot干货系列:(四)Thymeleaf篇 原创 2017-04-05 嘟嘟MD 嘟爷java超神学堂 前言 Web开发是我们平时开发中至关重要的,这里就来介绍一下Spring Boo ...

  6. Spring Boot干货系列:(一)优雅的入门篇

    Spring Boot干货系列:(一)优雅的入门篇 2017-02-26 嘟嘟MD 嘟爷java超神学堂   前言 Spring一直是很火的一个开源框架,在过去的一段时间里,Spring Boot在社 ...

  7. Spring Boot干货系列:(十二)Spring Boot使用单元测试(转)

    前言这次来介绍下Spring Boot中对单元测试的整合使用,本篇会通过以下4点来介绍,基本满足日常需求 Service层单元测试 Controller层单元测试 新断言assertThat使用 单元 ...

  8. (转)Spring Boot干货系列:(七)默认日志logback配置解析

    转:http://tengj.top/2017/04/05/springboot7/ 前言 今天来介绍下Spring Boot如何配置日志logback,我刚学习的时候,是带着下面几个问题来查资料的, ...

  9. (转)Spring Boot干货系列:(四)开发Web应用之Thymeleaf篇

    转:http://tengj.top/2017/03/13/springboot4/ 前言 Web开发是我们平时开发中至关重要的,这里就来介绍一下Spring Boot对Web开发的支持. 正文 Sp ...

  10. 【WEB API项目实战干货系列】- 导航篇(十足干货分享)

    在今天移动互联网的时代,作为攻城师的我们,谁不想着只写一套API就可以让我们的Web, Android APP, IOS APP, iPad APP, Hybired APP, H5 Web共用共同的 ...

随机推荐

  1. 【项目源码】基于JavaEE的健康管理系统

    随着网络技术的不断发展,网站的开发与运用变得更加广泛.这次采用java语言SSH框架(Spring,Struts,Hibernate)设计并实现了面向特定群体的健康管理平台.该网站主要有教师饮食管理. ...

  2. torch.nn.ReLU(inplace=True)的具体含义:

    首先根据源文档中的ReLU(x)=max(0,x),得出结论.大于0的数值不变,小于0的数据变成0. 补充:这里需要注意的是 ReLU并没有限制数据的大小. 这是对应的文档链接:https://pyt ...

  3. 自学一周python做的一个小游戏《大球吃小球》

    需求 1,显示一个窗口. 2,我们要做到的功能有鼠标点击屏幕生成小球. 3,生成的小球大小随机,颜色随机,向随机方向移动,速度也随机. 4,大的球碰到小球时可以吃掉小球,吃掉后会变大. 5,球碰到边界 ...

  4. DP 复习

    背包 约定使用 \(v_i\) 表示放入第 \(i\) 件物品的花费,\(w_i\) 表示第 \(i\) 件物品的价值,背包容量 \(M\),物品件数 \(N\). 01 背包 每种物品仅有一件,可以 ...

  5. Spring Boot整合OAuth2实现GitHub第三方登录

    Github OAuth 第三方登录示例 1.第三方登录原理 第三方登录的原理是借助OAuth授权来实现,首先用户先向客户端提供第三方网站的数据证明自己的身份获取授权码,然后客户端拿着授权码与授权服务 ...

  6. 导出所有容器id号

    #!/bin/bash a=`docker ps|awk 'NR>1{print $1}'` FORMAT="%-12s\t,\t%-12s\t,\t%-12s\n" pri ...

  7. CF1295D Same GCDs

    前置知识: 辗转相除法 欧拉函数 首先,根据辗转相除法求 \(\gcd\) 的公式,可得 \(\gcd(a+x,m)=\gcd((a+x)\mod m,m)\). 则题目可以转化为:求有多少 \(x\ ...

  8. 解决使用mitmprox抓包可以访问网页,但是使用python request 调用该网站接口报错问题

    可能有几种原因导致这种情况.以下是一些常见的问题和可能的解决方法: 证书验证问题: 当你使用mitmproxy抓包时,它通常会生成自签名的SSL证书,以便进行中间人攻击检查.但在Python中使用re ...

  9. [Python急救站课程]整数数列求和

    整数数列求和程序: n = input("请输入整数N: ") sum = 0 for i in range(int(n)): # range用于计数整数.for表示循环 sum ...

  10. [Python急救站课程]蟒蛇的绘制

    Python的英文是有蟒蛇的意思,用Python画一条蟒蛇试试吧 一.普通蟒蛇的绘制 import turtle # 调用turtle(海龟绘图)加as t表示将库名改命名为t,后续用t.(函数名表式 ...