首页 > 其他分享 >I/O多路转接之 select 与 poll

I/O多路转接之 select 与 poll

时间:2024-08-17 23:26:52浏览次数:14  
标签:转接 int rfds set fd poll select

目录

一、select

(一)初识select

(二)理解select执行过程

(三)socket就绪条件

(四)select的特点

(五)select缺点

(六)select使用示例

二、poll

(一)poll函数接口

(二)socket就绪条件

(三)poll的优点

(四)poll的缺点

(五)poll使用示例


在Linux系统中,I/O多路转接是一种重要的I/O模型(也称I/O多路复用),它能够同时等待多个文件描述符的就绪状态,提高程序的效率。本文将重点介绍I/O多路转接中的select和poll。

理解 I/O 多路复用中的“多路”和“复用”

多路”指的是存在大量需要处理的连接或任务。在网络通信或文件操作等场景中,可能同时有众多的客户端请求、多个文件的读写操作等。这意味着系统面临着处理众多不同来源和类型的输入/输出需求。

复用”强调的是通过有效利用有限的资源,如线程,来应对这些众多的连接或任务。它的核心是避免为每个连接或任务单独分配一个资源(如线程),而是让一个资源能够同时处理多个连接或任务,从而降低系统开销,提高资源的利用效率。


实现复用及相关问题的解决

在非阻塞 I/O 中,通过不断轮询众多连接的 socket 上的接收缓冲区来判断是否有数据到达,从而实现一个线程处理多个连接的读写事件。然而,这种方式存在频繁系统调用带来大量上下文切换开销的问题。

为解决这一问题,将轮询操作从用户空间转移到内核空间,让操作系统来实现轮询过程。这样做的好处在于,减少了用户空间与内核空间的频繁切换,降低了系统调用的次数,从而提高了系统的性能。

常见的操作系统实现的 I/O 多路复用模型有 select、poll 和 epoll 等。以 epoll 为例,它通过事件驱动的方式,当有数据就绪时才通知应用程序进行处理,避免了无意义的轮询,进一步提高了性能。

一、select

(一)初识select

系统提供select函数来实现多路复用输入/输出模型。select系统调用用于监视多个文件描述符的状态变化,程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变。

select函数原型如下:

#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

传入最大文件描述符,以实现内核中根据进程的文件描述符表遍历fd

参数解释:

  • nfds:需要监视的最大的文件描述符值 + 1。
  • rdsetwrsetexset:分别对应于需要检测的可读文件描述符的集合、可写文件描述符的集合及异常文件描述符的集合;它们是输入输出型参数,输入时告知内核要检测的fd,输出时告知调用者已经就绪的fd;每个参数都是一个位图,比特位代表了fd的值,0/1代表的1是否检测或是否就绪。
  • timeout:为结构timeval,用来设置select()的等待时间。其取值包括:
    • NULL:表示select()没有timeout,将一直被阻塞,直到某个文件描述符上发生了事件。
    • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
    • 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

关于fd_set结构:

typedef struct
{
#ifdef USE_XOPEN
    _fd_mask fds_bits[_FD_SETSIZE / NFDBITS];
    #define FDS_BS(set) (set)->fds_bits
#else
    fd_mask __fds_bits[_FD_SETSIZE / NFDBITS];
    #define FDS_BITS(set) ((set)->fds_bits)
#endif
} fd_set;

fd_set实际上是一个整数数组,更严格地说,是一个“位图”,使用位图中对应的位来表示要监视的文件描述符。同时,提供了一组操作fd_set的接口,如FD_CLRFD_ISSETFD_SETFD_ZERO,用于方便地操作位图。

关于timeval结构:

struct timeval
{
    time_t tv_sec; // 秒
    suseconds_t tv_usec; // 微秒
};

timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。

函数返回值:

  • 执行成功则返回文件描述词状态已改变的个数。
  • 如果返回0代表在描述词状态改变前已超过timeout时间,等待下一轮的检测。
  • 当有错误发生时则返回 - 1,错误原因存于errno,此时参数readfdswritefdsexceptfdstimeout的值变成不可预测。

常见的程序片段如下:

fd_set readset;
FD_SET(fd, &readset);
select(fd + 1, &readset, NULL, NULL, NULL);
if (FD_ISSET(fd, readset)){……}

(二)理解select执行过程

理解select模型的关键在于理解fd_set。为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd

  1. 执行fd_set set; FD_ZERO(&set);,则set用位表示是0000,0000
  2. fd = 5,执行FD_SET(fd, &set);后,set变为0001,0000(第5位置为1)。
  3. 若再加入fd = 2fd = 1,则set变为0001,0011
  4. 执行select(6, &set, 0, 0, 0)阻塞等待。
  5. fd = 1fd = 2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd = 5被清空。

(三)socket就绪条件

  1. 读就绪
    • socket内核中,接收缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT。此时可以无阻塞地读该文件描述符,并且返回值大于0。
    • socket TCP通信中,对端关闭连接,此时对该socket读,则返回0。
    • 监听的socket上有新的连接请求。
    • socket上有未处理的错误。
  2. 写就绪
    • socket内核中,发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小),大于等于低水位标记SO_SNDLOWAT,此时可以无阻塞地写,并且返回值大于0。
    • socket的写操作被关闭(close或者shutdown)。对一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号。
    • socket使用非阻塞connect连接成功或失败之后。
    • socket上有未读取的错误。
  3. 异常就绪
    socket上收到带外数据。关于带外数据,和TCP紧急模式相关(TCP协议头中,有一个紧急指针的字段)。

(四)select的特点

  1. 可监控的文件描述符个数取决于sizeof(fd_set)的值。服务器上sizeof(fd_set) = 512,每bit表示一个文件描述符,则服务器上支持的最大文件描述符是512 * 8 = 4096(不同内核可能情况不同)
  2. fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于在select返回后,array作为源数据和fd_set进行FD_ISSET判断;二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入,扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

(五)select缺点

  1. 每次调用select,都需要手动设置fd集合,从接口使用角度来说非常不便。
  2. 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。
  3. 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。
  4. select支持的文件描述符数量太小,所以select本身能检测的fd是有上限的。

(六)select使用示例

  • 检测标准输入输出
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>

int main() {
    fd_set read_fds;
    FD_ZERO(&read_fds);
    FD_SET(0, &read_fds);

    for (;;) {
        printf("> ");
        fflush(stdout);

        int ret = select(1, &read_fds, NULL, NULL, NULL);
        if (ret < 0) {
            perror("select");
            continue;
        }

        if (FD_ISSET(0, &read_fds)) {
            char buf[1024] = {0};
            read(0, buf, sizeof(buf) - 1);
            printf("input: %s", buf);
        } else {
            printf("error! invaild fd\n");
            continue;
        }

        FD_ZERO(&read_fds);
        FD_SET(0, &read_fds);
    }

    return 0;
}

说明:当只检测文件描述符0(标准输入)时,因为输入条件只有在有输入信息的时候才成立,所以如果一直不输入,如果设置了超时时间,就会产生超时信息。

  • 使用select实现服务器

通过单执行流,就可处理多个客户端的连接

#pragma once

#include <iostream>
#include <string>
#include <sys/select.h>
#include <memory>
#include "Log.hpp"
#include "Socket.hpp"

using namespace Net_Work;

const static int gdefaultport = 8888;
const static int gbacklog = 8;
const static int num = sizeof(fd_set) * 8;

class SelectServer
{
private:
    void HandlerEvent(fd_set &rfds)
    {
        for (int i = 0; i < num; i++)
        {
            if (_rfds_array[i] == nullptr)
                continue;
            // 合法的sockfd
            // 读事件分两类,一类是新连接到来。 一类是新数据到来
            int fd = _rfds_array[i]->GetSockFd();
            if (FD_ISSET(fd, &rfds))
            {
                // 读事件就绪
                if (fd == _listensock->GetSockFd())
                {
                    lg.LogMessage(Info, "get a new link\n");
                    // 获取连接
                    std::string clientip;
                    uint16_t clientport;
                    // 不会阻塞!!,因为select已经检测到了listensock已经就绪了
                    Socket *sock = _listensock->AcceptConnection(&clientip, &clientport);
                    if (!sock)
                    {
                        lg.LogMessage(Error, "accept error\n");
                        continue;
                    }
                    lg.LogMessage(Info, "get a client, client info is# %s:%d, fd: %d\n", clientip.c_str(), clientport, sock->GetSockFd());

                    // 这里已经获取连接成功了,接下来怎么办???
                    // read?write?绝对不能!!!read 底层数据是否就绪时不确定的!谁清楚fd上面是否有读事件呢?select!
                    // 新链接fd到来的时候,要把新的fd, 想办法交给select托管 -- 只需要添加到数组_rfds_array中即可
                    int pos = 0;
                    for (; pos < num; pos++)
                    {
                        if (_rfds_array[pos] == nullptr)
                        {
                            _rfds_array[pos] = sock;
                            lg.LogMessage(Info, "get a new link, fd is : %d\n", sock->GetSockFd());
                            break;
                        }
                    }
                    if (pos == num)
                    {
                        sock->CloseSocket();
                        delete sock;
                        lg.LogMessage(Warning, "server is full...!\n");
                    }
                }
                else
                {
                    // 普通的读事件就绪
                    // 读数据是有问题的
                    // 这一次读取不会被卡住吗?
                    std::string buffer;
                    bool res = _rfds_array[i]->Recv(&buffer, 1024);
                    if (res)
                    {
                        lg.LogMessage(Info, "client say# %s\n", buffer.c_str());
                        buffer += ": 你好呀,少年";
                        _rfds_array[i]->Send(buffer);
                        buffer.clear();
                    }
                    else
                    {
                        lg.LogMessage(Warning, "client quit, maybe close or error, close fd : %d\n", _rfds_array[i]->GetSockFd());
                        _rfds_array[i]->CloseSocket();
                        delete _rfds_array[i];
                        _rfds_array[i] = nullptr;
                    }
                }
            }
        }
    }

public:
    SelectServer(int port = gdefaultport) : _port(port), _listensock(new TcpSocket()), _isrunning(false)
    {
    }
    void InitServer()
    {
        _listensock->BuildListenSocketMethod(_port, gbacklog);
        for (int i = 0; i < num; i++)
        {
            _rfds_array[i] = nullptr;
        }
        _rfds_array[0] = _listensock.get();
    }
    void Loop()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 我们能不能直接accept新连接呢?不能!所有的fd,都要交给select. listensock上面新连接,相当于读事件,有新连接,就等价于有新数据到来
            // 首先不能直接accept,而是将listensock交给select。因为只有select有资格知道有没有IO事件就绪
            // 故意放在循环内部
            // 遍历数组,1. 找最大的fd 2. 合法的fd添加到rfds集合中
            fd_set rfds;
            FD_ZERO(&rfds);
            int max_fd = _listensock->GetSockFd();
            for (int i = 0; i < num; i++)
            {
                if (_rfds_array[i] == nullptr)
                {
                    continue;
                }
                else
                {
                    int fd = _rfds_array[i]->GetSockFd();
                    FD_SET(fd, &rfds); // 添加所有合法fd到rfds集合中
                    if (max_fd < fd)   // 更新最大fd
                    {
                        max_fd = fd;
                    }
                }
            }
            // 定义时间
            struct timeval timeout = {0, 0};
            // rfds本质是一个输入输出型参数,rfds是在select调用返回的时候,不断被修改,所以,每次都要重置
            PrintDebug();
            int n = select(max_fd + 1, &rfds, nullptr, nullptr, /*&timeout*/ nullptr);
            switch (n)
            {
            case 0:
                lg.LogMessage(Info, "select timeout..., last time: %u.%u\n", timeout.tv_sec, timeout.tv_usec);
                break;
            case -1:
                lg.LogMessage(Error, "select error!!!\n");
                break;
            default:
                // 正常的就绪的fd
                lg.LogMessage(Info, "select success, begin event handler, last time: %u.%u\n", timeout.tv_sec, timeout.tv_usec);
                HandlerEvent(rfds); // _rfds_array: 3,4,5,6,7,8,9,10 -> rfds: 4,5,6
                break;
            }
        }
        _isrunning = false;
    }
    void Stop()
    {
        _isrunning = false;
    }

    void PrintDebug()
    {
        std::cout << "current select rfds list is : ";
        for (int i = 0; i < num; i++)
        {
            if (_rfds_array[i] == nullptr)
                continue;
            else
                std::cout << _rfds_array[i]->GetSockFd() << " ";
        }
        std::cout << std::endl;
    }
    ~SelectServer()
    {
    }

private:
    std::unique_ptr<Socket> _listensock;
    int _port;
    int _isrunning;

    // select 服务器要被正确设计,需要程序员定义数据结构,来把所有的fd管理起来,往往是数组!
    Socket *_rfds_array[num];
};

二、poll

(一)poll函数接口

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

传入pollfd数组首地址和元素个数,以实现内核中遍历fd

pollfd结构:

struct pollfd {
    int fd; // 文件描述符
    short events; // 请求的事件集合
    short revents; // 返回的事件集合
};

参数说明:

  • fds:是一个poll函数监听的结构列表,每一个元素中包含了文件描述符、监听的事件集合、返回的事件集合。
  • nfds:表示fds数组的长度。
  • timeout:表示poll函数的超时时间,单位是毫秒(ms)。

eventsrevents的取值:

事件描述是否可作为输入是否可作为输出
POLLIN数据(包括普通数据和优先数据)可读
POLLRDNORM普通数据可读
POLLRDBAND优先级带数据可读(Linux不支持)
POLLPRI高优先级数据可读,比如TCP带外数据
POLLOUT数据(包括普通数据和优先数据)可写
POLLWRNORM普通数据可写
POLLWRBAND优先级带数据可写
POLLRDHUPTCP连接被对方关闭,或者对方关闭了写操作。它由GNU引入
POLLERR错误
POLLHUP挂起。比如管道的写端被关闭后,读端描述符上将收到POLLHUP事件
POLLNVAL文件描述符没有打开

返回结果:

  • 返回值小于0,表示出错。
  • 返回值等于0,表示poll函数等待超时。
  • 返回值大于0,表示poll由于监听的文件描述符就绪而返回。

(二)socket就绪条件

select,见上文。读就绪、写就绪、异常就绪。

(三)poll的优点

  1. 不同于select使用三个位图来表示三个事件集合,poll使用一个pollfd的指针实现。
  2. pollfd结构包含了要监视的event和发生的event,不再使用select“参数 - 值”传递的方式,分离了输入输出参数,接口使用比select更方便。
  3. poll并没有最大数量限制(但是数量过大后性能也是会下降)。

(四)poll的缺点

  1. select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
  2. 每次调用poll都需要把大量的pollfd结构从用户态拷贝到内核中。
  3. 同时连接的大量客户端在某一时刻可能只有很少的处于就绪状态,随着监听的描述符数量增多,效率也会线性下降。

(五)poll使用示例

#pragma once

#include <iostream>
#include <string>
#include <poll.h>
#include <memory>
#include "Log.hpp"
#include "Socket.hpp"

using namespace Net_Work;

const static int gdefaultport = 8888;
const static int gbacklog = 8;
const int gnum = 1024;

class PollServer
{
private:
    void HandlerEvent()
    {
        for (int i = 0; i < _num; i++)
        {
            if (_rfds[i].fd == -1)
                continue;
            // 合法的sockfd
            // 读事件分两类,一类是新连接到来。 一类是新数据到来
            int fd = _rfds[i].fd;
            short revents = _rfds[i].revents;

            if (revents & POLLIN)
            {
                // 新连接到来了
                if (fd == _listensock->GetSockFd())
                {
                    lg.LogMessage(Info, "get a new link\n");
                    // 获取连接
                    std::string clientip;
                    uint16_t clientport;
                    // 不会阻塞!!,因为select已经检测到了listensock已经就绪了
                    int sock = _listensock->AcceptConnection(&clientip, &clientport);
                    if (sock == -1)
                    {
                        lg.LogMessage(Error, "accept error\n");
                        continue;
                    }
                    lg.LogMessage(Info, "get a client, client info is# %s:%d, fd: %d\n", clientip.c_str(), clientport, sock);

                    // 这里已经获取连接成功了,接下来怎么办???
                    // read?write?绝对不能!!!read 底层数据是否就绪时不确定的!谁清楚fd上面是否有读事件呢?poll!
                    // 新链接fd到来的时候,要把新的fd, 想办法交给poll托管 -- 只需要添加到数组_rfds中即可
                    int pos = 0;
                    for (; pos < _num; pos++)
                    {
                        if (_rfds[pos].fd == -1)
                        {
                            _rfds[pos].fd = sock;
                            _rfds[pos].events = POLLIN;
                            lg.LogMessage(Info, "get a new link, fd is : %d\n", sock);
                            break;
                        }
                    }
                    if (pos == _num)
                    {
                        // 1. 扩容
                        // 2. 关闭
                        close(sock);
                        lg.LogMessage(Warning, "server is full...!\n");
                    }
                }
                else
                {
                    // 普通的读事件就绪
                    // 读数据是有问题的
                    // 这一次读取不会被卡住吗?
                    char buffer[1024];
                    ssize_t n = recv(fd, buffer, sizeof(buffer-1), 0); // 这里读取会阻塞吗?不会!
                    if (n > 0)
                    {
                        buffer[n] = 0;
                        lg.LogMessage(Info, "client say# %s\n", buffer);
                        std::string message = "你好呀,少年, ";
                        message += buffer;
                        send(fd, message.c_str(), message.size(), 0);
                    }
                    else
                    {
                        lg.LogMessage(Warning, "client quit, maybe close or error, close fd : %d\n", fd);
                        close(fd);
                        // 取消poll的关心
                        _rfds[i].fd = -1;
                        _rfds[i].events = 0;
                        _rfds[i].revents = 0;
                    }
                }
            }
        }
    }

public:
    PollServer(int port = gdefaultport) : _port(port), _listensock(new TcpSocket()), _isrunning(false), _num(gnum)
    {
    }
    void InitServer()
    {
        _listensock->BuildListenSocketMethod(_port, gbacklog);
        _rfds = new struct pollfd[_num];
        for (int i = 0; i < _num; i++)
        {
            _rfds[i].fd = -1;
            _rfds[i].events = 0;
            _rfds[i].revents = 0;
        }
        // 最开始的时候,只有一个文件描述符, Listensock
        _rfds[0].fd = _listensock->GetSockFd();
        _rfds[0].events |= POLLIN;
    }
    void Loop()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 定义时间
            int timeout = -1;
            // rfds本质是一个输入输出型参数,rfds是在select调用返回的时候,不断被修改,所以,每次都要重置
            PrintDebug();
            int n = poll(_rfds, _num, timeout);
            switch (n)
            {
            case 0:
                lg.LogMessage(Info, "poll timeout...\n");
                break;
            case -1:
                lg.LogMessage(Error, "poll error!!!\n");
                break;
            default:
                // 正常的就绪的fd
                lg.LogMessage(Info, "select success, begin event handler\n");
                HandlerEvent(); // _rfds_array: 3,4,5,6,7,8,9,10 -> rfds: 4,5,6
                break;
            }
        }
        _isrunning = false;
    }
    void Stop()
    {
        _isrunning = false;
    }

    void PrintDebug()
    {
        std::cout << "current poll fd list is : ";
        for (int i = 0; i < _num; i++)
        {
            if (_rfds[i].fd == -1)
                continue;
            else
                std::cout << _rfds[i].fd << " ";
        }
        std::cout << std::endl;
    }
    ~PollServer()
    {
        delete[] _rfds;
    }

private:
    std::unique_ptr<Socket> _listensock;
    int _port;
    int _isrunning;
    struct pollfd *_rfds;
    int _num;
};

标签:转接,int,rfds,set,fd,poll,select
From: https://blog.csdn.net/weixin_73567058/article/details/141288217

相关文章

  • 基于LangChain手工测试用例转接口自动化测试生成工具
    接口自动化测试用例是一个老生常谈的问题,在未引入人工智能之前,也有非常多的生成方案,比如如下所示,通过har生成接口自动化测试用例:但是以上的生成方式依然是有一些弊端,比如har本身虽然能表述一定的接口信息和业务信息,但是毕竟无法用来表述全部的应用场景与用例场景。而大部分的......
  • Paper Reading: AutoLearn - Automated Feature Generation and Selection
    目录研究动机文章贡献本文方法问题定义AutoLearn的设计原理预处理挖掘相关特征特征生成特征选择样例展示实验结果数据集和实验设置对比实验可扩展性分析优点和创新点PaperReading是从个人角度进行的一些总结分享,受到个人关注点的侧重和实力所限,可能有理解不到位的地方。具体的......
  • 在 C# 中使用 Socket.Poll() 提高 CPU 利用率
    一、背景       最近在使用Sharp7.cs连接西门子PLC时,发现当在同一台工控机上连接多个(实际超过100)CPU时,工控机的CPU占用非常大,会去到20~30%。然而此时实际的网络流量确并不高,只有10mbps。大量CPU资源消耗不知道在做什么。因为此时工控机上也没有跑其他业务代码......
  • MySQL——单表查询(一)简单查询(1)SELECT语句
        MySQL从数据表中查询数据的基本语句是SELECT语句。在SELECT语句中,可以根据自己对数据的需求,使用不同的查询条件,SELECT语句的基本语法格式如下:SELECT[DISTINCT]*|字段名1,字段名2,字段名3,…FROM表名[WHERE条件表达式1][GR......
  • MySQL常用命令增删改查select,update,alter...
    MySQL常用命令增删改查select,update,alter…文章目录MySQL常用命令增删改查select,update,alter...MySQL修改用户密码MySQL删除用户命令MySQL修改用户密码查看MySQL的用户及hostselectuser,hostfrommysql.user;备注host分别代表的连接方式user@‘localh......
  • ProTable rowSelection 支持多选
    前言:第一次用到多选,gpt非常好用,比之前网页方便太多。 importProTablefrom'@ant-design/pro-table';importReact,{useState}from'react';constTableWithRowSelection=()=>{//使用useState钩子来保存选中的行const[selectedRows,setSelectedRows]......
  • PrimeFaces SelectOneMenu 与 Ajax 集成实例解析
    ======在现代Web开发中,用户界面的交互性是至关重要的。PrimeFaces作为JavaServerFaces(JSF)的一个流行UI组件库,提供了丰富的组件来增强用户界面。本文将通过一个具体实例,详细介绍如何使用PrimeFaces的SelectOneMenu组件与Ajax技术相结合,实现在选择事件时通过Ajax提交数据。......
  • 贷齐乐系统最新版SQL注入(绕过WAF可union select跨表查询)
    目录标题:贷齐乐系统最新版SQL注入(绕过WAF可unionselect跨表查询)内容:一,环境部署二,源码分析三,sql注入总结:[回到顶部](#article_top)一,环境部署本漏洞由于是2015年爆出的,所以这里源码使用的PHP版本不支持7版本,这里我们可以使用PHP5.4.45版本,只需小皮进行下载更改即......
  • Pollard-Rho的一些应用
    P4718https://www.luogu.com.cn/problem/P4718要求找最大的素因子,考虑可能出现在因子的因子中,所以需要递归i64max_prime(i64n){if(isp(n)){returnn;}i64mx{std::numeric_limits<i64>::min()};while(n!=1){autodiv{findDiv(n)};mx......
  • springboot集成Apollo
    ​目前市面上用的比较多的配置中心有:1.Nacos/Apollo对此Nacos Apollo灰度发布 不支持 支持IP级别的灰度发布权限管理 不支持 基本完善版本管理&回滚 支持 之前回滚上一个版本配置实时推送(动态刷新) 支持,但对加密数据有bug存在基本完善敏感加密 引入Jasypt 引入Jasypt......