深入理解Reactor模型的原理与应用

news2024/11/17 21:47:49

1、什么是Reactor模型

        Reactor意思是“反应堆”,是一种事件驱动机制。

        和普通函数调用的不同之处在于:应用程序不是主动的调用某个 API 完成处理,而是恰恰相反,Reactor逆置了事件处理流程,应用程序需要提供相应的接口并注册到 Reactor 上,如果相应的时间发生,Reactor将主动调用应用程序注册的接口,这些接口又称为“回调函数”。

        对于刚开始接触这个机制,个人感觉翻译成“感应器”可能会更好理解一点,因为注册在Reactor上的函数就像感应器一样,只要有事件到达,就会触发它开始工作。

        Reactor 模式是编写高性能网络服务器的必备技术之一。


2、Reactor模型的优点

  • 响应快,不必为单个同步时间所阻塞,虽然 Reactor 本身依然是同步的;
  • 编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;
  • 可扩展性强,可以方便的通过增加 Reactor 实例个数来充分利用 CPU 资源;
  • 可复用性高,reactor 框架本身与具体事件处理逻辑无关,具有很高的复用性;
           Reactor 模型开发效率上比起直接使用 IO 复用要高,它通常是单线程的,设计目标是希望单线程使用一颗 CPU 的全部资源。
            优点即每个事件处理中很多时候可以不考虑共享资源的互斥访问。可是缺点也是明显的,现在的硬件发展,已经不再遵循摩尔定律,CPU 的频率受制于材料的限制不再有大的提升,而改为是从核数的增加上提升能力,当程序需要使用多核资源时,Reactor 模型就会悲剧 , 为什么呢?
            如果程序业务很简单,例如只是简单的访问一些提供了并发访问的服务,就可以直接开启多个反应堆,每个反应堆对应一颗 CPU 核心,这些反应堆上跑的请求互不相关,这是完全可以利用多核的。例如 Nginx 这样的 http 静态服务器。

3、通过对网络编程(epoll)代码的优化,深入理解Reactor模型

1、epoll的普通版本,根据fd类型(listen_fd和client_fd)分为两大类处理。

        如果是listen_fd,调用accept处理连接请求;

        如果是client_fd,调用recv或者send处理数据。

         代码实现:


#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <unistd.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

#include <errno.h>

int main(int argc, char* argv[])
{
    if (argc < 2)
        return -1;

    int port = atoi(argv[1]);   //字符串转换为整型
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
        return -1;

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(struct sockaddr_in));   //新申请的空间一定要置零

    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);    //转换成网络字节序
    addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0)
        return -2;

    if (listen(sockfd, 5) < 0)
        return -3;

    //epoll

    int epfd = epoll_create(1); //创建epoll,相当于红黑树的根节点
    struct epoll_event ev, events[1024] = {0};  //events相当于就绪队列,一次性可以处理的集合
    ev.events = EPOLLIN;
    ev.data.fd = sockfd;

    epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);    //将ev节点加入到epoll,此处的sockfd参数随便添加没有意义,需要操作系统索引和它有对应的句柄

    while (1)
    {
        int nready = epoll_wait(epfd, events, 1024, -1);    //第四个参数-1表示一直等待,有事件才返回
        if (nready < 1) //没有事件触发,nready代表触发事件的个数
            break;

        int i = 0;
        for (i = 0; i < nready; i++)    //epoll_wait带出的就绪fd包括两大类:1、处理连接的listen_fd,2、处理数据的send和recv
        {
            if (events[i].data.fd == sockfd) //如果是listenfd,就将它加入到epoll
            {
                struct sockaddr_in client_addr;
                memset(&client_addr, 0, sizeof(struct sockaddr_in));
                socklen_t client_len = sizeof(client_addr);
                
                int client_fd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len);
                if (client_fd <= 0)
                    continue;

                char str[INET_ADDRSTRLEN] = {0};
                printf("recv from IP = %s ,at Port= %d\n", inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)), ntohs(client_addr.sin_port));

                ev.events = EPOLLIN | EPOLLET;  //epoll默认是LT模式
                ev.data.fd = client_fd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
            }
            else    //fd进行读写操作
            {

               //对fd的读写操作没有分开
                int client_fd = events[i].data.fd;

                char buf[1024] = {0};
                int ret = recv(client_fd, buf, 1024, 0);
                if (ret < 0)
                {
                    if (errno == EAGAIN || errno == EWOULDBLOCK)
                    {
                        //
                    }
                    else
                    {
                        //
                    }

                    printf("ret < 0,断开连接:%d\n", client_fd);

                    close(client_fd);
                    ev.events = EPOLLIN;
                    ev.data.fd = client_fd;
                    epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, &ev);
                }
                else if (ret == 0)  //接收到了客户端发来的断开连接请求FIN后,没有及时调用close函数,进入了CLOSE _WAIT状态
                {
                    printf("ret = 0,断开连接:%d\n", client_fd);

                    close(client_fd);
                    ev.events = EPOLLIN;
                    ev.data.fd = client_fd;
                    epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, &ev); //close关闭连接后要将它既是从epoll中删除
                }
                else
                {
                    printf("Recv: %s, %d Bytes\n", buf, ret);
                }

                //区分fd的读写操作,即recv和send
                if (events[i].events & EPOLLIN)
                {
                    int client_fd = events[i].data.fd;

                    char buf[1024] = {0};
                    int ret = recv(client_fd, buf, 1024, 0);
                    if (ret < 0)
                    {
                        if (errno == EAGAIN || errno == EWOULDBLOCK)
                        {
                            //...
                        }
                        else
                        {
                            //...
                        }

                        printf("ret < 0,断开连接:%d\n", client_fd);

                        close(client_fd);
                        ev.events = EPOLLIN;
                        ev.data.fd = client_fd;
                        epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, &ev);
                    }
                    else if (ret == 0)  //接收到了客户端发来的断开连接请求FIN后,没有及时调用close函数,进入了CLOSE _WAIT状态
                    {
                        printf("ret = 0,断开连接:%d\n", client_fd);

                        close(client_fd);
                        ev.events = EPOLLIN;
                        ev.data.fd = client_fd;
                        epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, &ev); //close关闭连接后要将它既是从epoll中删除
                    }
                    else
                    {
                        printf("Recv: %s, %d Bytes\n", buf, ret);
                    }
                }
                if (events[i].events & EPOLLOUT)    //为什么需要判断EPOLLOUT,而不是直接else?因为一个fd有可能同时存在可读和可写事件的
                {
                    int client_fd = events[i].data.fd;

                    char buf[1024] = {0};
                    send(client_fd, buf, sizeof(buf), 0);
                }

            }
        }
    }

    return 0;
}

 

2、epoll的优化版本,根据事件类型(读和写)分为两大类处理。

         代码实现:

        for (i = 0; i < nready; i++)    //epoll_wait带出的就绪fd包括两大类:1、处理连接的listen_fd,2、处理数据的send和recv
        {
            //区分fd的读写操作
            if (events[i].events & EPOLLIN)
            {
                if (events[i].data.fd == sockfd) //如果是listenfd,就将它加入到epoll
                {
                    struct sockaddr_in client_addr;
                    memset(&client_addr, 0, sizeof(struct sockaddr_in));
                    socklen_t client_len = sizeof(client_addr);
                    
                    int client_fd = accept(sockfd, (struct sockaddr *)&client_addr, &client_len);
                    if (client_fd <= 0)
                        continue;

                    char str[INET_ADDRSTRLEN] = {0};
                    printf("recv from IP = %s ,at Port= %d\n", inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)), ntohs(client_addr.sin_port));

                    ev.events = EPOLLIN | EPOLLET;  //epoll默认是LT模式
                    ev.data.fd = client_fd;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);
                }
                else 
                {
                    int client_fd = events[i].data.fd;

                    char buf[1024] = {0};
                    int ret = recv(client_fd, buf, 1024, 0);
                    if (ret < 0)
                    {
                        if (errno == EAGAIN || errno == EWOULDBLOCK)
                        {
                            //...
                        }
                        else
                        {
                            //...
                        }

                        printf("ret < 0,断开连接:%d\n", client_fd);

                        close(client_fd);
                        ev.events = EPOLLIN;
                        ev.data.fd = client_fd;
                        epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, &ev);
                    }
                    else if (ret == 0)  //接收到了客户端发来的断开连接请求FIN后,没有及时调用close函数,进入了CLOSE _WAIT状态
                    {
                        printf("ret = 0,断开连接:%d\n", client_fd);

                        close(client_fd);
                        ev.events = EPOLLIN;
                        ev.data.fd = client_fd;
                        epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, &ev); //close关闭连接后要将它既是从epoll中删除
                    }
                    else
                    {
                        printf("Recv: %s, %d Bytes\n", buf, ret);
                    }
                }
            }
            //为什么需要判断EPOLLOUT,而不是直接else?因为一个fd有可能同时存在可读和可写事件的
            if (events[i].events & EPOLLOUT)    
            {
                int client_fd = events[i].data.fd;

                char buf[1024] = {0};
                send(client_fd, buf, sizeof(buf), 0);
            }

        }

 

3、epoll的Reactor模式, epoll由以前的对网络io(fd)进行管理,转变成对events事件进行管理。

         代码实现:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <unistd.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

#include <errno.h>


//每个fd所对应的信息
struct sockitem
{
    int sockfd;
    int (*callback)(int fd, int events, void*arg);
    char sendbuf[1024];
    char recvbuf[1024];
};

//每个epoll所对应的信息
struct epollitem
{
    int epfd;
    struct epoll_event events[1024];    //events相当于就绪队列,一次性可以处理的集合
};

struct epollitem *eventloop = NULL;

int recv_cb(int fd, int events, void*arg);
int send_cb(int fd, int events, void*arg);

int accept_cb(int fd, int events, void*arg)
{
    printf("---accept_cb(int fd, int events, void*arg)---\n");

    struct sockaddr_in client_addr;
    memset(&client_addr, 0, sizeof(struct sockaddr_in));
    socklen_t client_len = sizeof(client_addr);
    
    int client_fd = accept(fd, (struct sockaddr *)&client_addr, &client_len);
    if (client_fd <= 0)
        return -1;

    char str[INET_ADDRSTRLEN] = {0};
    printf("recv from IP = %s ,at Port= %d\n", inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)), ntohs(client_addr.sin_port));

    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET;  //epoll默认是LT模式

    struct sockitem *si = (struct sockitem*)malloc(sizeof(struct sockitem));
    si->sockfd = client_fd;
    si->callback = recv_cb;

    ev.data.ptr = si;

    epoll_ctl(eventloop->epfd, EPOLL_CTL_ADD, client_fd, &ev);

    return client_fd;
}

int recv_cb(int fd, int events, void*arg)
{
    printf("---recv_cb(int fd, int events, void*arg)---\n");

    struct epoll_event ev;
    struct sockitem *sit = (struct sockitem*)arg;

    int ret = recv(fd, sit->recvbuf, 1024, 0);
    if (ret < 0)
    {
        if (errno == EAGAIN || errno == EWOULDBLOCK)
        {
            //...
        }
        else
        {
            //...
        }

        printf("ret < 0,断开连接:%d\n", fd);

        ev.events = EPOLLIN;
        epoll_ctl(eventloop->epfd, EPOLL_CTL_DEL, fd, &ev);    //close关闭连接后要将它既是从epoll中删除


        close(fd);
        free(sit);  //连接关闭后释放内存
    }
    else if (ret == 0)  //接收到了客户端发来的断开连接请求FIN后,没有及时调用close函数,进入了CLOSE _WAIT状态
    {
        printf("ret = 0,断开连接:%d\n", fd);
        
        ev.events = EPOLLIN;
        epoll_ctl(eventloop->epfd, EPOLL_CTL_DEL, fd, &ev); 

        close(fd);
        free(sit);
    }
    else
    {
        printf("Recv from recvbuf:  %s, %d Bytes\n", sit->recvbuf, ret);

        ev.events = EPOLLIN | EPOLLOUT;  //
        sit->sockfd = fd;
        sit->callback = send_cb;
        ev.data.ptr = sit;

        epoll_ctl(eventloop->epfd, EPOLL_CTL_MOD, fd, &ev);
    }

    return ret;
}

int send_cb(int fd, int events, void*arg)
{
    struct epoll_event ev;
    struct sockitem *sit = (struct sockitem*)arg;

    strncpy(sit->sendbuf, sit->recvbuf, sizeof(sit->recvbuf) + 1);
    send(fd, sit->sendbuf, sizeof(sit->recvbuf) + 1, 0);

    ev.events = EPOLLIN | EPOLLET;  //
    sit->sockfd = fd;
    sit->callback = recv_cb;
    ev.data.ptr = sit;

    epoll_ctl(eventloop->epfd, EPOLL_CTL_MOD, fd, &ev);

    return fd;
}

int main(int argc, char* argv[])
{
    if (argc < 2)
        return -1;

    int port = atoi(argv[1]);   //字符串转换为整型
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
        return -1;

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(struct sockaddr_in));   //新申请的空间一定要置零

    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);    //转换成网络字节序
    addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(sockfd, (struct sockaddr*)&addr, sizeof(struct sockaddr_in)) < 0)
        return -2;

    if (listen(sockfd, 5) < 0)
        return -3;

    //epoll

    eventloop = (struct epollitem *)malloc(sizeof(struct epollitem));
    eventloop->epfd = epoll_create(1); //创建epoll,相当于红黑树的根节点

    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLET;

    struct sockitem *si = (struct sockitem*)malloc(sizeof(struct sockitem));
    si->sockfd = sockfd;
    si->callback = accept_cb;

    ev.data.ptr = si;   //将fd和对应的回调函数绑定一起带进epoll

    epoll_ctl(eventloop->epfd, EPOLL_CTL_ADD, sockfd, &ev);    //将ev节点加入到epoll,此处的sockfd参数随便添加没有意义,需要操作系统索引和它有对应的句柄

    while (1)
    {
        int nready = epoll_wait(eventloop->epfd, eventloop->events, 1024, -1);    //第四个参数-1表示一直等待,有事件才返回
        if (nready < 1) //没有事件触发,nready代表触发事件的个数
            break;

        int i = 0;
        for (i = 0; i < nready; i++)
        {
            //区分fd的读写操作
            if (eventloop->events[i].events & EPOLLIN)
            {
                struct sockitem *sit = (struct sockitem*)eventloop->events[i].data.ptr;
                sit->callback(sit->sockfd, eventloop->events[i].events, sit);    //不用区分listen_fd和recv_fd,相应的fd都会调用他们所对应的callback
                
            }
            //为什么需要判断EPOLLOUT,而不是直接else?因为一个fd有可能同时存在可读和可写事件的
            if (eventloop->events[i].events & EPOLLOUT)    
            {
                struct sockitem *sit = (struct sockitem*)eventloop->events[i].data.ptr;
                sit->callback(sit->sockfd, eventloop->events[i].events, sit);
            }
        }
    }

    return 0;
}

4、Reactor模型的应用 

        1、单线程模式的Reactor,参考libevent、redis;

        2、多线程模式的Reactor,参考memcached;

        3、多进程模式的Reactor,参考nginx。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Illustrator打开visio导出的emf为什么会报错

问题描述 将使用Visio绘制的.emf文件直接拖入Adobe Illustrator有时候会弹出如下报错窗口——“无法完成操作&#xff0c;因为出现未知错误。” 原因分析 经过多次测试&#xff0c;发现这个跟Visio中元素的数量有关&#xff0c;当数量>24或>27&#xff08;差不多就这…

Git向远程仓库与推送以及拉取远程仓库

理解分布式版本控制系统 1.中央服务器 我们⽬前所说的所有内容&#xff08;⼯作区&#xff0c;暂存区&#xff0c;版本库等等&#xff09;&#xff0c;都是在本地也就是在你的笔记本或者计算机上。⽽我们的 Git 其实是分布式版本控制系统&#xff01;什么意思呢? 那我们多人…

如何评价国产CEC-IDE开发工具

前两天&#xff0c;看到了一则信息&#xff1a;新出的“自主研发”的 CEC-IDE&#xff0c;于是在好奇心的驱使下打开了官网。 主页&#xff1a;https://cecide.digitalgd.com.cn/monorepo/app-front/home 文档&#xff1a;https://cecide.digitalgd.com.cn/monorepo/app-fron…

深入探讨C存储类和存储期——Storage Duration

&#x1f517; 《C语言趣味教程》&#x1f448; 猛戳订阅&#xff01;&#xff01;&#xff01; ​—— 热门专栏《维生素C语言》的重制版 —— &#x1f4ad; 写在前面&#xff1a;这是一套 C 语言趣味教学专栏&#xff0c;目前正在火热连载中&#xff0c;欢迎猛戳订阅&#…

软件设计师学习笔记7-输入输出技术+总线+可靠性+性能指标

目录 1.输入输出技术 1.1数据传输控制方式 1.2中断处理过程 2.总线 3.可靠性 3.1可靠性指标 3.2串联系统与并联系统 3.3混合模型 4.性能指标 1.输入输出技术 即CPU控制主存与外设交互的过程 1.1数据传输控制方式 (1)程序控制&#xff08;查询&#xff09;方式&…

使用windeployqt和InstallShield打包发布Qt软件的流程

前言 Qt编译之后需要打包发布&#xff0c;并且发布给用户后需要增加一个安装软件&#xff0c;通过安装软件可以实现Qt软件的安装&#xff1b;用于安装软件的软件有很多&#xff0c;这里主要介绍InstallShield使用的流程&#xff1b; 使用windeployqt打包Qt编译后的程序 Qt程序…

【八股】2023秋招八股复习笔记5(计算机网络-CN)

文章目录 八股目录目录1、应用层 & HTTP一些http题HTTPS 加密原理&#xff08;问过&#xff09;HTTP/1.1 新特性HTTP/2.0 与 RPC&#xff08;问过&#xff09;GET 和 POST 比较 2、传输层 & TCPTCP三次握手 & 四次挥手&#xff08;问过&#xff09;为什么每次TCP 连…

Java的异常与错误

对比 Exception 和 Error&#xff0c;另外&#xff0c;运行时异常与一般异常有什么区别&#xff1f; Exception 和 Error 都是继承了 Throwable 类&#xff0c;在 Java 中只有 Throwable 类型的实例才可以被抛出&#xff08;throw&#xff09;或者捕获&#xff08;catch&#x…

浅析Linux SCSI子系统:IO路径

文章目录 概述scsi_cmd&#xff1a;SCSI命令result字段proto_op字段proto_type字段 SCSI命令下发scsi_request_fnscsi_dev_queue_readyscsi_host_queue_ready SCSI命令响应命令请求完成的软中断处理 相关参考 概述 SCSI子系统向上与块层对接&#xff0c;由块层提交的对块设备的…

桌面图标不显示

问题 桌面图标不显示 解决办法 鼠标 右击->选择-查看->显示桌面图标

学习创建第一个 React 项目

目标 本篇的目标是配置好基础的环境并创建出第一个 React 项目。 由于之前没接触过相关的知识&#xff0c;所以还需要了解其依赖的一些概念。 步骤主要参考First React app using create-react-app | VS code | npx | npm - YouTube 0. 简单了解相关概念 JavaScript 一种语…

基于未来搜索算法优化的BP神经网络(预测应用) - 附代码

基于未来搜索算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码 文章目录 基于未来搜索算法优化的BP神经网络&#xff08;预测应用&#xff09; - 附代码1.数据介绍2.未来搜索优化BP神经网络2.1 BP神经网络参数设置2.2 未来搜索算法应用 4.测试结果&#xff1a;5…

MLCC产生噪音的原因及解决方案

1.内部构造及工作原理 MLCC是Multilayer Ceramic Capacitor多层片式陶瓷电容 决定电容容值大小的主要参数&#xff1a; 真空介电率 相对介电常数K&#xff1a;和MLCC使用材料有关的常数 有效面积S 介电层厚度d 堆叠层数N 所以面积越大堆叠层数越多的MLCC容值越高 2.MLCC产生啸…

shell数据结构

less可以创建一个文件分页 3次a&#xff0c;是不是连续的 重定向输出>会清空文件内容 cp一份新的&#xff0c;或者新建一个 journalctl 查看启动日志 将前面id传给 xargs 通过journalctl -b 查找对应的日志&#xff0c; pi好像的地址 &#xff1f;&#xff1f;&#xff1f…

用Cmake build OpenCV后,在VS中查看OpenCV源码的方法(环境VS2022+openCV4.8.0) Part III

用Cmake build OpenCV后&#xff0c;在VS中查看OpenCV源码的方法(环境VS2022openCV4.8.0) Part III 用Cmake build OpenCV后&#xff0c;在VS中查看OpenCV源码的方法&#xff08;环境VS2022openCV4.8.0&#xff09; Part I_松下J27的博客-CSDN博客 用Cmake build OpenCV后&…

C++设计模式_01_设计模式简介(多态带来的便利;软件设计的目标:复用)

文章目录 本栏简介1. 什么是设计模式2. GOF 设计模式3. 从面向对象谈起4. 深入理解面向对象5. 软件设计固有的复杂性5.1 软件设计复杂性的根本原因5.2 如何解决复杂性 ? 6. 结构化 VS. 面向对象6.1 同一需求的分解写法6.1.1 Shape1.h6.1.2 MainForm1.cpp 6.2 同一需求的抽象的…

时序预测 | MATLAB实现基于TSO-XGBoost金枪鱼算法优化XGBoost的时间序列预测(多指标评价)

时序预测 | MATLAB实现基于TSO-XGBoost金枪鱼算法优化XGBoost的时间序列预测(多指标评价) 目录 时序预测 | MATLAB实现基于TSO-XGBoost金枪鱼算法优化XGBoost的时间序列预测(多指标评价)预测效果基本介绍程序设计参考资料 预测效果 基本介绍 Matlab实现基于TSO-XGBoost金枪鱼算…

Android学习之路(11) ActionBar与ToolBar的使用

自android5.0开始&#xff0c;AppCompatActivity代替ActionBarActivity&#xff0c;而且ToolBar也代替了ActionBar&#xff0c;下面就是ActionBar和ToolBar的使用 ActionBar 1、截图 2、使用 2.1、AppCompatActivity和其对应的Theme AppCompatActivity使用的是v7的ActionBa…

数据分析基础(1)——超实用‼️Excel 常用函数和实用技巧

学习教程&#xff1a;☑️ 懒人Excel - Excel 函数公式、操作技巧、数据分析、图表模板、VBA、数据透视表教程 目录 一、Excel知识体系✨ 二、Excel 常用函数&#x1f4a1; 三、Excel 技巧 &#x1f914; 补充&#xff1a; 1、自学数据分析学习路线 2、数据查询网站 一、…

服务器安全-增加clamav杀毒

1.安装epel源 yum install epel-release 2.安装 clamav yum install clamav clamav-server clamav-data clamav-update clamav-filesystem clamav-scanner-systemd clamav-devel clamav-lib clamav-server-systemd pcre* gcc zlib zlib-devel libssl-devel libssl openssl …