【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)

大家都在看

  • Git常用命令

    克隆拉取远程代码 git clone https://xxxxxxxxx 本地添加远程仓库地址 git remote add origin(设定名字,随意。不过一般都叫这个名字) …

    Linux 2023年6月8日
    078
  • 剑指offer计划链表

    剑指offer计划链表 从尾到头打印链表 /** * public class ListNode { * int val; * ListNode next = null; * * …

    Linux 2023年6月11日
    068
  • alloc_pages的实现浅析

    alloc_pages的使用 struct page *alloc_pages(gft_t gfp, unsigned int order) alloc_pages定义于 inux…

    Linux 2023年6月7日
    098
  • 根据温度、气压计算海拔高度

    基本概念 标准大气压:表示气压的单位,习惯上常用水银柱高度。例如,一个标准大气压等于760毫米高的水银柱的重量,它相当于一平方厘米面积上承受1.0336公斤重的大气压力。由于各国所…

    Linux 2023年6月8日
    087
  • make

    make 背景 gcc 在编译一个包含多个源文件的工程时, gcc需要将每一个源文件都编译一遍,任何再链接起来形成一个可执行文件。实际上,用户很少对所有源文件都进行修改,这就会造成…

    Linux 2023年6月7日
    093
  • Linux命令之find、grep、echo、tar、whoami、uname

    1. whoami–查看当前登录的用户名 book@100ask:~/linux$ whoami book 2. echo–打印命令,配合’&g…

    Linux 2023年6月6日
    090
  • http代理连接

    基于Linux服务器的http代理连接 1. 准备工作 &#x76EE;&#x6807;&#x670D;&#x52A1;&#x5668; &…

    Linux 2023年5月27日
    093
  • kafka 在 zookeeper 中保存的数据内容

    转载请注明出处: 服务器上下载 kafka : wget https://archive.apache.org/dist/kafka/2.4.0/kafka_2.12-3.2.0….

    Linux 2023年6月14日
    086
  • 虚拟机Ubuntu22.04 chrome页面显示异常

    虚拟机上ubuntu安装chrome出现页面显示异常的解决办法 将chrome上的硬件加速关掉就能恢复正常 具体原因是啥,有没有大佬能解答以下啊 引用:https://blog.c…

    Linux 2023年6月13日
    078
  • SpringBoot——自定义Redis缓存Cache

    SpringBoot自带Cache存在问题: 1.生成Key过于简单,容易冲突 默认为cacheNames + ":" + Key2.无法设置过期时间,默认时间…

    Linux 2023年5月28日
    088
  • LeetCode-349. 两个数组的交集

    题目来源 题目详情 给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。 示例 1: 输入: n…

    Linux 2023年6月7日
    066
  • [ Linux ] column 简明用法

    options -c 指定每一行输出的宽度。 -t 判断列来输出,对齐所有列。 主要用到的就是这个选项。 -s 指定分隔符,默认为空白符。 -o 指定用于对齐列填充的符号,默认为空…

    Linux 2023年6月7日
    092
  • linux中python虚拟环境的创建及问题

    linux中python虚拟环境的创建 LINUX 出现 -BASH-4.2# 问题的解决方法 linux中python虚拟环境的创建 1)安装依赖 >: pip3 inst…

    Linux 2023年6月14日
    077
  • 如何优雅的处理 accept 出现 EMFILE 的问题

    通常情况下,服务端调用 accept 函数会返回一个新的文件描述符,用于和客户端之间的数据传输 在服务器的开发中,有时会遇到这种情况:当调用 accept 函数接受客户端连接,函数…

    Linux 2023年6月13日
    087
  • Ubuntu 18.04安装sysv-rc-conf

    sudo nano /etc/apt/sources.list &#x52A0;&#x5165;deb http://archive.ubuntu.com/ubun…

    Linux 2023年6月13日
    079
  • [20220811]奇怪的隐式转换问题.txt

    [20220811]奇怪的隐式转换问题.txt –//生产系统遇到一个奇怪的隐式转换问题,问题在于没有发生隐式转换,做一个分析调查。 –//后记:后面的分析…

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