wmproxy
wmproxy
已用Rust
实现http/https
代理, socks5
代理, 反向代理, 负载均衡, 静态文件服务器,websocket
代理,四层TCP/UDP转发,内网穿透等,会将实现过程分享出来,感兴趣的可以一起造个轮子
项目地址
国内: https://gitee.com/tickbh/wmproxy
github: https://github.com/tickbh/wmproxy
设计目标
负载均衡时通过匹配规则匹配正确的location进行处理相关的操作。
设计方案变更
初始设计方案
初始方案以最快的方式进行支持,仅支持前缀匹配,即如果配置
[[http.server.location]] rule = "/wmproxy"
那么当我们访问/wmproxy/xx
时将会被分配到该location,此方案相对简单,但是当我们碰到复杂的需求时将无法被满足。
设计方案需求
除了前缀匹配外,我们将会有其它各种需求的匹配:
- 后缀匹配 比如以wmproxy结尾的path,如
/api/update/wmproxy
需要匹配成*wmproxy
- 中间匹配 比如常用的api中间转化成数据
/api/
,那么匹配为/get /api/*/get
- 正则匹配 当前的配置的为正则规则,需进行匹配
- 请求方法匹配 比如仅当请求方法为
POST
才进行转发 - 客户端IP 比如仅当客户端内网或者外网时区分请求
- Host地址 比如当前如果请求为ip则不进行转发,需要匹配host才进行转发
- 协议 比如某个网站不支持
http
当我们匹配到http
时需强制转化成https
实际配置中当仅仅只有前缀匹配时已经显然无法满足我们的需求
设计方案迭代
当前我们就必须将数据进行更迭,但是在通常情况下我们又不想将配置变得复杂,此时就需要我们支持更多的类的自定义化,首先我们定义类:
/// location匹配,将根据该类的匹配信息进行是否匹配 #[serde_as] #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct Matcher { path: Option<String>, #[serde_as(as = "Option" )] client_ip: Option, #[serde_as(as = "Option" )] remote_ip: Option, host: Option<String>, #[serde_as(as = "Option" )] method: Option, #[serde_as(as = "Option" )] scheme: Option, }
此时我们将location中的rule的类型从String变成了Matcher,那么此时我们首先遇到的一个问题他可能为一个String值或者可能为一个Map值,我们先得对这种情况进行处理。
我们根据serde的提供的解析方案进行如下函数,当前我们重写了visit_str
及visit_map
表示我们将只支持这两种源格式转化成Matcher
pub fn string_or_struct<'de, T, D>(deserializer: D) -> Result where T: Deserialize<'de> + FromStr<Err = WebError>, D: Deserializer<'de>, { struct StringOrStruct(PhantomData<fn() -> T>); impl<'de, T> Visitor<'de> for StringOrStruct where T: Deserialize<'de> + FromStr<Err = WebError>, { type Value = T; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("string or map") } fn visit_str(self, value: &str) -> Result where E: de::Error, { Ok(FromStr::from_str(value).unwrap()) } fn visit_map(self, map: M) -> Result where M: MapAccess<'de>, { Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)) } } deserializer.deserialize_any(StringOrStruct(PhantomData)) }
其次我们将在location中做处理
/// 负载均衡中的location匹配,将匹配合适的处理逻辑 #[serde_as] #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LocationConfig { #[serde(deserialize_with = "string_or_struct")] pub rule: Matcher, //... }
由于这种大类的匹配通常会在别处额外定义,我们通过以@name
以@
开头来表示索引的信息,来简化配置。通过初始化的时候来重新初始化Matcher
处理匹配
我们初始化完Matcher之后,需要能正确的判断传入的数据是否当前能正确匹配。主要的复杂点在于path的匹配,主要为正则匹配、前缀匹配、中间匹配 、后缀匹配。
对其进行细分,可确定分为两种
- 正则匹配
- 带
*
的路径匹配- 前缀匹配可以看成
/start*
或者/start
- 中间匹配可以看成
/start*end
- 后缀匹配可以看成
*end
- 前缀匹配可以看成
即当前我们只需处理两种匹配模式:
- 正则匹配,频繁调用时主要在于初始化正则时可能会消耗大量的算力。当前我们对我们的匹配规则的正则进行缓存
/// may memory leak pub fn try_cache_regex(origin: &str) -> Option { // 因为均是从配置中读取的数据, 在这里缓存正则表达示会在总量上受到配置的限制 lazy_static! { static ref RE_CACHES: Mutex'static str, Option>> = Mutex::new(HashMap::new()); }; if origin.len() == 0 { return None; } if let Ok(mut guard) = RE_CACHES.lock() { if let Some(re) = guard.get(origin) { return re.clone(); } else { if let Ok(re) = Regex::new(origin) { guard.insert( Box::leak(origin.to_string().into_boxed_str()), Some(re.clone()), ); return Some(re); } } } return None; }
此处我们用到了static变量,也就是将某部分数据进行了静态化处理,且此处我们将String转化成了&'static str
可能存在一定的内存泄漏,大小值跟配置的数据有关,可以接受这空间换取时间。然后用正则的is_match进行匹配即可。
if let Some(re) = Helper::try_cache_regex(&p) { if !re.is_match(path) { return Ok(false); } }
- 带
*
的路径匹配 主要将路径中的*进行前进字符串的匹配。
在rust中的字符串切割主要由split
或者strip_prefix
或者strip_suffix
来处理,相对其它语言中均存在的subString
或者substr
在rust中的则表示为引用,所以在rust中不存在substring函数
let src = "wmproxy is good"; let first = &src[..7]; let second = &src[3..8]; let end = &src[8..]; let vals = src.split(" ").collect::<Vec<&str>>();
以上各数据均引用src的资源,即在这过程中并没有创建内存对象。
那么匹配函数则先将'*'
进行分割,数组的第一个则前缀匹配,最后一个则后缀匹配,若不存在'*'
则数组数量为1,符合前缀匹配。
pub fn is_match(src: &str, pattern: &str) -> bool { let mut oper = src; let vals = pattern.split("*").collect::<Vec<&str>>(); for i in 0..vals.len() { if i == 0 { if let Some(val) = oper.strip_prefix(vals[i]) { oper = val; } else { return false; } } else if i == vals.len() - 1 { if let Some(val) = oper.strip_suffix(vals[i]) { oper = val; } else { return false; } } else { if let Some(idx) = oper.find(vals[i]) { oper = &oper[idx + vals[i].len() .. ] } else { return false; } } } true }
那么完整的匹配函数在Matcher
/// 当本地限制方法时,优先匹配方法,在进行路径的匹配 pub fn is_match_rule(&self, path: &String, req: &RecvRequest) -> ProtResult<bool> { if let Some(p) = &self.path { let mut is_match = false; if Helper::is_match(&path, p) { is_match = true; } if !is_match { if let Some(re) = Helper::try_cache_regex(&p) { if !re.is_match(path) { return Ok(false); } } else { return Ok(false); } } } if let Some(m) = &self.method { if !m.0.contains(req.method()) { return Ok(false); } } if let Some(s) = &self.scheme { if !s.0.contains(req.scheme()) { return Ok(false); } } if let Some(h) = &self.host { match req.get_host() { Some(host) if &host == h => {}, _ => return Ok(false), } } if let Some(c) = &self.client_ip { match req.headers().system_get("{client_ip}") { Some(ip) => { let ip = ip .parse::<IpAddr>() .map_err(|_| ProtError::Extension("client ip error"))?; if !c.contains(&ip) { return Ok(false) } }, None => return Ok(false), } } Ok(true) }
小结
匹配规则在对于复杂匹配的时候尤为重要,我们可以轻松的将各个请求分配到合适的位置,此处我们着重介绍了正则匹配及带*
的路径匹配。
点击 [关注],[在看],[点赞] 是对作者最大的支持