wmproxy

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

项目地址

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

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

设计目标

负载均衡时通过匹配规则匹配正确的location进行处理相关的操作。

设计方案变更

初始设计方案

初始方案以最快的方式进行支持,仅支持前缀匹配,即如果配置

  1. [[http.server.location]]
  2. rule = "/wmproxy"

那么当我们访问/wmproxy/xx时将会被分配到该location,此方案相对简单,但是当我们碰到复杂的需求时将无法被满足。

设计方案需求

除了前缀匹配外,我们将会有其它各种需求的匹配:

  • 后缀匹配 比如以wmproxy结尾的path,如/api/update/wmproxy 需要匹配成 *wmproxy
  • 中间匹配 比如常用的api中间转化成数据/api/<user_id>/get,那么匹配为 /api/*/get
  • 正则匹配 当前的配置的为正则规则,需进行匹配
  • 请求方法匹配 比如仅当请求方法为POST才进行转发
  • 客户端IP 比如仅当客户端内网或者外网时区分请求
  • Host地址 比如当前如果请求为ip则不进行转发,需要匹配host才进行转发
  • 协议 比如某个网站不支持http当我们匹配到http时需强制转化成https

    实际配置中当仅仅只有前缀匹配时已经显然无法满足我们的需求

设计方案迭代

当前我们就必须将数据进行更迭,但是在通常情况下我们又不想将配置变得复杂,此时就需要我们支持更多的类的自定义化,首先我们定义类:

  1. /// location匹配,将根据该类的匹配信息进行是否匹配
  2. #[serde_as]
  3. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
  4. pub struct Matcher {
  5. path: Option<String>,
  6. #[serde_as(as = "Option<DisplayFromStr>")]
  7. client_ip: Option<IpSets>,
  8. #[serde_as(as = "Option<DisplayFromStr>")]
  9. remote_ip: Option<IpSets>,
  10. host: Option<String>,
  11. #[serde_as(as = "Option<DisplayFromStr>")]
  12. method: Option<MatchMethod>,
  13. #[serde_as(as = "Option<DisplayFromStr>")]
  14. scheme: Option<MatchScheme>,
  15. }

此时我们将location中的rule的类型从String变成了Matcher,那么此时我们首先遇到的一个问题他可能为一个String值或者可能为一个Map值,我们先得对这种情况进行处理。

我们根据serde的提供的解析方案进行如下函数,当前我们重写了visit_strvisit_map表示我们将只支持这两种源格式转化成Matcher

  1. pub fn string_or_struct<'de, T, D>(deserializer: D) -> Result<T, D::Error>
  2. where
  3. T: Deserialize<'de> + FromStr<Err = WebError>,
  4. D: Deserializer<'de>,
  5. {
  6. struct StringOrStruct<T>(PhantomData<fn() -> T>);
  7. impl<'de, T> Visitor<'de> for StringOrStruct<T>
  8. where
  9. T: Deserialize<'de> + FromStr<Err = WebError>,
  10. {
  11. type Value = T;
  12. fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
  13. formatter.write_str("string or map")
  14. }
  15. fn visit_str<E>(self, value: &str) -> Result<T, E>
  16. where
  17. E: de::Error,
  18. {
  19. Ok(FromStr::from_str(value).unwrap())
  20. }
  21. fn visit_map<M>(self, map: M) -> Result<T, M::Error>
  22. where
  23. M: MapAccess<'de>,
  24. {
  25. Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))
  26. }
  27. }
  28. deserializer.deserialize_any(StringOrStruct(PhantomData))
  29. }

其次我们将在location中做处理

  1. /// 负载均衡中的location匹配,将匹配合适的处理逻辑
  2. #[serde_as]
  3. #[derive(Debug, Clone, Serialize, Deserialize)]
  4. pub struct LocationConfig {
  5. #[serde(deserialize_with = "string_or_struct")]
  6. pub rule: Matcher,
  7. //...
  8. }

由于这种大类的匹配通常会在别处额外定义,我们通过以@name@开头来表示索引的信息,来简化配置。通过初始化的时候来重新初始化Matcher

处理匹配

我们初始化完Matcher之后,需要能正确的判断传入的数据是否当前能正确匹配。主要的复杂点在于path的匹配,主要为正则匹配前缀匹配中间匹配后缀匹配

对其进行细分,可确定分为两种

  1. 正则匹配
  2. *的路径匹配
    1. 前缀匹配可以看成/start*或者/start
    2. 中间匹配可以看成/start*end
    3. 后缀匹配可以看成*end

即当前我们只需处理两种匹配模式:

  • 正则匹配,频繁调用时主要在于初始化正则时可能会消耗大量的算力。当前我们对我们的匹配规则的正则进行缓存
  1. /// may memory leak
  2. pub fn try_cache_regex(origin: &str) -> Option<Regex> {
  3. // 因为均是从配置中读取的数据, 在这里缓存正则表达示会在总量上受到配置的限制
  4. lazy_static! {
  5. static ref RE_CACHES: Mutex<HashMap<&'static str, Option<Regex>>> =
  6. Mutex::new(HashMap::new());
  7. };
  8. if origin.len() == 0 {
  9. return None;
  10. }
  11. if let Ok(mut guard) = RE_CACHES.lock() {
  12. if let Some(re) = guard.get(origin) {
  13. return re.clone();
  14. } else {
  15. if let Ok(re) = Regex::new(origin) {
  16. guard.insert(
  17. Box::leak(origin.to_string().into_boxed_str()),
  18. Some(re.clone()),
  19. );
  20. return Some(re);
  21. }
  22. }
  23. }
  24. return None;
  25. }

此处我们用到了static变量,也就是将某部分数据进行了静态化处理,且此处我们将String转化成了&'static str可能存在一定的内存泄漏,大小值跟配置的数据有关,可以接受这空间换取时间。然后用正则的is_match进行匹配即可。

  1. if let Some(re) = Helper::try_cache_regex(&p) {
  2. if !re.is_match(path) {
  3. return Ok(false);
  4. }
  5. }
  • *的路径匹配 主要将路径中的*进行前进字符串的匹配。

    在rust中的字符串切割主要由split或者strip_prefix或者strip_suffix来处理,相对其它语言中均存在的subString或者substr在rust中的则表示为引用,所以在rust中不存在substring函数
  1. let src = "wmproxy is good";
  2. let first = &src[..7];
  3. let second = &src[3..8];
  4. let end = &src[8..];
  5. let vals = src.split(" ").collect::<Vec<&str>>();

以上各数据均引用src的资源,即在这过程中并没有创建内存对象。

那么匹配函数则先将'*'进行分割,数组的第一个则前缀匹配,最后一个则后缀匹配,若不存在'*'则数组数量为1,符合前缀匹配。

  1. pub fn is_match(src: &str, pattern: &str) -> bool {
  2. let mut oper = src;
  3. let vals = pattern.split("*").collect::<Vec<&str>>();
  4. for i in 0..vals.len() {
  5. if i == 0 {
  6. if let Some(val) = oper.strip_prefix(vals[i]) {
  7. oper = val;
  8. } else {
  9. return false;
  10. }
  11. } else if i == vals.len() - 1 {
  12. if let Some(val) = oper.strip_suffix(vals[i]) {
  13. oper = val;
  14. } else {
  15. return false;
  16. }
  17. } else {
  18. if let Some(idx) = oper.find(vals[i]) {
  19. oper = &oper[idx + vals[i].len() .. ]
  20. } else {
  21. return false;
  22. }
  23. }
  24. }
  25. true
  26. }

那么完整的匹配函数在Matcher

  1. /// 当本地限制方法时,优先匹配方法,在进行路径的匹配
  2. pub fn is_match_rule(&self, path: &String, req: &RecvRequest) -> ProtResult<bool> {
  3. if let Some(p) = &self.path {
  4. let mut is_match = false;
  5. if Helper::is_match(&path, p) {
  6. is_match = true;
  7. }
  8. if !is_match {
  9. if let Some(re) = Helper::try_cache_regex(&p) {
  10. if !re.is_match(path) {
  11. return Ok(false);
  12. }
  13. } else {
  14. return Ok(false);
  15. }
  16. }
  17. }
  18. if let Some(m) = &self.method {
  19. if !m.0.contains(req.method()) {
  20. return Ok(false);
  21. }
  22. }
  23. if let Some(s) = &self.scheme {
  24. if !s.0.contains(req.scheme()) {
  25. return Ok(false);
  26. }
  27. }
  28. if let Some(h) = &self.host {
  29. match req.get_host() {
  30. Some(host) if &host == h => {},
  31. _ => return Ok(false),
  32. }
  33. }
  34. if let Some(c) = &self.client_ip {
  35. match req.headers().system_get("{client_ip}") {
  36. Some(ip) => {
  37. let ip = ip
  38. .parse::<IpAddr>()
  39. .map_err(|_| ProtError::Extension("client ip error"))?;
  40. if !c.contains(&ip) {
  41. return Ok(false)
  42. }
  43. },
  44. None => return Ok(false),
  45. }
  46. }
  47. Ok(true)
  48. }

小结

匹配规则在对于复杂匹配的时候尤为重要,我们可以轻松的将各个请求分配到合适的位置,此处我们着重介绍了正则匹配及带*的路径匹配。

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

47从零开始用Rust编写nginx,配对还有这么多要求!负载均衡中的路径匹配的更多相关文章

  1. 在Linux上使用Nginx为Solr集群做负载均衡

    在Linux上使用Nginx为Solr集群做负载均衡 在Linux上搭建solr集群时需要用到负载均衡,但测试环境下没有F5 Big-IP负载均衡交换机可以用,于是先后试了weblogic的proxy ...

  2. nginx作反向代理,实现负载均衡

    nginx作反向代理,实现负载均衡按正常的方法安装好 ngixn,方法可参考http://www.cnblogs.com/lin3615/p/4376224.html其中作了反向代理的服务器的配置如下 ...

  3. LVS + keepalived + nginx + tomcat 实现主从热备 + 负载均衡

    前言 首先声明下,由于这两天找资料,看了不少博客 ,但是出于不细心,参考者的博客地址没有记录下来,所有文中要是出现了与大家博客相同的地方,那么请大家在评论区说明并附上博客地址,我好引用进来:这里表示抱 ...

  4. Nginx + Memcached 实现Session共享的负载均衡

    session共享 我们在做站点的试试,通常需要保存用户的一些基本信息,比如登录就会用到Session:当使用Nginx做负载均衡的时候,用户浏览站点的时候会被分配到不同的服务器上,此时如果登录后Se ...

  5. 使用nginx sticky实现基于cookie的负载均衡

    在多台后台服务器的环境下,我们为了确保一个客户只和一台服务器通信,我们势必使用长连接.使用什么方式来实现这种连接呢,常见的有使用nginx自带的ip_hash来做,我想这绝对不是一个好的办法,如果前端 ...

  6. nginx的概念与几种负载均衡算法

    Nginx的背景 Nginx和Apache一样都是一种WEB服务器.基于REST架构风格,以URI(Uniform Resources Identifier,统一资源描述符)或URL(Uniform ...

  7. 用Nginx搭建IIS集群实现负载均衡

    长话短说,我们用Nginx来搭建一个简单的集群,实现Web应用的负载均衡,架构图如下: 两台Web服务器,一台静态资源服务器,因为是演示,我们以网站形式部署在本机IIS中 一台Nginx代理服务器,安 ...

  8. Nginx负载均衡中后端节点服务器健康检查的操作梳理

    正常情况下,nginx做反向代理,如果后端节点服务器宕掉的话,nginx默认是不能把这台realserver踢出upstream负载集群的,所以还会有请求转发到后端的这台realserver上面,这样 ...

  9. Nginx负载均衡中后端节点服务器健康检查的一种简单方式

    摘自:https://cloud.tencent.com/developer/article/1027287 一.利用nginx自带模块ngx_http_proxy_module和ngx_http_u ...

  10. nginx之rewrite重写,反向代理,负载均衡

    rewrite重写(伪静态): 在地址栏输入xx.com/user-xxx.html, 实际上访问的就是xxx.com/user.php?id=xxx rewrite就这么简单 附上ecshop re ...

随机推荐

  1. Codeforce 318A - Even Odds(数学水题)

    Being a nonconformist, Volodya is displeased with the current state of things, particularly with the ...

  2. 【每日一题】41. 德玛西亚万岁 (状态压缩DP)

    补题链接:Here 经典状压DP问题 坑点,注意多组输入... const int N = 16, mod = 100000000; int f[N][1 << N]; int a[N]; ...

  3. Java 多线程上下文传递在复杂场景下的实践

    一.引言 海外商城从印度做起,慢慢的会有一些其他国家的诉求,这个时候需要我们针对当前的商城做一个改造,可以支撑多个国家的商城,这里会涉及多个问题,多语言,多国家,多时区,本地化等等.在多国家的情况下如 ...

  4. 详解异步任务 | 看 Serverless Task 如何解决任务调度&可观测性中的问题

    在上篇文章<解密函数计算异步任务能力之「任务的状态及生命周期管理」>中,我们介绍了任务系统的状态管理,并介绍了用户应如何根据需求,对任务状态信息进行实时的查询等操作.在本篇中我们将会进一步 ...

  5. 六、mycat全局自增

    系列导航 一.Mycat实战---为什么要用mycat 二.Mycat安装 三.mycat实验数据 四.mycat垂直分库 五.mycat水平分库 六.mycat全局自增 七.mycat-ER分片 一 ...

  6. RLHF · PbRL | 速通 ICLR 2024 RLHF

    检索关键词:ICLR 2024.reinforcement learning.preference.human feedback. https://openreview.net/search?term ...

  7. Vue事件方法中this.属性名

    vue事件方法中访问data对象中的成员 : this.属性名 注意: 如果事件处理代码没有写到methods中,而是写在行内则不需要this.

  8. c# 编写 WebAssembly

    创建一个.net 7.0类库工程,引用下面的nuget包: <PackageReference Include="Microsoft.AspNetCore.Components.Web ...

  9. Nginx loki监控日志的学习

    Nginx loki监控日志的学习 背景 学习自: https://mp.weixin.qq.com/s/Qt1r7vzWvCcJpNDilWHuxQ 增加了一些自己的理解 第一部分nginx日志的完 ...

  10. [转帖]goproxy 使用说明

    Go 版本要求 建议您使用 Go 1.13 及以上版本, 可以在这里下载最新的 Go 稳定版本. 配置 Goproxy 环境变量 Bash (Linux or macOS) export GOPROX ...