码农知识堂 - 1000bd
  •   Python
  •   PHP
  •   JS/TS
  •   JAVA
  •   C/C++
  •   C#
  •   GO
  •   Kotlin
  •   Swift
  • 19. 从零开始编写一个类nginx工具, 配置数据的热更新原理及实现


    合集 - wmproxy(18)
    1.用Rust手把手编写一个Proxy(代理), 动工09-192.用Rust手把手编写一个Proxy(代理), UDP绑定篇09-213.5. 用Rust手把手编写一个Proxy(代理), 通讯协议建立, 为内网穿透做准备09-284.6. 用Rust手把手编写一个wmproxy(代理,内网穿透等), 通讯协议源码解读篇09-305.7. 用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP及TCP内网穿透原理及运行篇10-046.8. 用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP改造篇之HPACK原理10-087. 用Rust手把手编写一个Proxy(代理), 准备篇, 动手造轮子09-168.9. 用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP2改造篇之HPACK示例, 了解http2头信息如何处理10-099.10. 用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP内网穿透支持修改头信息10-1010.11. 用Rust手把手编写一个wmproxy(代理,内网穿透等), 实现健康检查10-1211.12. 用Rust手把手编写一个wmproxy(代理,内网穿透等), TLS的双向认证信息及token验证10-1412.13. 从零开始编写一个类nginx工具, HTTP中的压缩gzip,deflate,brotli算法10-1713.14. 从零开始编写一个类nginx工具, HTTP文件服务器的实现过程及参数10-1914.15. 从零开始编写一个类nginx工具, 如果将nginx.conf转成yaml,toml,json会怎么样10-2015.16. 从零开始编写一个类nginx工具, 反向代理upstream源码实现10-2316.17. 从零开始编写一个类nginx工具, Rust中一些功能的实现10-2417.18. 从零开始编写一个类nginx工具, 主动式健康检查源码实现10-26
    18.19. 从零开始编写一个类nginx工具, 配置数据的热更新原理及实现10-27
    收起

    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(addr: A) -> io::Result {
    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,服务相关如果收到该消息则停止Accept
    server_sender_close: Option>,
    /// 通知中心服务的Sender,每个服务拥有一个该Sender,可反向通知中控关闭
    control_sender_close: Sender<()>,
    /// 通知中心服务的Receiver,收到一次则将当前的引用计数-1,如果为0则表示需要关闭服务器
    control_receiver_close: Option>,
    /// 服务的引用计数
    count: i32,
    }

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

    pub async fn start_control(control: Arc>) -> 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的另一种配置的加载已经开发完毕,配置的热加载可以让您更从容的保护好您的系统。

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

  • 相关阅读:
    学习ASP.NET Core Blazor编程系列十——路由(上)
    C语言深入理解指针(非常详细)(二)
    双十一买什么比较划算?四款实用性超强不吃灰的数码好物推荐
    将神经网络粒子化的内在合理性
    智慧农业大数据平台:农业中的“大智慧”
    【JVM技术专题】精心准备了一套JVM分析工具的锦囊「中篇」
    vue基础知识七:SPA首屏加载速度慢的怎么解决?
    PhaGCN2:病毒聚类
    Linux系统编程-进程等待
    顺丰快递商家寄件发货接口API类型和接入流程方案【快递100接口Demo】
  • 原文地址:https://www.cnblogs.com/wmproxy/p/17792338.html
  • 最新文章
  • 沪漂五周年了:我越来越迷茫了
    Agentic Skill Routing 实战:别再把所有 Skill 塞进 AI Agent 上下文
    MySQL-Seconds_behind_master的精度误差
    [MAF预定义ChatClient中间件-03]CachingChatClient——利用缓存省钱省时间
    AI的至暗历史:从万众期待到被政府撤资,AI的两次死亡徘徊
    Agent OS :五种驯服不确定性的范式
    PortSwigger SQL注入LAB11
    数据库即时编译JIT
    [Begin]AI Learn Data Day 0
    深度学习进阶(二十七)现代 LLM 的核心架构设计其二:SwiGLU
  • 热门文章
  • 十款代码表白小特效 一个比一个浪漫 赶紧收藏起来吧!!!
    奉劝各位学弟学妹们,该打造你的技术影响力了!
    五年了,我在 CSDN 的两个一百万。
    Java俄罗斯方块,老程序员花了一个周末,连接中学年代!
    面试官都震惊,你这网络基础可以啊!
    你真的会用百度吗?我不信 — 那些不为人知的搜索引擎语法
    心情不好的时候,用 Python 画棵樱花树送给自己吧
    通宵一晚做出来的一款类似CS的第一人称射击游戏Demo!原来做游戏也不是很难,连憨憨学妹都学会了!
    13 万字 C 语言从入门到精通保姆级教程2021 年版
    10行代码集2000张美女图,Python爬虫120例,再上征途
小工具 小游戏
Copyright © 2022 侵权请联系2656653265@qq.com    京ICP备2022015340号-1

京公网安备 11010502049817号