19. 从零开始编写一个类nginx工具, 配置数据的热更新原理及实现
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_address及set_reuse_port,不同平台该方法上有不同的表现,我们用的是socket2的封装,用该方法的注意事项:在windows平台上,不存在
set_reuse_port方法,仅调用set_reuse_address即可实现一个地址多次绑定在linux上,不同的版本上,有些只需调用
set_reuse_address即可端口复用,有些需要同时调用set_reuse_port在macos上,需要调用
set_reuse_address和set_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端口不可再访问

符合功能预期,初步测试完毕
相关源码实现
以下是启动及发送重载配置的流程示意图
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工具, 配置数据的热更新原理及实现的更多相关文章
- 从零开始编写一个BitTorrent下载器
从零开始编写一个BitTorrent下载器 BT协议 简介 BT协议Bit Torrent(BT)是一种通信协议,又是一种应用程序,广泛用于对等网络通信(P2P).曾经风靡一时,由于它引起了巨大的流量 ...
- 22.编写一个类A,该类创建的对象可以调用方法showA输出小写的英文字母表。然后再编写一个A类的子类B,子类B创建的对象不仅可以调用方法showA输出小写的英文字母表,而且可以调用子类新增的方法showB输出大写的英文字母表。最后编写主类C,在主类的main方法 中测试类A与类B。
22.编写一个类A,该类创建的对象可以调用方法showA输出小写的英文字母表.然后再编写一个A类的子类B,子类B创建的对象不仅可以调用方法showA输出小写的英文字母表,而且可以调用子类新增的方法sh ...
- 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 ...
- 编写一个类,其中包含一个排序的方法Sort(),当传入的是一串整数,就按照从小到大的顺序输出,如果传入的是一个字符串,就将字符串反序输出。
namespace test2 { class Program { /// <summary> /// 编写一个类,其中包含一个排序的方法Sort(),当传入的是一串整数,就按照从小到大的 ...
- 二、 编写一个类,用两个栈实现队列,支持队列的基本操作(add,poll,peek)
请指教交流! package com.it.hxs.c01; import java.util.Stack; /* 编写一个类,用两个栈实现队列,支持队列的基本操作(add,poll,peek) */ ...
- 如何编写一个SQL注入工具
0x01 前言 一直在思考如何编写一个自动化注入工具,这款工具不用太复杂,但是可以用最简单.最直接的方式来获取数据库信息,根据自定义构造的payload来绕过防护,这样子就可以. 0x02 SQL注 ...
- 从零开始编写一个vue插件
title: 从零开始编写一个vue插件 toc: true date: 2018-12-17 10:54:29 categories: Web tags: vue mathjax 写毕设的时候需要一 ...
- 题目一:编写一个类Computer,类中含有一个求n的阶乘的方法
作业:编写一个类Computer,类中含有一个求n的阶乘的方法.将该类打包,并在另一包中的Java文件App.java中引入包,在主类中定义Computer类的对象,调用求n的阶乘的方法(n值由参数决 ...
- 一个简单的工具开发:从学生端更新程序部署工具说起,浅谈qt中自定义控件制作和调用、TCP协议下文件的收发 、以及可执行文件的打包
一个简单的工具开发:从学生端更新程序部署工具说起,浅谈qt中ui的使用和TCP协议下文件的收发.以及可执行文件的打包 写在前面,Qt Designer是一个非常操蛋的页面编辑器,它非常的...怎么说呢 ...
- 从零开始学 Java - 利用 Nginx 负载均衡实现 Web 服务器更新不影响访问
还记得那些美妙的夜晚吗 你洗洗打算看一个小电影就睡了,这个时候突然想起来今天晚上是服务器更新的日子,你要在凌晨时分去把最新的代码更新到服务器,以保证明天大家一觉醒来打开网站,发现昨天的 Bug 都不见 ...
随机推荐
- LSP协议被劫持,导致无法上网
QQ无法登录,网页打不开 用火绒的断网修复 说已经修复了 结果屁用没有 然后找的百度经验 管理员打开命令行窗口 输入 netsh winsock reset catalog 重启即生效
- 2023年ccpc河南省程序设计竞赛-clk
很荣幸能够参加这次比赛,比赛机会挺难得得,还是第一次线下参加这样的大型比赛,比赛体验自然无话可说比较刺激..这次比赛我和队友crf和nhr共同解决了三道题,参与感极差,可以说问题很大,最简单的签到题我 ...
- 【go笔记】使用sqlx操作MySQL
前言 go在操作MySQL时,可以使用ORM(比如gorm.xorm),也可以使用原生sql.本文以使用sqlx为例,简单记录步骤. go version: 1.16 安装相关库 # mysql驱动 ...
- JVM常用运行时参数说明
前言 仅列出常用JVM调优参数,更多请转文末的官方文档链接. 堆内存 -Xmx,设置最大堆内存,默认为物理内存的1/4.示例:-Xmx4096m,设置为4G -Xms,设置初始内存,默认为物理内存的1 ...
- JavaScript的Map和WeakMap
熟悉JavaScript的Map和WeakMap Map Map的键/值可以是任何类型 基本API 初始化映射: //使用new关键字和Map构造函数进行初始化 const m1 = new Map( ...
- IDA的使用2
IDA的使用2 string类型的选择 Rename 要注意如果再namelist和public name里面是不能重名 操作数 这个主要和开发结合精密, change sign-改变符号 bitwi ...
- BUGKU逆向reverse 1-8题
练习IDA两年半 打开尘封已久的bugku,从题目中练习使用,现在都已经是新版本了 orz 入门逆向 运行baby.exe将解压后的baby.exe拖到IDA里面主函数中找到mov指令 可以看到这里就 ...
- Go 并发编程 - Goroutine 基础 (一)
基础概念 进程与线程 进程是一次程序在操作系统执行的过程,需要消耗一定的CPU.时间.内存.IO等.每个进程都拥有着独立的内存空间和系统资源.进程之间的内存是不共享的.通常需要使用 IPC 机制进行数 ...
- XV6中的锁:MIT6.s081/6.828 lectrue10:Locking 以及 Lab8 locks Part1 心得
这节课程的内容是锁(本节只讨论最基础的锁).其实锁本身就是一个很简单的概念,这里的简单包括 3 点: 概念简单,和实际生活中的锁可以类比,不像学习虚拟内存时,现实世界中几乎没有可以类比的对象,所以即使 ...
- 小红书获得小红书笔记详情 API 返回值说明
item_get_video-获得小红书笔记详情 注册开通 smallredbook.item_get_video 公共参数 名称 类型 必须 描述 key String 是 调用key(必须以 ...