【socket】基于poll和epoll通信温度上报

网络socket通信

*
poll函数
epoll函数
poll代码实现
epoll代码实现

poll函数

poll是Linux中的字符设备驱动中的一个函数,poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数:
pollfd:指向一个struct pollfd类型的数组,每一个pollfd结构体指定了一个被监视的文件描述符,指示poll()监视多个文件描述符。每个结构体的events域是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域,events域中请求的任何事件都可能在revents域中返回。

nfds 指定数组中监听的元素个数.

timeout指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定为负数值表示无限超时,使poll()
一直挂起直到一个指定事件发生;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。
struct pollfd
{
 int fd;
 short events;
 short revents;
} ;

下表列出指定 events 标志以 及测试 revents 标志的一些常值:

【socket】基于poll和epoll通信温度上报
timeout:该函数成功调用时,poll()返回结构体中revents域不为0的文件描述符个数;如果在超时前没有任何事件发生,poll()返回0; 失败时,poll()返回-1,并设置errno为下列值之一:
EBADF    一个或多个结构体中指定的文件描述符无效。  
EFAULTfds 指针指向的地址超出进程的地址空间。  
EINTR   请求的事件之前产生一个信号,调用可以重新起。  
EINVALnfds 参数超出PLIMIT_NOFILE值。  
ENOMEM    可用内存不足,无法完成请求。

epoll函数

epoll对文件描述符的操作有两种模式: LT(level trigger) 和 ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别如下:

水平触发LT模式

  • 水平触发的主要特点是,如果用户在监听epoll事件,当内核有事件的时候,会拷贝
    给用户态事件,但是如果用户只处理了一次,那么剩下没有处理的会在下一次
    epoll_wait再次返回该事件。
    这样如果用户永远不处理这个事件,就导致每次都会有该事件从内核到用户的拷
    贝,耗费性能,但是水平触发相对安全,最起码事件不会丢掉,除非用户处理完

边沿触发ET模式

  • 边缘触发,相对跟水平触发相反,当内核有事件到达, 只会通知用户一次,至于用
    户处理还是不处理,以后将不会再通知。这样减少了拷贝过程,增加了性能,但是
    相对来说,如果用户马虎忘记处理,将会产生事件丢的情况。

边沿触发仅触发一次,水平触发会一直触发。

int epoll_create(int size);
功能:
创建epoll
参数:
    size忽略不用
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *ev);
功能:修改epoll的增删改查
参数:
    epfd是epoll_create返回值
    op是用来指定需要执行的操作
    fd:指明了要修改兴趣列表中的哪一个文件描述符的设定。该参数可以是代表管道、FIFO、套接字、POSIX消息队列、inotify实例、终端、设备,甚至是另一个epoll实例的文件描述符
    ev:的指针结构体

op指定操作的值:添加,删除,修改对fd的监听

EPOLL_CTL_ADD添加fd到epollEPOLL_CTL_MOD修改已经注册fd的事件EPOLL_CTL_DEepfd删除一个fd

结构体epoll_event的指针,结构体的定义如下:

typedef union epoll_data
{
 void *ptr;
 int fd;
 uint32_t u32;
 uint64_t u64;
} epoll_data_t;

struct epoll_event
{
 uint32_t events;
 epoll_data_t data;
};
int epoll_wait(int epfd, struct epoll_event *evlist, int maxevents, int timeout);
功能:
等待就绪事件
参数:
    epfd是epoll_create()的返回值
    evlist所指向的结构体数组中返回的是有关就绪态文件描述符的信息,数组evlist的空间由调用者负责申请;
    maxevents指定所evlist数组里包含的元素个数;
    timeout用来确定epoll_wait()的阻塞行为,有如下几种:
    如果timeout等于-1,调用将一直阻塞,直到兴趣列表中的文件描述符上有事件产生或者直到捕获到一个信号为止。
    如果timeout等于0,执行一次非阻塞式地检查,看兴趣列表中的描述符上产生了哪个事件。
    如果timeout大于0,调用将阻塞至多timeout毫秒,直到文件描述符上有事件发生,或者直到捕获到一个信号为止

events中的值如下所示:

常量说明作为 epoll_ctl()的输入作为epoll_wait()的返回EPOLLIN可读取非高优先级数据能能EPOLLPRI可读取高优先级数据能能EPOLLRDHUPsocket对端关闭能能EPOLLOUT普通数据可写能能EPOLLET采用边沿触发事件通知能EPOLLONESHOT在完成事件通知之后禁用检查能EPOLLERR有错误发生能POLLHUP出现挂断能

epoll的优点

  • 支持一个进程打开大数目的socket描述符(fd)
  • IO效率不随fd数目增加而线性下降
  • 使用mmap加速内核与用户空间的消息传递
  • 内核微调

poll代码实现

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#define ARRAY_SIZE(x)   (sizeof(x)/sizeof(x[0]))

int socket_Server_init(char *listen_ip,int listen_port);
void sqlite_tem(char *buf);

void print_usage(char *progname)
{
     printf("%s usage: \n", progname);
     printf("-p(--port): sepcify server listen port.\n");
     printf("-h(--Help): print this help information.\n");
     printf("-d(--daemon):set program running on background\n");
     return ;
}

int main (int argc, char **argv)
{

    int                 listenfd = -1;
    int                 clifd;
    struct sockaddr_in  servaddr;
    struct sockaddr_in  cliaddr;
    socklen_t           len;
    int                serv_port = 0;
    int                 ch;
    int                 rv ;
    int                 on = 1;

    char                buf[1024];
    int                 i,j;
    int                 found;
    int                 max;
    int                 daemon_run = 0;
    char                *progname = NULL;
    struct pollfd       fds_arry[1024];

struct option opts[] =
{
     {"daemon",no_argument,NULL,'b'},
     {"port", required_argument, NULL, 'p'},
     {"help", no_argument, NULL, 'h'},
     {NULL, 0, NULL, 0}
};
progname = basename(argv[0]);

 while( (ch=getopt_long(argc, argv, "bp:h", opts, NULL)) != -1 )
      {
           switch(ch)
                {
                    case 'p':
                        serv_port=atoi(optarg);
                        break;
                    case 'b':
                        daemon_run=1;
                        break;
                    case 'h':
                        print_usage(argv[0]);
                        return 0;
                }
    }
  if( !serv_port )
  {
      print_usage(argv[0]);
      return 0;
  }
    if( (listenfd = socket_Server_init(NULL, serv_port)) <0)
    {
        printf("ERROR %s server listen on port %d failure\n",argv[0],serv_port);
        return -1;
    }

    printf("%s server start to listen on port %d\n",argv[0],serv_port);

    if(daemon_run)
    {
        daemon(0,0);
    }

    for(i=0; i<ARRAY_SIZE(fds_arry); i++)
    {
        fds_arry[i].fd=-1;
    }
    fds_arry[0].fd = listenfd;

    fds_arry[0].events = POLLIN;
    max = 0;

    for( ; ; )
    {

        rv = poll(fds_arry, max+1, -1);
    if(rv < 0)
    {
        printf("POLL failure:%s\n",strerror(errno));
        break;
    }
    else if( rv ==0 )
    {
        printf("poll get timeout\n");
        continue;
    }
    if( fds_arry[0].revents & POLLIN )
    {
        if( (clifd=accept(listenfd,(struct sockaddr *)NULL,NULL)) < 0)
        {
            printf("accept new client failure:%s\n",strerror(errno));
            continue;
        }

        found = 0;
        for(i=0; i<ARRAY_SIZE(fds_arry);i++)
        {
            if( fds_arry[i].fd< 0 )
            {
                printf("accept new client [%d] and add it into array\n",clifd);
                fds_arry[i].fd = clifd;
                fds_arry[i].events = POLLIN;
                found = 1;
                break;
            }
        }
        if(!found)
        {
            printf("accept new client [%d] but full, so refuse it\n",clifd);
            close(clifd);
            continue;
        }
        max = i>max ? i:max;
        if( rv 0 )
            continue;
    }
    else
    {
        for ( i=1; i<ARRAY_SIZE(fds_arry);i++)
        {
            if(fds_arry[i].fd < 0)
                continue;
            if( (rv=read(fds_arry[i].fd,buf,sizeof(buf))) 0)
            {
                printf("socket [%d] read failure or get disconnected\n",fds_arry[i].fd);
                close(fds_arry[i].fd);
                fds_arry[i].fd=-1;
            }
            else
            {
                printf("%s\n",buf);
                sqlite_tem(buf);
                printf("Database inserted successfully!\n");
            }
        }
    }
}
cleanup:
    close(listenfd);
    return 0;
}

int socket_Server_init(char *listen_ip, int listen_port)
{

    struct sockaddr_in  servaddr;
    int     rv = 0;
    int     on = 1;
    int     listenfd;

    if( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        printf("use socket()to create a TCP socket failure:%s\n",strerror(errno));
                return -1;
    }
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    memset(&servaddr,0,sizeof(servaddr));
    servaddr.sin_family=AF_INET;
    servaddr.sin_port = htons(listen_port);

    if( !listen_ip )
    {
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    }
    else
    {
        if(inet_pton(AF_INET, listen_ip, &servaddr.sin_addr) 0)
        {
            printf("inet_pton set listen IP address failure\n");
            rv = -2;
            goto cleanup;
        }
    }

    if( bind(listenfd,(struct sockaddr *)&servaddr,sizeof(servaddr)) < 0)
    {
        printf("socket[%d] bind to port failure:%s\n",listenfd,strerror(errno));
        rv = -3;
        goto cleanup;
    }

    if( listen(listenfd,13) < 0)
    {
         printf("use bind to bind tcp socket failure:%s\n",strerror(errno));
         rv = -4;
         goto cleanup;
    }

cleanup:
    if(rv < 0)
        close(listenfd);
    else
        rv = listenfd;

    return rv;
}

void sqlite_tem(char *buf)
{
    int             nrow=0;
    int             ncolumn = 0;
    char          **azResult=NULL;
    int             rv;
    sqlite3        *db=NULL;
    char           *zErrMsg = 0;
    char            sql1[100];
    char           *ipaddr=NULL;
    char           *datetime=NULL;
    char           *temper=NULL;
    char           *sql = "create table if not exists temperature(ipaddr char(30), datetime char(50), temper  char(30))";

    ipaddr = strtok(buf,"/");
    datetime = strtok(NULL, "/");
    temper = strtok(NULL, "/");

    rv = sqlite3_open("tempreture.db", &db);
    if(rv)
    {
        printf("Can't open database:%s\n", sqlite3_errmsg(db));
        sqlite3_close(db);
        return;
    }
    printf("opened a sqlite3 database named tempreture.db successfully!\n");

    int ret = sqlite3_exec(db,sql, NULL, NULL, &zErrMsg);
    if(ret != SQLITE_OK)
    {
        printf("create table fail: %s\n",zErrMsg);
    }

    if(snprintf(sql1,sizeof(sql1), "insert into temper values('%s','%s','%s')", ipaddr, datetime, temper) < 0)
    {
        printf("Failed to write data\n");
    }

    sqlite3_exec(db, sql1, 0, 0, &zErrMsg);
    sqlite3_free(zErrMsg);
    sqlite3_close(db);
    return;
}

epoll代码实现

#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include

#include "sqlite3.h"

#define BACKLOG 13
#define MAX_EVENTS  512

void print_usage(char* progname);
void sig_stop(int signum);
void sqlite_tem(char *buf);
int socket_listen(char *listen_ip, int port);

static int g_stop = 0;

int main(int argc, char* argv[])
{
    int                     rv;
    int                     ret;
    int                     opt;
    int                     idx;
    int                     port;
    int                     log_fd;
    int                     ch = 1;
    int                     daemon_run = 0;

    int                     ser_fd = -1;
    int                     cli_fd = -1;
    struct sockaddr_in      cli_addr;
    socklen_t               cliaddr_len = 20;
    int                     epollfd;
    struct epoll_event      event;
    struct epoll_event      event_array[MAX_EVENTS];
    int                     events;
    int                     found;
    int                     a;
    int                     i;

    char                    *zErrMsg;
    sqlite3                 *db;
    char                    buf[1024];

    struct option            opts[] = {
            {"daemon", no_argument, NULL, 'd'},
            {"port", required_argument, NULL, 'p'},
            {"help", no_argument, NULL, 'h'},
            {NULL, 0, NULL, 0}
    };
    while ((opt = getopt_long(argc, argv, "dp:h", opts, &idx)) != -1)
    {
        switch (opt)
        {
        case 'd':
            daemon_run = 1;
            break;
        case 'p':
            port = atoi(optarg);
            break;
        case 'h':
            print_usage(argv[0]);
            return 0;
        }
    }

    if (!port)
    {
        print_usage(argv[0]);
        return 0;
    }

    if (daemon_run)
    {
        printf("Program %s is running at the background now\n", argv[0]);

        log_fd = open("receive_temper.log", O_CREAT | O_RDWR, 0666);
        if (log_fd < 0)
        {
            printf("Open the logfile failure : %s\n", strerror(errno));
            return 0;
        }

        dup2(log_fd, STDOUT_FILENO);
        dup2(log_fd, STDERR_FILENO);

        if ((daemon(1, 1)) < 0)
        {
            printf("Deamon failure : %s\n", strerror(errno));
            return 0;
        }
    }

    signal(SIGUSR1, sig_stop);

    if( (ser_fd = socket_listen(NULL, port)) < 0 )
    {
        printf("ERROR: %s server listen on serv_port %d failure\n", argv[0], port);
        return -2;
    }
    printf("server start to listen on serv_port %d\n",  port);

    if ((epollfd = epoll_create(MAX_EVENTS)) < 0)
    {
        printf("epoll_create failure:%s\n", strerror(errno));
        return 0;
    }

    event.events = EPOLLIN;
    event.data.fd = ser_fd;

    if (epoll_ctl(epollfd, EPOLL_CTL_ADD, ser_fd, &event) < 0)
    {
        printf("epoll add ser_fd failure:%s\n", strerror(errno));
        return 0;
    }

    while (!g_stop)
    {
        events = epoll_wait(epollfd, event_array, MAX_EVENTS, -1);
        if (events < 0)
        {
            printf("epoll failure:%s\n", strerror(errno));
            break;
        }
        else if (events == 0)
        {
            printf("epoll get timeout\n");
            continue;
        }

        for (i = 0;i < events;i++)
        {
            if ((event_array[i].events & EPOLLERR) || (event_array[i].events & EPOLLHUP))
            {
                printf("epoll_wait get error on fd[%d]:%s\n", event_array[i].data.fd, strerror(errno));
                epoll_ctl(epollfd, EPOLL_CTL_DEL, event_array[i].data.fd, NULL);
                close(event_array[i].data.fd);
            }

            if (event_array[i].data.fd == ser_fd)
            {
                cli_fd = accept(ser_fd, (struct sockaddr*) & cli_addr, &cliaddr_len);
                if (cli_fd < 0)
                {
                    printf("Accept the request from client failure:%s\n", strerror(errno));
                    continue;
                }
                event.data.fd = cli_fd;
                event.events = EPOLLIN;
                if (epoll_ctl(epollfd, EPOLL_CTL_ADD, cli_fd, &event) < 0)
                {
                    printf("epoll add client socket failure:%s\n", strerror(errno));
                    close(cli_fd);
                    continue;
                }
            }
            else
            {
                memset(buf, 0, sizeof(buf));

                a = read(cli_fd, buf, sizeof(buf));
                if (a < 0)
                {
                    printf("Read information from client failure:%s\n", strerror(errno));
                    close(cli_fd);
                    exit(0);
                }
                else if (a == 0)
                {
                    printf("The connection with client has broken!\n");
                    close(cli_fd);
                    exit(0);
                }
                else
                {
                    printf("%s\n",buf);
                    sqlite_tem(buf);
                    printf("Database inserted successfully!\n");
                }

            }
        }
    }
    close(ser_fd);

    return 0;
}

void print_usage(char* progname)
{
    printf("-d(--daemon):let program run in the background.\n");
    printf("-p(--port):enter server port.\n");
    printf("-h(--help):print this help information.\n");

    return;
}

void sig_stop(int signum)
{
    if (SIGUSR1 == signum)
    {
        g_stop = 1;
    }

    return;
}

int socket_listen(char *listen_ip, int port)
{
    int                     rv = 0;
    int                     on = 1;
    int                     ser_fd;
    struct sockaddr_in      servaddr;

    if ( (ser_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0 )
    {
        printf("Use socket() to create a TCP socket failure: %s\n", strerror(errno));
        return -1;
    }

    setsockopt(ser_fd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(port);

    if( !listen_ip )
    {
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    }
    else
    {
        if( inet_pton(AF_INET, listen_ip, &servaddr.sin_addr)  0 )
        {
            printf("Inet_pton() set listen IP address failure\n");
            rv = -2;
            goto cleanup;
        }
    }

    if( bind(ser_fd, (struct sockaddr *) &servaddr, sizeof(servaddr)) < 0 )
    {
        printf("Use bind() to bind the TCP socket failure: %s\n", strerror(errno));
        rv = -3;
        goto cleanup;
    }

    if( listen(ser_fd, 64) < 0 )
    {
        printf("Use bind() to bind the TCP socket failure: %s\n", strerror(errno));
        rv = -4;
        goto cleanup;
    }

cleanup:
    if( rv < 0 )
        close(ser_fd);
    else
        rv = ser_fd;
    return rv;
}

void sqlite_tem(char *buf)
{
    int             nrow=0;
    int             ncolumn = 0;
    char          **azResult=NULL;
    int             rv;
    sqlite3        *db=NULL;
    char           *zErrMsg = 0;
    char            sql1[100];
    char           *ipaddr=NULL;
    char           *datetime=NULL;
    char           *temper=NULL;
    char           *sql = "create table if not exists temperature(ipaddr char(30), datetime char(50), temper  char(30))";

    ipaddr = strtok(buf,"/");
    datetime = strtok(NULL, "/");
    temper = strtok(NULL, "/");

    rv = sqlite3_open("tempreture.db", &db);
    if(rv)
    {
        printf("Can't open database:%s\n", sqlite3_errmsg(db));
        sqlite3_close(db);
        return;
    }
    printf("opened a sqlite3 database named tempreture.db successfully!\n");

    int ret = sqlite3_exec(db,sql, NULL, NULL, &zErrMsg);
    if(ret != SQLITE_OK)
    {
        printf("create table fail: %s\n",zErrMsg);
    }

    if(snprintf(sql1,sizeof(sql1), "insert into temper values('%s','%s','%s')", ipaddr, datetime, temper) < 0)
    {
        printf("Failed to write data\n");
    }

    sqlite3_exec(db, sql1, 0, 0, &zErrMsg);
    sqlite3_free(zErrMsg);
    sqlite3_close(db);
    return;
}

Original: https://www.cnblogs.com/Ye-Wei/p/16728599.html
Author: 西故黄鹤楼
Title: 【socket】基于poll和epoll通信温度上报

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/606344/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 源码安装apache脚本部署

    源码安装apache脚本部署 [root@localhost ~]# ls anaconda-ks.cfg httpd.tar.xz [root@localhost ~]# tar…

    Linux 2023年6月6日
    0110
  • Vim配置文件-详解(.vimrc)

    Vim配置文件的作用 Vim启动时,会根据配置文件(.vimrc)来设置 Vim,因此我们可以通过此文件来定制适合自己的 Vim 所有系统用户在启动Vim时,都会加载这个配置文件。…

    Linux 2023年6月13日
    092
  • 怎么实现Redis的高可用?(主从、哨兵、集群)

    高可用有两个含义: 一是数据尽量不丢失,二是保证服务尽可能可用。 AOF 和 RDB 数据持久化保证了数据尽量不丢失,那么多节点来保证服务尽可能提供服务。 一般在实际生产中,服务不…

    Linux 2023年5月28日
    098
  • 搭建一个完整的K8S集群——-基于CentOS 8系统

    创建三个centos节点: 192.168.5.141 k8s-master 192.168.5.142 k8s-nnode1 192.168.5.143 k8s-nnode2 查…

    Linux 2023年6月7日
    090
  • Tomcat 介绍及使用教程

    镜像下载、域名解析、时间同步请点击阿里云开源镜像站 1. Tomcat 介绍 Apache Tomcat 是由 Apache Software Foundation(ASF)开发的…

    Linux 2023年5月27日
    079
  • [ Python ] 音视频剪辑

    https://www.cnblogs.com/yeungchie/ 视频 提取视频 from moviepy.editor import * 从 MP4 文件提取 video =…

    Linux 2023年6月7日
    095
  • Linux安装宝塔,OCI8,连接ORACLE数据库

    基础环境 centos7 安装BT宝塔 &#x7F51;&#x5740;&#xFF1A;https://www.bt.cn/download/linux.h…

    Linux 2023年6月6日
    0115
  • 服务器部署 Vue 和 Django 项目的全记录

    本篇记录我在一个全新服务器上部署 Vue 和 Django 前后端项目的全过程,内容包括服务器初始配置、安装 Django 虚拟环境、python web 服务器 uWSGI 和反…

    Linux 2023年6月7日
    0119
  • Centos7 找回root密码

    在开机界面,按”e”进入编辑界面 按”e”进图下图界面后,找到开头为”linux16″行,在行后面加入 &#…

    Linux 2023年5月27日
    0147
  • 记录XorDDos木马清理步骤

    1.检查 查看定时任务文件发现有两个异常定时任务 [root@manage ~]# cat /etc/crontab user-name command to be execute…

    Linux 2023年6月7日
    0102
  • linux设备模型及实例

    1.linux设备模型基本概念 BUS(总线):用于关联设备和驱动,代表一个实际的物理总线(如USB、PCI bus)或虚拟总线(如platform bus),总线会提供与总线相关…

    Linux 2023年6月6日
    0104
  • php发送邮件 (phpmailer)

    1.首先下载phpMailer文件官方文件https://sourceforge.net/projects/phpmailer/; 还有class.smtp.php. 2.去配置一…

    Linux 2023年6月14日
    087
  • VRRP配置即实验

    VRRP 概念: VRRP 全称是虚拟路由器冗余协议,它是一种容错协议。该协议通过把几台路由设备联合组成一台虚拟的路由设备,该虚拟路由器在本地局域网拥有唯一的一个虚拟ID和虚拟IP…

    Linux 2023年6月6日
    085
  • ETCD分布式存储部署

    一、ETCD 概述 ETCD 是一个分布式一致性k-v存储系统,可用于服务注册发现与共享配置。具有一下优点: 简单: 相比于晦涩难懂的paxos算法,etcd基于相对简单且易实现的…

    Linux 2023年6月14日
    0103
  • DSTAT, Versatile resource statistics tool, 多功能资源统计工具;

    之前用到的一个IO监控工具,今天要使用,却忘记了名字,记录一下: dstat命令是一个用来替换vmstat、iostat、netstat、nfsstat和ifstat这些命令的工具…

    Linux 2023年6月13日
    090
  • 重写并自定义依赖的原生的Bean方法

    转载请注明出处: 在项目开发过程中,往往是直接应用很多jar包中依赖且声明好的Bean,拿来即用,但很多场景也需要对这些原生的Bean 进行自定义,定制化封装,这样在项目使用的过程…

    Linux 2023年6月15日
    0129
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球