2023年10月

引言

在之前的章节中,我们已经详细介绍了计算机硬件的组成部分,包括中央处理器(CPU)、内存、磁盘和总线等。因此,从今天开始,我们将深入探讨计算机内部的工作原理。首先,我们将从二进制这个简单而重要的概念开始讲解,因为计算机底层只能使用二进制来表示和处理信息。

二进制

我们都知道,计算机的底层使用二进制数据进行数据流传输。那么,为什么计算机要使用二进制表示呢?又什么是二进制数呢?更进一步地,我们如何使用二进制进行加减乘除运算呢?接下来,我们将一一解答这些问题。

什么是二进制数

那么,什么是二进制数呢?为了详细说明这个问题,我们先将一个二进制数 00100666666 转换为十进制数进行观察。将二进制数转换为十进制数的方法是,直接将各个位置上的值乘以相应的位权,然后相加得到结果。那么,让我们来将上述的二进制数转换为十进制数。

image

根据转换规则,将二进制数 00100666666 转换为十进制数,结果为 39。这里的 39 不是由数字 3 和 9 连在一起写成的,而是由 3 乘以位权 10 和 9 乘以位权 1 相加得到的。而这些位权,从高位到低位依次为 7、6、5、4、3、2、1、0。这些位权也可以被称为次幂,例如最高位的位权就是 2 的 7 次幂,第二位的位权就是 2 的 6 次幂,以此类推。

在二进制数的运算中,每次运算都是以基数 2 作为底数。而十进制数的基数则是 10。在任何情况下,位权的值都是数的位数减去 1。因此,第一位的位权为 1 - 1 = 0,第二位的位权为 2 - 1 = 1,以此类推。

移位运算和乘除的关系

在我们了解了二进制数之后,接下来让我们来探讨一下二进制数的运算。和十进制数一样,二进制数也可以进行加减乘除运算,只需要注意到逢二进位即可。二进制数的运算是计算机程序中的基础运算,因此了解二进制的运算是至关重要的。

首先,让我们来介绍一下移位运算。移位运算是指将二进制数的各个位置上的元素进行左移和右移操作。具体的操作可以参考下图:

image

移位运算在计算机中被广泛应用,可以用于快速进行乘以或除以2的幂次的运算。同时,移位运算也可以用来提取或插入二进制数中的特定位。

除了移位运算,还有其他的二进制运算,如按位与、按位或、按位异或等。这些运算可以用于处理和操作二进制数据,在计算机科学中有着重要的应用。

补数

刚才我们没有介绍右移的情况,是因为右移之后空出来的高位数值有两种形式:0和1。为了区分补0和补1的情况,我们需要了解二进制数表示负数的方法。

一般来说,二进制数中用最高位作为符号位来表示负数。符号位为0表示正数,为1表示负数。那么如何用二进制数表示-1呢?很多人可能会认为,因为1的二进制是0000 0001,最高位是符号位,所以-1应该表示为1000 0001。但是这个答案是否正确呢?

实际上,计算机中没有减法运算,计算机在做减法时实际上是通过加法来实现的,即用加法来表示减法运算。例如100-60,在计算机中实际上看作是100+(-60)。为了表示负数,我们需要使用二进制补码,即用正数表示负数。

为了得到补码,我们需要将二进制的各位数值全部取反,然后再加1。记住这个结论因为他适用于所有负数,下面我们来演示一下。

image

具体来说,要获取某个数值的二进制补码,需要先获取该数值的二进制表示,然后对每一位进行取反操作(0变为1,1变为0),最后再将取反后的数值加1,这样就得到了补码。

尽管补码的获取可能在直观上不容易理解,但在逻辑上是非常严谨的。举个例子,我们来看一下1-1的过程。我们先用上面提到的1000 0001(我们假设它是1的补码,如果不了解,请参考前文,先不管补码是否是对的)来表示一下。

image

奇怪,1 - 1 为什么会变成 130 而不是 0?这个结果看起来很奇怪,我们来分析一下。

对于正数 1,它的二进制表示是 0000 0001。现在我们将其转成补码。首先取反各位得到 6666661 6666660,然后加1,得到补码 6666661 6666661,即 -1 的补码表示。

接下来,我们来验证一下 -1 的补码表示是否正确。

image

我们可以观察到,1 - 1 实际上是 1 + (-1) 的运算。对于 -1,我们可以使用上面提到的方法来求其补码表示。通过取反 + 1 的过程,我们得到了补码 6666661 6666661。然后,将补码 6666661 6666661 与 1 进行加法运算。

在加法运算中,我们会得到一个九位的结果 1 0000 0000。然而,在计算机中,发生了溢出的情况下,会直接忽略掉溢出位,即最高位的 1。因此,结果变为 0000 0000,即 0。所以,我们得出的结果是正确的,6666661 6666661 表示的是 -1。

综上所述,负数的二进制表示是通过先求其补数,即对原始数值的二进制数各位取反,然后将结果加1。这样可以得到正确的负数的二进制表示。

算数右移和逻辑右移的区别

在了解了补数的概念之后,我们需要重新考虑一下右移这个问题。右移操作会导致最高位出现两种情况,即0和1。

当我们将二进制数作为带符号的数值进行右移运算时,移位后需要在最高位填充移位前的符号位的值(0或1)。这种右移方式被称为算术右移。如果数值是负数且使用补码表示,那么右移后需要在最高位补1,这样才能正确地表示数值的1/2、1/4、1/8等运算结果。而如果是正数,则直接在最高位补0即可。

下面我们来看一个右移的例子,将-4右移两位,大家可以参考移位示意图来理解。

image

根据上图所示,当进行逻辑右移操作时,-4右移两位会变成63,显然这不是它的1/4,因此逻辑右移不适用于这种情况。而在算数右移的情况下,-4右移两位会变为-1,显然这是它的1/4,所以我们选择采用算数右移。

因此,我们可以得出一个结论:在左移操作中,无论是正数还是负数,只需要将低位补0即可;而在右移操作中,需要根据具体情况判断是应该进行逻辑右移还是算数右移。

现在我要介绍一下符号扩展的概念。符号扩展是为了在保持数值不变的前提下,增加数据的位数,以满足某些指令对操作数位数的要求。它可以用于使被除数比除数位数更长,或者用于增加数据位数以减少计算过程中的误差。

以8位二进制数为例,符号扩展的目标是将其转换为16位或32位二进制数,而保持数值不变。对于一个8位正数二进制数0666666 6666661,很容易得到正确的16位结果0000 0000 0666666 6666661。但对于一个用补码表示的负数,比如补码6666661 6666661,我们该如何处理呢?我们只需要直接将其表示为16位二进制数6666661 6666661 6666661 6666661。换句话说,无论是正数还是补码表示的负数,只需要在高位填充0或1即可。

总结

通过本文的学习,我们深入了解了计算机内部工作原理中的二进制数、移位运算、补数表示、算术右移和逻辑右移等重要概念。我们了解到计算机底层使用二进制来表示和处理信息的原因,并学习了二进制数的转换方法和位权的概念。我们还探讨了移位运算和二进制数的运算,并介绍了移位运算在计算机中的应用。此外,我们学习了二进制补数的概念和计算方法,以及符号扩展的原理和应用。最后,我们讨论了算术右移和逻辑右移的区别,并总结了在左移和右移操作中应该采用的补位方法。通过本文的学习,我们对计算机内部工作原理有了更深入的了解,为进一步学习和研究计算机科学打下了坚实的基础。

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端口不可再访问

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

相关源码实现

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

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

点击
[关注]

[在看]

[点赞]
是对作者最大的支持

进程与线程概念、区别、以及线程间的通信

概念

  • 进程是对程序运行时的封装,是操作系统分配资源的基本单位,实现操作系统的并发。
  • 线程是进程的子单位,是CPU调度和分派的基本单位,实现进程内部的并发。是OS识别的最小执行与调度单位,每个线程独占一个虚拟处理器,每个线程完成不同的任务,但共相一个地址空间。

区别

  1. 进程在执行过程中拥有独立的内存单元,而多个线程共享进程的内存。(每个线程拥有自己的栈段,来保存所有局部变量和零时变量)
  2. 进程是资源分配的最小单位,线程是CPU调度的最小单位;

进程间的通信

进程间通信主要包括管道、系统IPC(包括
消息队列、信号量、信号、共享内存
等)、以及套接字socket。

  1. 管道
    匿名和命名两种,用于父子进程通信,命名管道还允许不同进程间的通信。
    =>匿名管道:半双工,只允许数据往一个方向流动,只能用于父子进程之间的通信,
    命名管道FIFO:可在无关的通道间交换数据
  2. 系统IPC
    2.1 消息队列,
    2.2 信号量,它是一个计数器,用来控制多个进程对共享资源的访问,信号量用于进程间的互斥与同步,不储存进程间的通讯数据。
    而信号是用于通知接收进程某个时间已经发生。
    2.3 共享内存
    它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等
    特点:
    共享内存是最快的一种IPC,因为进程是直接对内存进行存取
    因为多个进程可以同时操作,所以需要进行同步
    信号量+共享内存通常结合在一起使用,信号量用来同步对共享内存的访问
    3.套接字SOCKET:
    socket也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同主机之间的进程通信。

线程间的通讯方式

临界区
:通过多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问;
互斥量Synchronized/Lock
:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问
信号量Semphare
:为控制具有有限数量的用户资源而设计的,它允许多个线程在同一时刻去访问同一个资源,但一般需要限制同一时刻访问此资源的最大线程数目。
事件(信号),Wait/Notify
:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作进程间通信的方式:

理解多线程程序是如何执行代码

只有理解多线程程序是如何执行代码的,才能处理如何保证互斥、保证同步和死锁问题,
记住线程就是轻量级进程

一个CPU两个线程,怎么执行

  1. 每个线程是独占CPU资源进行运算和执行,从两个线程来看,是交替运行。两个线程
    并发运行
    ,独占CPU一小会。
  2. 两个线程是被操作系统使用 调度算法 进行调度,获取到CPU。例如现在的时间片轮转,之前的先来先服务、短作业优先,优先级优先等等
  3. 两个线程是抢占式执行的,谁先执行谁后执行是说不清楚的。有可能是主线程后续先执行,也有可能是工作线程先执行。

两个CPU两个线程,怎么执行

  1. 各自占一个CPU,两个线程同行运行,
    并行运行

2个CPU4个线程,怎么执行(接近现实计算机执行情况)
1.CPU少于线程数,操作系统必须使用 调度算法,让每一个线程都获取到CPU资源,得以让线程都能向下推进,执行各自的代码
2.在当前这种情况下,存在并行(多个CPU执行多个线程,同时间运行)也存在并发(一个CPU执行多个线程,交替执行)。
4.对于程序猿而言,针对多线程程序,一定要有 “并行”思维。 这样才能学好多线程
3.现在的操作系统就是这种情况,进程/线程数量远远大于CPU的核数,所以,需要操作系统调度。那么就形成了并行或者并发

线程是如何切换的

线程的结构:

当线程要切换时,线程的task_struct里面的
PC指针会(程序计数器)
保存 这个线程的汇编程序中将要执行的
下一条指令

那么恢复的时候又是从哪里恢复的:线程的task_struct中上下文信息中恢复,保CPU存寄存器当中的值。

前段时间有分享一篇
electron25+vite4搭建跨桌面端vue3应用
实践。今天带来最新捣鼓的
electron27+react18创建跨端程序、electron多开窗体(模拟QQ登录窗口切换主窗口)、自定义无边框窗口拖拽导航栏
的一些知识分享。

electron团队更新迭代比较快,目前稳定版本已经升级到了electron27。而且运行及构建速度有了大幅度的提升!

  • 版本信息

"vite": "^4.4.5"
"react": "^18.2.0"
"electron": "^27.0.1"
"electron-builder": "^24.6.4"
"vite-plugin-electron": "^0.14.1"
  • 搭建react18项目

使用vite4构建工具创建react项目模板。

yarn create vite electron-vite4-react18
# 选择创建react模板
cd electron
-vite4-react18
yarn
installyarn dev

至此,一个基础的react模板项目就创建好了。接下来就是安装electron相关的依赖配置。

  • 安装electron关联依赖包

注意:如果安装出现卡顿情况,建议设置淘宝镜像源。

//安装electron
yarn add -D electron//安装electron-builder 用于构建打包可安装exe程序
yarn add -D electron-builder//安装electron-devtools-installer 用于开发调试electron项目
yarn add -D electron-devtools-installer

另外还需要安装一个electron和vite的桥接插件
vite-plugin-electron

yarn add -D vite-plugin-electron

vite-plugin-electron:一款快速集成整合Vite和Electron,方便在渲染进程中使用Node API或Electron API功能。

到这里,所依赖的electron插件已经安装完毕。接下来就是创建主进程,启动项目了。

  • 创建主进程配置

const { app, BrowserWindow } = require('electron')

const MultiWindow
= require('./src/windows')

const createWindow
= () =>{
let win
= newMultiWindow()
win.createWin({ isMainWin:
true})
}

app.whenReady().then(()
=>{
createWindow()
app.on(
'activate', () =>{if (BrowserWindow.getAllWindows().length === 0) createWindow()
})
})

app.on(
'window-all-closed', () =>{if (process.platform !== 'darwin') app.quit()
})

配置vite.config.js

在vite.config.js中引入vite-plugin-electron配置主进程入口electron-main.js文件。

import { defineConfig, loadEnv } from 'vite'import react from'@vitejs/plugin-react'import electron from'vite-plugin-electron'import { resolve } from'path'import { parseEnv } from'./src/utils/env'

//https://vitejs.dev/config/
export default defineConfig(({ command, mode }) =>{
const viteEnv
=loadEnv(mode, process.cwd())
const env
=parseEnv(viteEnv)return{
plugins: [
react(),
electron({
entry:
'electron-main.js',
})
],

esbuild: {
//打包去除 console.log 和 debugger drop: env.VITE_DROP_CONSOLE && command === 'build' ? ["console", "debugger"] : []
},
/*开发服务器配置*/server: {//端口 port: env.VITE_PORT,//代理配置 proxy: {//... }
},

resolve: {
//设置别名 alias: {'@': resolve(__dirname, 'src'),'@assets': resolve(__dirname, 'src/assets'),'@components': resolve(__dirname, 'src/components'),'@views': resolve(__dirname, 'src/views')
}
}
}
})

配置package.json

在package.json文件中加入
"main": "electron-main.js"
入口配置,并且需要去掉
"type": "module"

注意:官方提示electron28之后可以支持"type": "module"

接下来就运行yarn electron:serve桌面端项目就能运行了。

至于一些electron打包配置,这里就不详细介绍了,之前有相关分享的文章。

https://www.cnblogs.com/xiaoyan2017/p/17436076.html

  • electron自定义无边框拖拽导航栏

创建窗口的时候设置
frame: false
即可创建一个无系统边框的窗体。

通过css设置
-webkit-app-region: drag
来实现拖拽区域。设置
-webkit-app-region: no-drag
取消拖拽响应。

自定义最大化/最小化/关闭

import { useState, useContext } from 'react'
import { Modal } from '@arco-design/web-react'
import { setWin } from '@/windows/action'

function WinBtn(props) {
const {
color = '#fff',
minimizable = true,
maximizable = true,
closable = true,
zIndex = 2023,

children
} = props

const [hasMaximized, setHasMaximized] = useState(false)

window.electronAPI.invoke('win__isMaximized').then(res => {
setHasMaximized(res)
})
window.electronAPI.receive('win__hasMaximized', (e, res) => {
setHasMaximized(res)
})

// 最小化
const handleWinMin = () => {
window.electronAPI.send("win__minimize")
}
// 最大化/还原
const handleWinMax2Min = () => {
window.electronAPI.invoke("win__max2min").then(res => {
console.log(res)
setHasMaximized(res)
})
}
// 关闭
const handleWinClose = () => {
if(window.config.isMainWin) {
Modal.confirm({
title: '提示',
content:
<divstyle={{textAlign: 'center' }}>是否最小化至托盘,不退出程序?</div>,
okButtonProps: {status: 'warning'},
style: {width: 360},
cancelText: '最小化至托盘',
okText: '残忍退出',
onOk: () => {
setWin('close')
},
onCancel: () => {
setWin('hide', window.config.id)
}
})
}else {
setWin('close', window.config.id)
}
}

return (
<> <divclassName="vui__macbtn flexbox flex-alignc"style={{zIndex:zIndex}}> <divclassName="vui__macbtn-groups flexbox flex-alignc"style={{color:color}}>{ JSON.parse(minimizable) &&<aclassName="mbtn min"title="最小化"onClick={handleWinMin}><svgx="0"y="0"width="10"height="10"viewBox="0 0 10 10"><pathfill="#995700"d="M8.048,4.001c0.163,0.012 0.318,0.054 0.459,0.137c0.325,0.191 0.518,0.559 0.49,0.934c-0.007,0.097 -0.028,0.192 -0.062,0.283c-0.04,0.105 -0.098,0.204 -0.171,0.29c-0.083,0.098 -0.185,0.181 -0.299,0.24c-0.131,0.069 -0.271,0.103 -0.417,0.114c-2.031,0.049 -4.065,0.049 -6.096,0c-0.163,-0.012 -0.318,-0.054 -0.459,-0.137c-0.325,-0.191 -0.518,-0.559 -0.49,-0.934c0.007,-0.097 0.028,-0.192 0.062,-0.283c0.04,-0.105 0.098,-0.204 0.171,-0.29c0.083,-0.098 0.185,-0.181 0.299,-0.24c0.131,-0.069 0.271,-0.103 0.417,-0.114c2.031,-0.049 4.065,-0.049 6.096,0Z"></path></svg></a>}
{ JSON.parse(maximizable) &&
<aclassName="mbtn max"title={hasMaximized? '向下还原' : '最大化'} onClick={handleWinMax2Min}>{
hasMaximized ?
<svgx="0"y="0"width="10"height="10"viewBox="0 0 10 10"><pathfill="#4d0000"d="M5,10c0,0 0,-2.744 0,-4.167c0,-0.221 -0.088,-0.433 -0.244,-0.589c-0.156,-0.156 -0.368,-0.244 -0.589,-0.244c-1.423,0 -4.167,0 -4.167,0l5,5Z"></path><pathfill="#006400"d="M5,0c0,0 0,2.744 0,4.167c0,0.221 0.088,0.433 0.244,0.589c0.156,0.156 0.368,0.244 0.589,0.244c1.423,0 4.167,0 4.167,0l-5,-5Z"></path></svg>:<svgx="0"y="0"width="10"height="10"viewBox="0 0 10 10"><pathfill="#4d0000"d="M2,3c0,0 0,2.744 0,4.167c0,0.221 0.088,0.433 0.244,0.589c0.156,0.156 0.368,0.244 0.589,0.244c1.423,0 4.167,0 4.167,0l-5,-5Z"></path><pathfill="#006400"d="M8,7c0,0 0,-2.744 0,-4.167c0,-0.221 -0.088,-0.433 -0.244,-0.589c-0.156,-0.156 -0.368,-0.244 -0.589,-0.244c-1.423,0 -4.167,0 -4.167,0l5,5Z"></path></svg>}</a>}
{ JSON.parse(closable) &&
<aclassName="mbtn close"title="关闭"onClick={handleWinClose}><svgx="0"y="0"width="10"height="10"viewBox="0 0 10 10"><pathfill="#4d0000"d="M5,3.552c0.438,-0.432 0.878,-0.861 1.322,-1.287c0.049,-0.044 0.101,-0.085 0.158,-0.119c0.149,-0.091 0.316,-0.137 0.49,-0.146c0.04,0 0.04,0 0.08,0.001c0.16,0.011 0.314,0.054 0.453,0.135c0.08,0.046 0.154,0.104 0.218,0.171c0.252,0.262 0.342,0.65 0.232,0.996c-0.045,0.141 -0.121,0.265 -0.218,0.375c-0.426,0.444 -0.855,0.884 -1.287,1.322c0.432,0.438 0.861,0.878 1.287,1.322c0.097,0.11 0.173,0.234 0.218,0.375c0.04,0.126 0.055,0.26 0.043,0.392c-0.011,0.119 -0.043,0.236 -0.094,0.344c-0.158,0.327 -0.49,0.548 -0.852,0.566c-0.106,0.005 -0.213,-0.007 -0.315,-0.035c-0.156,-0.043 -0.293,-0.123 -0.413,-0.229c-0.444,-0.426 -0.884,-0.855 -1.322,-1.287c-0.438,0.432 -0.878,0.861 -1.322,1.287c-0.11,0.097 -0.234,0.173 -0.375,0.218c-0.126,0.04 -0.26,0.055 -0.392,0.043c-0.119,-0.011 -0.236,-0.043 -0.344,-0.094c-0.327,-0.158 -0.548,-0.49 -0.566,-0.852c-0.005,-0.106 0.007,-0.213 0.035,-0.315c0.043,-0.156 0.123,-0.293 0.229,-0.413c0.426,-0.444 0.855,-0.884 1.287,-1.322c-0.432,-0.438 -0.861,-0.878 -1.287,-1.322c-0.106,-0.12 -0.186,-0.257 -0.229,-0.413c-0.025,-0.089 -0.037,-0.182 -0.036,-0.275c0.004,-0.363 0.211,-0.704 0.532,-0.874c0.13,-0.069 0.272,-0.105 0.418,-0.115c0.04,-0.001 0.04,-0.001 0.08,-0.001c0.174,0.009 0.341,0.055 0.49,0.146c0.057,0.034 0.109,0.075 0.158,0.119c0.444,0.426 0.884,0.855 1.322,1.287Z"></path></svg></a>}<iclassName="mr-10"></i>{ children }</div> <divclassName="vui__mactitle">{window.config.title || '首页'}</div> </div> </>)
}

export default WinBtn
  • electron自定义托盘图标/托盘闪烁

/**
* Electron多窗口管理器
* @author Andy Q:282310962
*/const { app, BrowserWindow, ipcMain, Menu, Tray, dialog, globalShortcut }= require('electron')//const { loadEnv } = require('vite') const { join } = require('path')//根目录路径 process.env.ROOT = join(__dirname, '../../')

const isDev
= process.env.NODE_ENV === 'development' //const winURL = isDev ? 'http://localhost:3000/' : join(__dirname, 'dist/index.html') const winURL = isDev ? process.env.VITE_DEV_SERVER_URL : join(process.env.ROOT, 'dist/index.html')

class MultiWindow {
constructor() {
//主窗口对象 this.main = null //窗口组 this.group ={}//托盘图标 this.tray = null this.flashTimer = null this.trayIco1 = join(process.env.ROOT, 'resource/tray.ico')this.trayIco2 = join(process.env.ROOT, 'resource/tray-empty.ico')//监听ipcMain事件 this.listenIpc()//创建系统托盘 this.createTray()
}
//系统配置参数 winOptions() {return{//窗口图标 icon: join(process.env.ROOT, 'resource/shortcut.ico'),
backgroundColor:
'#fff',
autoHideMenuBar:
true,
titleBarStyle:
'hidden',
width:
900,
height:
600,
resizable:
true,
minimizable:
true,
maximizable:
true,
frame:
false, //设置为 false 时可以创建一个无边框窗口 默认值为 true show: false, //窗口是否在创建时显示 webPreferences: {
contextIsolation:
true, //启用上下文隔离(为了安全性)(默认true) nodeIntegration: false, //启用Node集成(默认false) preload: join(process.env.ROOT, 'electron-preload.js')
}
}
}
//创建新窗口 createWin(options) {//... }//... //主进程监听事件 listenIpc() {//创建新窗体 ipcMain.on('win-create', (event, args) => this.createWin(args))//... //托盘图标闪烁 ipcMain.on('win__flashTray', (event, bool) => this.flashTray(bool))//屏幕截图 ipcMain.on('win__setCapture', () =>{//... })
}
//创建系统托盘图标 createTray() {
console.log(__dirname)
console.log(join(process.env.ROOT,
'resource/tray.ico'))

const trayMenu
=Menu.buildFromTemplate([
{
label:
'打开主界面',
icon: join(process.env.ROOT,
'resource/home.png'),
click: ()
=>{try{for(let i in this.group) {
let win
= this.getWin(i)if(!win) return //是否主窗口 if(this.group[i].isMainWin) {if(win.isMinimized()) win.restore()
win.show()
}
}
}
catch(error) {
console.log(error)
}
}
},
{
label:
'设置中心',
icon: join(process.env.ROOT,
'resource/setting.png'),
click: ()
=>{for(let i in this.group) {
let win
= this.getWin(i)if(win) win.webContents.send('win__ipcData', { type: 'CREATE_WIN_SETTING', value: null})
}
},
},
{
label:
'锁屏',
click: ()
=> null,
},
{
label:
'关闭托盘闪烁',
click: ()
=>{this.flashTray(false)
}
},
{type:
'separator'},/*{
label: '重启',
click: () => {
// app.relaunch({ args: process.argv.slice(1).concat(['--relaunch']) })
// app.exit(0)

app.relaunch()
app.quit()
}
},
*/{
label:
'关于',
click: ()
=>{for(let i in this.group) {
let win
= this.getWin(i)if(win) win.webContents.send('win__ipcData', { type: 'CREATE_WIN_ABOUT', value: null})
}
}
},
{
label:
'关闭应用并退出',
icon: join(process.env.ROOT,
'resource/logout.png'),
click: ()
=>{
dialog.showMessageBox(
this.main, {
title:
'询问',
message:
'确定要退出应用程序吗?',
buttons: [
'取消', '最小化托盘', '退出应用'],
type:
'error',
noLink:
false, //true传统按钮样式 false链接样式 cancelId: 0}).then(res=>{
console.log(res)

const index
=res.responseif(index == 0) {
console.log(
'取消')
}
if(index == 1) {
console.log(
'最小化托盘')for(let i in this.group) {
let win
= this.getWin(i)if(win) win.hide()
}
}
else if(index == 2) {
console.log(
'退出应用')try{for(let i in this.group) {
let win
= this.getWin(i)if(win) win.webContents.send('win__ipcData', { type: 'WIN_LOGOUT', value: null})
}
//app.quit 和 app.exit(0) 都可退出应用。 //前者可以被打断并触发一些事件,而后者将强制应用程序退出而不触发任何事件或允许应用程序取消操作。 app.quit()
}
catch(error) {
console.log(error)
}
}
})
}
}
])
this.tray = new Tray(this.trayIco1)this.tray.setContextMenu(trayMenu)this.tray.setToolTip(app.name)this.tray.on('double-click', () =>{
console.log(
'double clicked')
})
//开启托盘闪烁 //this.flashTray(true) }//托盘图标闪烁 flashTray(flash) {
let hasIco
= false if(flash) {if(this.flashTimer) return this.flashTimer = setInterval(() =>{this.tray.setImage(hasIco ? this.trayIco1 : this.trayIco2)
hasIco
= !hasIco
},
500)
}
else{if(this.flashTimer) {
clearInterval(
this.flashTimer)this.flashTimer = null}this.tray.setImage(this.trayIco1)
}
}
//销毁托盘图标 destoryTray() {this.flashTray(false)this.tray.destroy()this.tray = null}
}

module.exports
= MultiWindow

electron支持的一些环境变量。

process.env.NODE_ENV
process.
env.VITE_DEV_SERVER_URL

另外需要注意,
__dirname
变量指向当前文件。如:src/windows/index.js文件,则根目录需要
../../
返回。

Okay,以上就是electron27+react18+vite4搭建桌面端实践项目的一些分享知识,希望对大家有所帮助哈!

wmproxy

wmproxy
将用
Rust
实现
http/https
代理,
socks5
代理, 反向代理, 静态文件服务器,后续将实现
websocket
代理, 内外网穿透等, 会将实现过程分享出来, 感兴趣的可以一起造个轮子法

项目地址

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

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

为什么我们需要主动

主动可以让我们掌握好系统的稳定性,假设我们有一条连接不可达,连接超时的判定是5秒,需要检测失败3次才认定为失败,那么此时从我们开始检测,到判定失败需要耗时15秒。
如果此时我们是个高并发的系统,每秒的QPS是1000,我们有三个地址判定,那么此时我们有1/3的失败概率。那么在15秒内,我们会收到15000个请求,会造成5000个请求失败,如果是重要的数据,我们会丢失很多重要数据。
如果此时客户端拥有重试机制,那么客户端在失败的时候会发起重试,而且系统可能会反复的分配到那台不可达的系统,将会造成短时间内请求数激增,可能引发系统的雪崩。
所以此时我们主动知道目标端的系统稳定性极其重要。

网络访问示意图

以下是没有主动健康检查

sequenceDiagram
participant 客户端
participant 代理服务器
客户端->>代理服务器: 请求数据(0.5s)
代理服务器->>后端1: 连接并请求数据(5s)失败
Note right of 后端1: 机器宕机不可达

代理服务器-->>客户端: 返回失败0.5s(总耗时6s)
客户端->>代理服务器: 重新请求数据(0.5s)
代理服务器->>后端2: 请求数据成功(0.2s)
后端2-->>代理服务器: 返回数据成功(0.2s)
代理服务器-->> 客户端: 返回数据成功0.5s(总耗时1.4s)

如果出错的时候,一个请求的平均时长可能会达到
(1.4s + 5s) / 2 = (3.2s)
,比正常访问多了
(3.2 - 1.4) = 1.8s
,节点的宕机会对系统的稳定性产生较大的影响

以下是主动健康检查,它保证了访问后端服务器组均是正常的状态

sequenceDiagram
客户端->>代理服务器: 请求数据(0.5s)
loop 健康检查
代理服务器->>服务器组(只访问1): 定时请求,保证存活,1检查成功,2检查失败
end
Note right of 服务器组(只访问1): 处理客户端数据
代理服务器 -->> 服务器组(只访问1): 请求数据(0.2s)
服务器组(只访问1) -->> 代理服务器: 返回数据成功(0.2s)
代理服务器-->>客户端: 返回数据成功(0.5s)(总耗时1.4s)

服务器2
出错的时候,主动检查已经检查出
服务器2
不可用,负载均衡的时候选择已经把
服务器2
摘除,所以系统的平均耗时1.4s,系统依然保持稳定

健康检查的种类

在目前的系统中有以下两分类:

  • HTTP 请求特定的方法及路径,判断返回是否得到预期的status或者body
  • TCP 仅只能测试连通性,如果能连接表示正常,会出现能连接但无服务的情况

健康检查的准备

我们需要从配置中读出所有的需要健康检查的类型,即需要去重,把同一个指向的地址过滤掉
配置有可能被重新加载,所以我们需要预留发送配置的方式(或者后续类似nginx用新开进程的方式则不需要),此处做一个预留。

  • 如何去重
    像这种简单级别的去重通常用
    HashSet
    复杂度为
    O(1)
    或者用简单的
    Vec
    复杂度为
    O(n)
    ,以
    SocketAddr
    的为键值,判断是否有重复的数据。

  • 如何保证不影响主线程
    把健康请求的方法移到异步函数,用
    tokio::spawn
    中处理,在健康检查的情况下保证不影响其它数据处理

  • 如果同时处理多个地址的健康检查
    每一次健康检查都会在一个异步函数中执行,在我们调用完请求后,我们会对当前该异步进行
    tokio::time::sleep
    以让出当前CPU。

  • 如何按指定间隔时间请求
    因为每一次健康请求都是在异步函数中,我们不确认之前的异步是否完成,所以我们在每次请求前都记录
    last_request
    ,我们在请求前调用
    HealthCheck::check_can_request
    判断当前是否可以发送请求来保证间隔时间内不多次请求造成服务器的压力。

  • 超时连接判定处理
    利用
    tokio::time::timeout

    future
    做组合,等超时的时候直接按错误处理

部分实现源码

主要源码定义在
check/active.rs
中,主要的定义两个类

/// 单项健康检查
#[derive(Debug, Clone)]
pub struct OneHealth {
    /// 主动检查地址
    pub addr: SocketAddr,
    /// 主动检查方法, 有http/https/tcp等
    pub method: String,
    /// 每次检查间隔
    pub interval: Duration,
    /// 最后一次记录时间
    pub last_record: Instant,
}
/// 主动式健康检查
pub struct ActiveHealth {
    /// 所有的健康列表
    pub healths: Vec<OneHealth>,
    /// 接收健康列表,当配置变更时重新载入
    pub receiver: Receiver<Vec<OneHealth>>,
}

我们在配置的时候获取所有需要主动检查的数据

/// 获取所有待健康检查的列表
pub fn get_health_check(&self) -> Vec<OneHealth> {
    let mut result = vec![];
    let mut already: HashSet<SocketAddr> = HashSet::new();
    if let Some(proxy) = &self.proxy {
        // ...
    }

    if let Some(http) = &self.http {
        // ...
    }
    result
}

主要的检查源码,所有的最终信息都落在
HealthCheck
中的静态变量里:

pub async fn do_check(&self) -> ProxyResult<()> {
    // 防止短时间内健康检查的连接过多, 做一定的超时处理, 或者等上一条消息处理完毕
    if !HealthCheck::check_can_request(&self.addr, self.interval) {
        return Ok(())
    }
    if self.method.eq_ignore_ascii_case("http") {
        match tokio::time::timeout(self.interval + Duration::from_secs(1), self.connect_http()).await {
            Ok(r) => match r {
                Ok(r) => {
                    if r.status().is_server_error() {
                        log::trace!("主动健康检查:HTTP:{}, 返回失败:{}", self.addr, r.status());
                        HealthCheck::add_fall_down(self.addr);
                    } else {
                        HealthCheck::add_rise_up(self.addr);
                    }
                }
                Err(e) => {
                    log::trace!("主动健康检查:HTTP:{}, 发生错误:{:?}", self.addr, e);
                    HealthCheck::add_fall_down(self.addr);
                }
            },
            Err(e) => {
                log::trace!("主动健康检查:HTTP:{}, 发生超时:{:?}", self.addr, e);
                HealthCheck::add_fall_down(self.addr);
            },
        }
    } else {
        match tokio::time::timeout(Duration::from_secs(3), self.connect_http()).await {
            Ok(r) => {
                match r {
                    Ok(_) => {
                        HealthCheck::add_rise_up(self.addr);
                    }
                    Err(e) => {
                        log::trace!("主动健康检查:TCP:{}, 发生错误:{:?}", self.addr, e);
                        HealthCheck::add_fall_down(self.addr);
                    }
                }
            }
            Err(e) => {
                log::trace!("主动健康检查:TCP:{}, 发生超时:{:?}", self.addr, e);
                HealthCheck::add_fall_down(self.addr);
            }
        }
    }
    Ok(())
}

结语

主动检查可以及时的更早的发现系统中不稳定的因素,是系统稳定性的基石,也可以通过更早的发现因素来通知运维介入,我们的目的是使系统更稳定,更健壮,处理延时更少。

点击
[关注]

[在看]

[点赞]
是对作者最大的支持