wmproxy

wmproxy是由Rust编写,已实现http/https代理,socks5代理, 反向代理,静态文件服务器,内网穿透,配置热更新等, 后续将实现websocket代理等,同时会将实现过程分享出来, 感兴趣的可以一起造个轮子法

项目地址

gite: https://gitee.com/tickbh/wmproxy

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

配置数据

数据通常配置在配置文件中,如果需要变更配置,我们通常将配置文件进行更新,并通知程序重新加载配置以便生效。

nginx的变更方式

在nginx中,我们通常用

nginx -s reload

进行数据的安全无缝的重载。在nginx中,是多进程的模式,也就是在nginx -s reload信号发出后master进程通知之前的work进程停止接收新的流,也就是accpet暂停,但是会服务完当前的数据请求,并同时会启用新的work进程来接受新的请求

缺点:nginx只能整体的配置做全部重置,且无法查看当前的配置(除非看配置文件,配置可能被重新修改过和内存中的值可能不匹配)

当前选取的方式

当前选择的是用HTTP请求的方式,也就是对本地的端口进行监听(http://127.0.0.1:8837),对本地端口监听也不会造成对外暴露端口带来的安全问题,这样子可以高度的自定义。具有比较高的活跃性,也可以实时查询内存中的数据。

例如访问:

  • http://127.0.0.1:8837/reload即可通知目标进程重载当前的配置
  • http://127.0.0.1:8837/now即可以知道当前的所有的配置列表
  • http://127.0.0.1:8837/stop即可以关闭当前的进程,停止服务,类似于nginx中的nginx -s stop
  • http://127.0.0.1:8837/adapt加载当前配置,看是否错误,但是不进行应用。

    等功能。

功能实现的原理

  • 单进程

    单进程模式的缺点:如果存在内存泄漏之类的情况,无论如何重载进程都无法将内存恢复,会始终保持较高的内存值直到最终不可用的阶段。如果发生未正确处理的异常,可能会使该进程崩溃的风险,处于无服务状态。

    单进程模式的优点:在当前进程存储的一些有利于加速服务的将会很好的被保留下来(如健康检查的数据),异步进程里正在处理的数据等。无需进行进程间通讯,配合tokio的异步处理可以将单进程的优势完美发挥出来。

  • 端口复用

    无论哪种模式,都需要处理数据重载时,绑定对象的转移TcpListener或者重新绑定TcpListener,在Rust中转移绑定对象相对来说较麻烦后续如果拓展成多进程模式也无法进行转移,所以不考虑用转移所有权的问题。那么此时我们的解决方法就是set_reuse_addressset_reuse_port,不同平台该方法上有不同的表现,我们用的是socket2的封装,用该方法的注意事项:

  • 在windows平台上,不存在set_reuse_port方法,仅调用set_reuse_address即可实现一个地址多次绑定

  • 在linux上,不同的版本上,有些只需调用set_reuse_address即可端口复用,有些需要同时调用set_reuse_port

  • 在macos上,需要调用set_reuse_addressset_reuse_port函数才可实现端口复用

所以这里涉及一个分平台的编码,我们在此使用的是,这和C/C++中的#ifdef WINDOWS类似,但是只能在函数级的做调整,所以此处额外在封装了两个函数来做调用。

/// 非windows平台
#[cfg(not(target_os = "windows"))]
fn set_reuse_port(socket: &Socket, reuse: bool) -> io::Result<()> {
socket.set_reuse_port(true)?;
Ok(())
} /// windows平台,空实现
#[cfg(target_os = "windows")]
fn set_reuse_port(_socket: &Socket, _sreuse: bool) -> io::Result<()> {
Ok(())
}

然后将原来的TcpListener::bind(addr)函数改成Helper::bind即可无缝切换到支持端口复用的功能,针对代理端及反向代理端:

/// 可端口复用的绑定方式,该端口可能被多个进程同时使用
pub async fn bind<A: ToSocketAddrs>(addr: A) -> io::Result<TcpListener> {
let addrs = addr.to_socket_addrs()?;
let mut last_err = None;
for addr in addrs {
let socket = Socket::new(Domain::IPV4, Type::STREAM, None)?;
socket.set_nonblocking(true)?;
let _ = socket.set_only_v6(false);
socket.set_reuse_address(true)?;
Self::set_reuse_port(&socket, true)?;
socket.bind(&addr.into())?;
match socket.listen(128) {
Ok(_) => {
let listener: std::net::TcpListener = socket.into();
return TcpListener::from_std(listener);
}
Err(e) => {
log::info!("绑定端口地址失败,原因: {:?}", addr);
last_err = Some(e);
}
}
} Err(last_err.unwrap_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"could not resolve to any address",
)
}))
}

测试功能

测试配置加载reload,一开始我们绑定81的端口

进程启动后改为绑定82的端口,然后调用reload(curl.exe http://127.0.0.1:8837/reload)

此时,再调用stop(curl.exe http://127.0.0.1:8837/stop),正确的预期应该显示关闭,且82端口不可再访问

符合功能预期,初步测试完毕

相关源码实现

以下是启动及发送重载配置的流程示意图

flowchart TD
A[加载配置]
B[绑定端口]
C[控制端]
D[服务1]
E[服务2]
F[控制窗户端]

A -->|加载数据后绑定| B
B -->|"(1)绑定端口后启动"| C
B -->|"(1)异步的方式启动"| D
F -->|发送重载入命令| C
C -->|"(3)发送关闭服务命令"| D
C -->|"(2)启动新的服务后关闭原服务"| E

以下是中控的定义,消息的通知主要通过Sender/Receiver来进行数据的通知。

/// 控制端,可以对配置进行热更新
pub struct ControlServer {
/// 控制端当前的配置文件,如果部分修改将直接修改数据进行重启
option: ConfigOption,
/// 通知服务进行关闭的Sender,服务相关如果收到该消息则停止Accept
server_sender_close: Option<Sender<()>>,
/// 通知中心服务的Sender,每个服务拥有一个该Sender,可反向通知中控关闭
control_sender_close: Sender<()>,
/// 通知中心服务的Receiver,收到一次则将当前的引用计数-1,如果为0则表示需要关闭服务器
control_receiver_close: Option<Receiver<()>>,
/// 服务的引用计数
count: i32,
}

启动控制终端,接收HTTP的指令和关闭的指令,此时control已经变成了Arc<Mutex<ControlServer>>,方便在各各线程间传播,同步修改数据。

pub async fn start_control(control: Arc<Mutex<ControlServer>>) -> ProxyResult<()> {
let listener = {
let value = &control.lock().await.option;
TcpListener::bind(format!("127.0.0.1:{}", value.control)).await?
}; loop {
let mut receiver = {
let value = &mut control.lock().await;
value.control_receiver_close.take()
}; tokio::select! {
Ok((conn, addr)) = listener.accept() => {
let cc = control.clone();
tokio::spawn(async move {
let mut server = Server::new_data(conn, Some(addr), cc);
if let Err(e) = server.incoming(Self::operate).await {
log::info!("反向代理:处理信息时发生错误:{:?}", e);
}
});
let value = &mut control.lock().await;
value.control_receiver_close = receiver;
}
_ = Self::receiver_await(&mut receiver) => {
let value = &mut control.lock().await;
value.count -= 1;
log::info!("反向代理:控制端收到关闭信号,当前:{}", value.count);
if value.count <= 0 {
break;
}
value.control_receiver_close = receiver;
}
}
}
Ok(())
}

处理相关消息:

if req.path() == "/reload" {
// 将重新启动服务器
let _ = value.do_restart_serve().await;
return Ok(Response::text()
.body("重新加载配置成功")
.unwrap()
.into_type());
} if req.path() == "/stop" {
// 通知控制端关闭,控制端阻塞主线程,如果控制端退出后进程退出
if let Some(sender) = &value.server_sender_close {
let _ = sender.send(()).await;
}
return Ok(Response::text()
.body("关闭进程成功")
.unwrap()
.into_type());
}

以下是主要的启动代码:

async fn inner_start_server(&mut self, option: ConfigOption) -> ProxyResult<()>  {
let sender = self.control_sender_close.clone();
let (sender_no_listen, receiver_no_listen) = channel::<()>(1);
let sender_close = self.server_sender_close.take();
// 每次启动的时候将让控制计数+1
self.count += 1;
tokio::spawn(async move {
let mut proxy = Proxy::new(option);
// 将上一个进程的关闭权限交由下一个服务,只有等下一个服务准备完毕的时候才能关闭上一个服务
if let Err(e) = proxy.start_serve(receiver_no_listen, sender_close).await {
log::info!("处理失败服务进程失败: {:?}", e);
}
// 每次退出的时候将让控制计数-1,减到0则退出
let _ = sender.send(()).await;
});
self.server_sender_close = Some(sender_no_listen);
Ok(())
}

结语

此时以不同于nginx的另一种配置的加载已经开发完毕,配置的热加载可以让您更从容的保护好您的系统。

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

19. 从零开始编写一个类nginx工具, 配置数据的热更新原理及实现的更多相关文章

  1. 从零开始编写一个BitTorrent下载器

    从零开始编写一个BitTorrent下载器 BT协议 简介 BT协议Bit Torrent(BT)是一种通信协议,又是一种应用程序,广泛用于对等网络通信(P2P).曾经风靡一时,由于它引起了巨大的流量 ...

  2. 22.编写一个类A,该类创建的对象可以调用方法showA输出小写的英文字母表。然后再编写一个A类的子类B,子类B创建的对象不仅可以调用方法showA输出小写的英文字母表,而且可以调用子类新增的方法showB输出大写的英文字母表。最后编写主类C,在主类的main方法 中测试类A与类B。

    22.编写一个类A,该类创建的对象可以调用方法showA输出小写的英文字母表.然后再编写一个A类的子类B,子类B创建的对象不仅可以调用方法showA输出小写的英文字母表,而且可以调用子类新增的方法sh ...

  3. 35.按要求编写Java程序: (1)编写一个接口:InterfaceA,只含有一个方法int method(int n); (2)编写一个类:ClassA来实现接口InterfaceA,实现int method(int n)接口方 法时,要求计算1到n的和; (3)编写另一个类:ClassB来实现接口InterfaceA,实现int method(int n)接口 方法时,要求计算n的阶乘(n

      35.按要求编写Java程序: (1)编写一个接口:InterfaceA,只含有一个方法int method(int n): (2)编写一个类:ClassA来实现接口InterfaceA,实现in ...

  4. 编写一个类,其中包含一个排序的方法Sort(),当传入的是一串整数,就按照从小到大的顺序输出,如果传入的是一个字符串,就将字符串反序输出。

    namespace test2 { class Program { /// <summary> /// 编写一个类,其中包含一个排序的方法Sort(),当传入的是一串整数,就按照从小到大的 ...

  5. 二、 编写一个类,用两个栈实现队列,支持队列的基本操作(add,poll,peek)

    请指教交流! package com.it.hxs.c01; import java.util.Stack; /* 编写一个类,用两个栈实现队列,支持队列的基本操作(add,poll,peek) */ ...

  6. 如何编写一个SQL注入工具

    0x01  前言 一直在思考如何编写一个自动化注入工具,这款工具不用太复杂,但是可以用最简单.最直接的方式来获取数据库信息,根据自定义构造的payload来绕过防护,这样子就可以. 0x02 SQL注 ...

  7. 从零开始编写一个vue插件

    title: 从零开始编写一个vue插件 toc: true date: 2018-12-17 10:54:29 categories: Web tags: vue mathjax 写毕设的时候需要一 ...

  8. 题目一:编写一个类Computer,类中含有一个求n的阶乘的方法

    作业:编写一个类Computer,类中含有一个求n的阶乘的方法.将该类打包,并在另一包中的Java文件App.java中引入包,在主类中定义Computer类的对象,调用求n的阶乘的方法(n值由参数决 ...

  9. 一个简单的工具开发:从学生端更新程序部署工具说起,浅谈qt中自定义控件制作和调用、TCP协议下文件的收发 、以及可执行文件的打包

    一个简单的工具开发:从学生端更新程序部署工具说起,浅谈qt中ui的使用和TCP协议下文件的收发.以及可执行文件的打包 写在前面,Qt Designer是一个非常操蛋的页面编辑器,它非常的...怎么说呢 ...

  10. 从零开始学 Java - 利用 Nginx 负载均衡实现 Web 服务器更新不影响访问

    还记得那些美妙的夜晚吗 你洗洗打算看一个小电影就睡了,这个时候突然想起来今天晚上是服务器更新的日子,你要在凌晨时分去把最新的代码更新到服务器,以保证明天大家一觉醒来打开网站,发现昨天的 Bug 都不见 ...

随机推荐

  1. Linux网络设备命名规则简介

    Linux网络设备命名规则简介 几年前, Linux内核为网络接口分配名称采用的是一种简单和直观的方式:一个固定的前缀和一个递增的序号.比如,内核使用eth0名称以标识启动后第一个加载的网络设备,第二 ...

  2. 云储存选择做Hexo博客图床(腾讯云、七牛云、网易云)

    前言 博客里需要添加很多图片作为内容的补充,但是把图片放在本地博客文件夹里,上传到网上后,加载这些图片就是一个很大的问题,他们会拖累网页加载的速度,所以建议把图片放图床里,通过外链来访问和加载这些图片 ...

  3. .NET Core中关于阿拉伯语环境下的坑:Input string was not in a correct format.

    结论 .NET Core项目(.NET Framework没出现)在阿拉伯语(即语言名称是ar-开头的语言)环境下,将负数字符串转成数字,即int.Parse("-1")或Conv ...

  4. Swithch反汇编(四种)

    ------------恢复内容开始------------ Switch语法格式 Switch(表达式) { case 常量表达式1: 语句; break; case 常量表达式2: 语句; bre ...

  5. 烧死10亿脑细胞的SQL长啥样?

    1 前言 今天在生产中碰到了一个让我十分费解的 SQL,十分有趣. 2 现象 SQL 很好复现,就是逻辑看起来有点唬人 postgres=# create table test(id1 int,id2 ...

  6. Linux下定时清空某个文件

    问题 在一台单点机器部署完成且运行一段时间后,发现页面接口报错,登上机器发现磁盘满了.通过du -lh --max-depth=1 和 du -sh * 找出是哪个文件. 发现是 项目中 stdout ...

  7. Windows 下搭建 Appium + Android+python 自动化测试环境

    前言 本来并不打算写这么一篇文章,但是实践下来发现网上的各种教程里大致有两个问题.一是文章有些跟不上时代,目前android开发和测试的技术更新都比较快,内容有些过期.二是细节部分不是太完整,拼拼凑凑 ...

  8. Scala的基本使用

    @ 目录 Scala的基本使用 一.基础语法 1.1 变量 1.1.1 var和val 1.1.2 自动类型推断 1.2 数据类型 1.2.1 基础数据类型 1.2.2 增强数据类型 1.3 操作符 ...

  9. Linux:通过ssh访问虚拟机

    好记性抵不过烂笔头.(过段时间用的时候,总要百度一下,别费这个时间了) 语法: ssh -p port username@IP #默认22端口,可省略"-p port",即:ssh ...

  10. go-zero 是如何做路由管理的?

    原文链接: go-zero 是如何做路由管理的? go-zero 是一个微服务框架,包含了 web 和 rpc 两大部分. 而对于 web 框架来说,路由管理是必不可少的一部分,那么本文就来探讨一下 ...