19. 从零用Rust编写正反向代理, 配置数据的热更新原理及实现

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端口不可再访问
在这里插入图片描述

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

相关源码实现

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

加载数据后绑定
(1)绑定端口后启动
(1)异步的方式启动
发送重载入命令
(3)发送关闭服务命令
(2)启动新的服务后关闭原服务
加载配置
绑定端口
控制端
服务1
服务2
控制窗户端

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

/// 控制端,可以对配置进行热更新
pub struct ControlServer {/// 控制端当前的配置文件,如果部分修改将直接修改数据进行重启option: ConfigOption,/// 通知服务进行关闭的Sender,服务相关如果收到该消息则停止Acceptserver_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();// 每次启动的时候将让控制计数+1self.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的另一种配置的加载已经开发完毕,配置的热加载可以让您更从容的保护好您的系统。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/342054.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

数字图像处理练习题

数字图像处理练习题 文章目录 数字图像处理练习题第 一 章1.什么是数字图像?2.数字图像有哪些特点?3.数字图像处理的目的是什么?4.简述数字图像的历史。5.数字图像有哪些主要应用?6.列举生活中数字图像的获得途径。7.结合自己的生活实例,举出一个数字图像的应用实例8.数字图…

Unity——VContainer的依赖注入

一、IOC控制反转和DI依赖倒置 1、IOC框架核心原理是依赖倒置原则 C#设计模式的六大原则 使用这种思想方式&#xff0c;可以让我们无需关心对象的生成方式&#xff0c;只需要告诉容器我需要的对象即可&#xff0c;而告诉容器我需要对象的方式就叫做DI&#xff08;依赖注入&…

centos8部署MySQL5.7故障集

转载说明&#xff1a;如果您喜欢这篇文章并打算转载它&#xff0c;请私信作者取得授权。感谢您喜爱本文&#xff0c;请文明转载&#xff0c;谢谢。 在centos8系统上安装MySQL&#xff0c;使用的是centos7上安装MySQL的脚本&#xff0c;出现了以下问题&#xff0c;以做记录&…

工程监测仪器振弦采集仪的应用及技术研究

工程监测仪器振弦采集仪的应用及技术研究 振弦采集仪是一种常用于工程监测的仪器&#xff0c;主要用于测量振动信号的频率、振幅及相位等参数。其应用和技术研究主要包括以下几个方面&#xff1a; 1. 结构监测&#xff1a;振弦采集仪可以用于对建筑物、桥梁、塔楼等结构物的振…

格密码:如何找最近的格点(CVP问题)

目录 一. 摘要 二. 介绍 2.1 简单的CVP问题 2.2 Gram-Schmidt向量 2.3 KZ基 三. 格密码的基本符号 四. CVP问题的发展 五. 如何解决CVP问题 5.1 随机取整算法 5.2 Babai算法随机取整 5.3 小结 六. 推荐论文 一. 摘要 本文章将解释如何利用随机取整算法&#xff08…

设计模式-数据映射模式

设计模式专栏 模式介绍模式特点应用场景技术难点代码示例Java实现数据映射模式Python实现数据映射模式 数据映射模式在spring中的应用 模式介绍 数据映射模式是一种将对象和数据存储映射起来的数据访问方式。具体来说&#xff0c;对一个对象的操作会映射为对数据存储的操作。这…

应急管理蓝皮书 |《应急预案数字化建设现状和发展建议》上篇

《应急预案数字化建设现状和发展建议》&#xff1a;297-313页 导读&#xff1a;《中国应急管理发展报告》系列蓝皮书由中央党校&#xff08;国家行政学院&#xff09;应急管理培训中心&#xff08;中欧应急管理学院&#xff09;联合社会科学文献出版社研创出版&#xff0c;本着…

2023一带一路暨金砖国家技能发展与技术创新大赛“网络安全”赛项省选拔赛样题卷②

2023金砖国家职业技能竞赛"网络安全" 赛项省赛选拔赛样题 2023金砖国家职业技能竞赛 省赛选拔赛样题第一阶段&#xff1a;职业素养与理论技能项目1. 职业素养项目1. 职业素养项目2. 网络安全项目3. 安全运营 第二阶段&#xff1a;安全运营项目1. 操作系统安全配置与加…

electron+vue网页直接播放RTSP视频流?

目前大部分摄像头都支持RTSP协议&#xff0c;但是在浏览器限制&#xff0c;最新版的浏览器都不能直接播放RTSP协议&#xff0c;Electron 桌面应用是基于 Chromium 内核的&#xff0c;所以也不能直接播放RTSP&#xff0c;但是我们又有这个需求怎么办呢&#xff1f; 市场上的方案…

07- OpenCV:模糊图像

目录 一、模糊原理 二、模糊的相关处理方法&#xff1a; 1、均值滤波&#xff08;归一化盒子滤波&#xff09; 2、高斯滤波&#xff08;正态分布的形状&#xff09; 3、中值模糊 4、双边模糊算法&#xff08;美容软件&#xff09; 5、相关代码&#xff1a; 6、几种模糊算法的比…

docker常用指令

一、目录 docker 指令 1. 创建镜像 2. 保存镜像 3. 加载镜像 4. 删除容器 5. 删除镜像 6. 查看容器日志 7. 查看docker 占用内存情况 8. 查看docker 根目录(安装路径) 9. 创建一个容器 二、实现 docker指令 1 创建镜像 docker commit fd69960ed65f jyf&#xff1a;0.0.1保…

VM无法连接U盘

打开Ubuntu系统的文件夹 找到以 .vmx结尾的文件 搜索 usb.restrictions.defaultAllow的选项 讲 FALSE 改成 TRUE