Linux网络编程Socket通信4-poll/epoll/reator/threadpoll

TCP_Socket通信4-poll/epoll/reator/threadpoll

poll

相对于select的优点是没有最大文件描述符限制,请求和返回时分离的。

一个进程最大能打开的socket文件描述符大小

cat /proc/sys/fs/file-max

haitu@ubuntu:~$ cat /proc/sys/fs/file-max
194530
# 可以通过修改配置文件的方式来修改该上限值
sudo vi /etc/security/limits.conf
# 在文件尾部写入以下配置,soft软限制,hard硬限制
* soft nofile 65536
* hard nofile 100000

pollAPI详解

#include <poll.h>

/*在linux系统的/usr/include/features.h文件中*/
/*打开_GNU_SOURCE这个宏可以打开一些功能,比如为了在Linux系统上编译使用带有检测文件type的宏*/
#define _GNU_SOURCE         /* See feature_test_macros(7) */

/*监听多个文件描述符的属性变化*/
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
int ppoll(struct pollfd *fds, nfds_t nfds,
               const struct timespec *tmo_p, const sigset_t *sigmask);

struct pollfd *fds:监听的数组的地址
nfds_t nfds:监控数组中有多少个文件描述符需要被监控,一般填写nfds数组有效元素最大下标+1
int timeout:超时时间,毫秒级等待,-1:永久监听

struct pollfd {
    int   fd;         /* file descriptor 文件描述符*/
    short events;     /* requested events 监听的事件*/
    short revents;    /* returned events 监听事件中满足条件返回的事件*/
};
short events: // 主要使用的就是POLLIN和POLLOUT,也就是读事件和写事件
	*POLLIN			普通或带外优先数据可读,即 POLLRDNORM | POLLRDBAND
	POLLRDNORM		数据可读
	POLLRDBAND		优先级带数据可读
	POLLPRI 		高优先级可读数据
	*POLLOUT		普通或带外数据可写
	POLLWRNORM		数据可写
	POLLWRBAND		优先级带数据可写
	POLLERR 		发生错误
	POLLHUP 		发生挂起
	POLLNVAL 		描述字不是一个打开的文件

poll_server代码

/* server.c */  
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <netinet/in.h>  
#include <arpa/inet.h>  
#include <poll.h>  
#include <errno.h>  
#include "wrap.h"  

#define MAXLINE 80  
#define SERV_PORT 6666  
#define OPEN_MAX 1024  
  
int main(int argc, char *argv[])  
{  
    int i, j, maxi, listenfd, connfd, sockfd;  
    int nready;  
    ssize_t n;  
    char buf[MAXLINE], str[INET_ADDRSTRLEN];  
    socklen_t clilen;  
    struct pollfd client[OPEN_MAX]; //  poll监听数组 
    struct sockaddr_in cliaddr, servaddr;  
  
    listenfd = Socket(AF_INET, SOCK_STREAM, 0);  
  
    bzero(&servaddr, sizeof(servaddr));  
    servaddr.sin_family = AF_INET;  
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);  
    servaddr.sin_port = htons(SERV_PORT);  
  
    Bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));  
    
    Listen(listenfd, 20);  
  
    client[0].fd = listenfd; 
    client[0].events = POLLRDNORM;                  /* listenfd监听普通读事件 */  
  
    for (i = 1; i < OPEN_MAX; i++)  
        client[i].fd = -1;                          /* 用-1初始化client[]里剩下元素 */  
    maxi = 0;                                       /* client[]数组有效元素中最大元素下标 */  
  
    for (;;) {  
        nready = poll(client, maxi+1, -1);          /* 阻塞 返回被监听到的数据个数*/ 
        /*监听lfd,查看是否有客户端连接进来*/
        if (client[0].revents & POLLRDNORM) {       /* 有客户端链接请求 */  
            /*这里不写为client[0].revents == POLLRDNORM, 因为可能返回监听了多个
            events = POLLWRNORM | POLLRDNORM,比如监听了可读可写,那么就可能会出现错误。
            这里需要将events和revents理解为位图,只需要判断你需要的那一位发生变化
            */
            clilen = sizeof(cliaddr);  
            connfd = Accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);  
            printf("received from %s at PORT %d\n",  
                    inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)),  
                    ntohs(cliaddr.sin_port));  
            for (i = 1; i < OPEN_MAX; i++) {  
                if (client[i].fd < 0) {  
                    client[i].fd = connfd;
                    /* 找到client[]中空闲的位置,存放accept返回的connfd */  
                    break;  
                }  
            }  
  
            if (i == OPEN_MAX)  
                perr_exit("too many clients");  
  
            client[i].events = POLLRDNORM;      /* 设置刚刚返回的connfd,监控读事件 */  
            if (i > maxi)  
                maxi = i;                       /* 更新client[]中最大元素下标 */  
            if (--nready <= 0)  
                continue;                       /* 没有更多就绪事件时,继续回到poll阻塞 */  
        }  
        /*对连接进来的客户端文件描述符进行监听*/
        for (i = 1; i <= maxi; i++) {            /* 检测client[] */  
            if ((sockfd = client[i].fd) < 0)  
                continue;  
            if (client[i].revents & (POLLRDNORM | POLLERR)) {  // 被监听到
                if ((n = Read(sockfd, buf, MAXLINE)) < 0) {  
                    if (errno == ECONNRESET) { /* 当收到 RST标志时 */  
                        /* connection reset by client */  
                        printf("client[%d] aborted connection\n", i);  
                        Close(sockfd);  
                        client[i].fd = -1;  
                    } else {  
                        perr_exit("read error");  
                    }  
                } else if (n == 0) {  
                    /* connection closed by client */  
                    printf("client[%d] closed connection\n", i);  
                    Close(sockfd);  
                    client[i].fd = -1;  
                } else {  
                    for (j = 0; j < n; j++)  
                        buf[j] = toupper(buf[j]);  
                        Writen(sockfd, buf, n);  
                }  
                if (--nready <= 0)  
                    break;              /* no more readable descriptors */  
            }  
        }  
    }  
    return 0;  
}  

poll_client代码

#include "wrap.h"
#include <poll.h>

#define SERV_PORT 6666

int main()
{
    struct sockaddr_in servaddr;
    char buf[1024];
    int n;

    int sockfd = Socket(AF_INET, SOCK_STREAM, 0);

    bzero(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(SERV_PORT);
    inet_pton(AF_INET,"127.0.0.1",&servaddr.sin_addr.s_addr);

    Connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    while (fgets(buf, 1024, stdin) != NULL) {
        // scanf("%s", buf);
		Write(sockfd, buf, strlen(buf));
		n = Read(sockfd, buf, 1024);
		if (n == 0)
			printf("the other side has been closed.\n");
		else
			Write(STDOUT_FILENO, buf, n);
	}
	Close(sockfd);

    return 0;
}

poll的优缺点

相对于select函数来说,没有文件描述符的1024的限制。
请求和返回都是分离的。
缺点和select是一样的,每次都是需要将监听的文件描述符都从应用层拷贝到内核。
每次都需要将数组中的元素遍历一遍,才知道哪一个发生了变化。
大量并发,少量活跃的效率低下。

epoll

  • 第一步:将创建一颗红黑树
  • 第二步:将需要监听的文件描述符上树
  • 第三步:监听
  • 特点:没有文件描述符的限制,不需要每次将文件描述符拷贝到内核。不需要进行遍历,
    返回的是变化的文件描述符,我们使用数组接收,不需要在进行遍历。

epollPAI详解

// a>创建红黑树
#include <sys/epoll.h>
int epoll_create(int size); // int epfd = epoll_create(1);
int epoll_create1(int flags); // int epfd = epoll_createl(0);
// 参数size:需要监听的文件描述符的上限,2.6版本以后,填1即可
// 返回值:指向新创建的红黑树的根节点的fd(红黑树的句柄) 失败返回-1 errno

// b>上树、下数、修改节点
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 参数epfd:红黑树的句柄
// 参数op:EPOLL_CTL_ADD(上树)、EPOLL_CTL_MOD(修改)、EPOLL_CTL_DE(下树)
// 参数fd:上树、下树、修改的文件描述符
// 参数event:上树的监听事件、
// 返回值:成功返回0 失败返回-1
typedef union epoll_data {
    void        *ptr; // 用于传递私有数据的
    int          fd; /*需要监听的文件描述符*/
    uint32_t     u32;
    uint64_t     u64;
} epoll_data_t;

struct epoll_event {
    uint32_t     events;      /* Epoll events 需要监听的事件*/
    epoll_data_t data;        /* User data variable 需要监听的文件描述符*/
};
// 将cfd上树
int epfd = epoll_create(1);
struct epoll_event ev;
ev.data.fd = cfd;
ev.eventd = EPOLLIN;
epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev);

// c>监听树上的文件描述符的变化
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
               int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events,
                int maxevents, int timeout,
                const sigset_t *sigmask);
// 参数epfd:树的句柄
// 参数events:返回的结构体数组节点,传出参数
// 参数maxevents:数组元素的个数
// 参数timeout:-1 阻塞监听,永久监听 大于等于0 限时等待
// 返回值:满足监听的文件描述符个数,失败返回-1 errno
struct epoll_event resevent[10];
 res = epoll_wait(epfd, resevent, 10, -1);

epoll特点

没有文件描述符1024的限制,以后每次监听都不需要再次将监听的文件描述符拷贝到内核中。不需要在遍历树,返回的是已经变化的文件描述符。

父子进程管道通信epoll监听

#include <stdio.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <errno.h>
#include <unistd.h>

#define MAXLINE 10

int main(int argc, char *argv[])
{
    int efd, i;
    int pfd[2];
    pid_t pid;
    char buf[MAXLINE], ch = 'a';

    pipe(pfd);  // 创建管道
    pid = fork();

    if (pid == 0) {             //子 写
        close(pfd[0]); // close read
        while (1) {
            //aaaa\n
            for (i = 0; i < MAXLINE/2; i++)
                buf[i] = ch;
            buf[i-1] = '\n';
            ch++;
            //bbbb\n
            for (; i < MAXLINE; i++)
                buf[i] = ch;
            buf[i-1] = '\n';
            ch++;
            //aaaa\nbbbb\n
            write(pfd[1], buf, sizeof(buf));
            sleep(5);
        }
        close(pfd[1]);

    } else if (pid > 0) {       //父 读
        struct epoll_event event;
        struct epoll_event resevent[10];        //epoll_wait就绪返回event
        int res, len;

        close(pfd[1]); // close write
        efd = epoll_create(1);

        event.events = EPOLLIN | EPOLLET;     // ET 边沿触发
       // event.events = EPOLLIN;                 // LT 水平触发 (默认)
        event.data.fd = pfd[0];
        epoll_ctl(efd, EPOLL_CTL_ADD, pfd[0], &event);

        while (1) {
            res = epoll_wait(efd, resevent, 10, -1);
            printf("res %d\n", res);
            if (resevent[0].data.fd == pfd[0]) {
                len = read(pfd[0], buf, MAXLINE/2);
                write(STDOUT_FILENO, buf, len);  // 写到屏幕上
            }
        }
        close(pfd[0]);
        close(efd);
    } else {
        perror("fork");
        exit(-1);
    }
    return 0;
}

epoll_sever

#include <stdio.h>
#include "./wrap/wrap.h"
#include <sys/epoll.h>
#include <sys/socket.h>

#define PORT 8000
#define IP "127.0.0.1"

int main(int argc, char *argv[])
{
    struct epoll_event ev;// 用于创建红黑树
    struct epoll_event reev[1024];// 用于接收epoll_wait返回的信息
    memset(reev,0,sizeof(reev));
    struct sockaddr_in cliaddr;// 用作Accept的时候传出客户端信息
    socklen_t clilen; 
    int res,i,ret,n,j;
    int lfd,epfd,sockfd; // 分别为监听套接字、红黑树节点套接字、客户端套接字
    char buf[1024];
    // 创建套接字
    lfd = tcp4bind(PORT, IP);
    // 监听
    listen(lfd, 128);
    // 创建树
    epfd = epoll_create(1);
    // 上树
    ev.data.fd = lfd;
    ev.events = EPOLLIN;// 写事件
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
    // 循环监听
    for(;;)
    {
        // 监听
        res = epoll_wait(epfd, reev, lfd, -1);
        if(res == -1)
        {
            perror("epoll_wait");
            break;
        }else if(res == 0)
        {
            continue;
        }
        for(i = 0;i < res; i++)
        {
            if(!(reev[i].events & EPOLLIN))
            {
                continue;  // 如果不是读事件,那么就直接继续
            }
            if(reev[i].data.fd == lfd)  // 监控到服务端fd发生变化
            {
                printf("accept\n");
                int cfd = Accept(lfd, (struct sockaddr*)&cliaddr, &clilen);
                if(cfd == -1)
                {
                    perror("accept");
                    break;
                }
                print_clientInfo(cliaddr);  // 打印出来ip地址
                // 上树
                ev.data.fd = cfd;
                ev.events = EPOLLIN;
                ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
                if(ret == -1)
                {
                    perror("epoll_ctl");
                    break;
                }
            }
            else { // 监控到客户端fd变化

                sockfd = reev[i].data.fd; 

                n = Read(sockfd,buf,1024);
                if(n < 0)
                {
                    perror("read err");
                    epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,NULL);  // 从红黑树中删除
                    // epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,&reev[i]);
                    Close(sockfd);
                }
                else if(n == 0)
                {   
                    printf("----%d----\n",sockfd);
                    epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,NULL); // 从红黑树中删除
                    Close(sockfd);
                }
                else
                {
                    for (j = 0;j<n;j++)
                    {
                        buf[j] = toupper(buf[j]);  // 转为大写的
                    }
                    Write(STDOUT_FILENO, buf, n); // 写到屏幕上
                    Write(sockfd,buf,n);  // 写回去客户端
                }
            }
        }
    }
    Close(lfd);
    Close(epfd);

    return 0;
}

epoll的两种工作方式

  • 水平触发(LT):持续的高电平或者持续的低电平。
    当监听读的时候,只要缓冲区有数据的话,那么就会触发epoll_wait()
    缓冲区中还有未读尽的数据会导致epoll_wait的返回。

    监听写缓冲区的时候,只要可以写,就会触发,所以只能使用边缘触发。

event.events = EPOLLIN;   // LT 水平触发 (默认)
event.data.fd = pfd;
epoll_ctl(efd, EPOLL_CTL_ADD, pfd, &event);
  • 边沿触发(ET):电平有高到低或者低到高的变化。
    当监听读的时候,边沿触发只有数据到来才触发epoll_wait(),不管缓存区中是否还有数据。
    缓冲区剩余未读尽的数据不会导致 epoll_wait 返回。 新的事件满足,才会触发。所以我们要求一次性的将数据全部读取完毕,那么就要求监听的客户端文件描述符是非阻塞的。

    当监听写缓冲区的时候,数据从有到无就会触发,也就是数据发送完毕以后触发epoll_wait

event.events = EPOLLIN | EPOLLET;  /* ET 边沿触发 ,默认是水平触发 */
event.data.fd = pfd;  
epoll_ctl(efd, EPOLL_CTL_ADD, pfd, &event); 

/*设置cfd为非阻塞的模式*/
int flags = fcntl(cfd, F_GETFL); // 获取cfd的标志位
flags |= O_NONBLOCK; // 设置为非阻塞模式
fcntl(cfd, F_SETFL, flags);

高速模式=边沿触发+非阻塞

要解决数据读不尽的问题,可以使用循环读取的方式,此时就要设置sockfd为非阻塞模式

#include <asm-generic/errno-base.h>
#include <stdio.h>
#include "./wrap/wrap.h"
#include <sys/epoll.h>
#include <sys/fcntl.h>
#include <sys/socket.h>

#define PORT 8000
#define IP "127.0.0.1"

int main(int argc, char *argv[])
{
    struct epoll_event ev;
    struct epoll_event reev[1024];
    memset(reev,0,sizeof(reev));
    struct sockaddr_in cliaddr;
    socklen_t clilen; 
    int res,i,ret,n,j;
    int lfd,epfd,sockfd;
    char buf[1024];
    // 创建套接字 socket bind 设置端口复用
    lfd = tcp4bind(PORT, IP);
    // 监听
    listen(lfd, 128);
    // 创建树
    epfd = epoll_create(1);
    // 上树
    ev.data.fd = lfd;
    ev.events = EPOLLIN; /*lfd一般不用设置为边沿触发*/
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);
    // 循环监听
    for(;;)
    {
        // 监听
        res = epoll_wait(epfd, reev, 1024, -1);
        printf("------ epoll_wait begin -------");
        if(res == -1)
        {
            perror("epoll_wait");
            break;
        }
        for(i = 0;i<res;i++)
        {
            if(!(reev[i].events & EPOLLIN))
            {
                continue;  // 如果不是读事件,那么就直接继续
            }
            if(reev[i].data.fd == lfd)  // 监控到服务端fd发生变化
            {
                printf("accept\n");
                int cfd = Accept(lfd, (struct sockaddr*)&cliaddr, &clilen);
                if(cfd == -1)
                {
                    perror("accept");
                    break;
                }
                print_clientInfo(cliaddr);  // 打印出来ip地址

                /*设置cfd为非阻塞的模式*/
                int flags = fcntl(cfd,F_GETFL); // 获取cfd的标志位
                flags |= O_NONBLOCK; // 设置为非阻塞模式
                fcntl(cfd,F_SETFL,flags);

                // 上树
                ev.data.fd = cfd;
                ev.events = EPOLLIN | EPOLLET; // 设置为边缘触发
                ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
                if(ret == -1)
                {
                    perror("epoll_ctl");
                    break;
                }
            }
            else { // 监控到客户端fd变化

                sockfd = reev[i].data.fd; 
                while(1)
                {
                    // 如果读取一个缓冲区,缓冲区没有数据,如果是带阻塞就阻塞等待
                    // 如果是非阻塞,返回值为-1 并且将errno的值设置为 EAGAIN
                    n = Read(sockfd,buf,1024);
                    if(n < 0)
                    {
                        if(errno == EAGAIN)  // 缓冲区没有数据的时候
                        {
                            printf("read finish"); // 缓冲区读取干净
                            break; // 结束去监听
                        }
                        perror("read err");
                        epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,NULL);  // 从中删除
                        Close(sockfd);
                        break;
                    }
                    else if(n == 0)
                    {   
                        printf("----%d----\n",sockfd);
                        epoll_ctl(epfd,EPOLL_CTL_DEL,sockfd,NULL);
                        Close(sockfd);
                        break;
                    }
                    else
                    {
                        for (j = 0;j<n;j++)
                        {
                            buf[j] = toupper(buf[j]);  // 转为大写的
                        }
                        Write(STDOUT_FILENO, buf, n); // 写到屏幕上
                        Write(sockfd,buf,n);  // 写回去客户端
                    }
                }               
            }
        }
    }
    Close(lfd);
    Close(epfd);

    return 0;
}

三种io复用对比

select 跨平台, poll 没有1024文件描述符的限制,缺点也很明显,每次监听都需要将监听到的信息从应用层拷贝到内存,返回变化的文件描述符的个数,具体那个文件描述符需要遍历,大量并发、少量活跃效率比较低

epoll 优点:没有1024文件描述符的限制,下次监听不需要将需要监听的文件描述符从应用层再次拷贝到内核,返回的是已经变化的文件描述符,不需要遍历,大量并发,少量活跃效率高。

epoll反应堆reator

当文件描述符发生事件去进行回调函数处理数据,将他们封装到一起。原理如下:

  • 初始化函数initlistensocket中将回调函数设置为acceptconn,添加服务器描述符lfd上树。
  • poll_wait监控到到lfd后调用函数acceptconn,产生的客户端cfd,将他的回调函数设置为recvdata
  • 当监控到客户端cfd变化,那么就会调用recvdata,在recvdata函数中将回调函数修改为senddata函数,并将事件修改为EPOLLOUT
  • 也就是接收到数据,然后客户端cfd的读缓冲区中接收到了数据,会触发epoll_wait,进行处理数据,将数据处理完成后,又将回调函数更改为recvdata,重写进行接收数据,修改事件为EPOLLIN
#include <stdio.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>

#define MAX_EVENTS 1024 /*监听上限*/
#define BUFLEN  4096    /*缓存区大小*/
#define SERV_PORT 6666  /*端口号*/

void recvdata(int fd,int events,void *arg);
void senddata(int fd,int events,void *arg);

/*描述就绪文件描述符的相关信息*/
struct myevent_s
{
    int fd;             //要监听的文件描述符
    int events;         //对应的监听事件,EPOLLIN和EPLLOUT
    void *arg;          //指向自己结构体指针
    void (*call_back)(int fd,int events,void *arg); //回调函数
    int status;         //是否在监听:1->在红黑树上(监听), 0->不在(不监听)
    char buf[BUFLEN];   
    int len;
    long last_active;   //记录每次加入红黑树 g_efd 的时间值
};

int g_efd;      //全局变量,作为红黑树根
struct myevent_s g_events[MAX_EVENTS+1];    //自定义结构体类型数组. +1-->listen fd


/*
 * 封装一个自定义事件,包括fd,这个fd的回调函数,还有一个额外的参数项
 * 注意:在封装这个事件的时候,为这个事件指明了回调函数,一般来说,一个fd只对一个特定的事件
 * 感兴趣,当这个事件发生的时候,就调用这个回调函数
 */
void eventset(struct myevent_s *ev, int fd, void (*call_back)(int fd,int events,void *arg), void *arg)
{
    ev->fd = fd;
    ev->call_back = call_back;
    ev->events = 0;
    ev->arg = arg;
    ev->status = 0;
    if(ev->len <= 0)
    {
        memset(ev->buf, 0, sizeof(ev->buf));
        ev->len = 0;
    }
    ev->last_active = time(NULL); //调用eventset函数的时间
    return;
}

/* 向 epoll监听的红黑树 添加一个文件描述符 */
void eventadd(int efd, int events, struct myevent_s *ev)
{
    struct epoll_event epv={0, {0}};
    int op = 0;
    epv.data.ptr = ev; // ptr指向一个结构体(之前的epoll模型红黑树上挂载的是文件描述符cfd和lfd,现在是ptr指针)
    epv.events = ev->events = events; //EPOLLIN 或 EPOLLOUT
    if(ev->status == 0)       //status 说明文件描述符是否在红黑树上 0不在,1 在
    {
        op = EPOLL_CTL_ADD; //将其加入红黑树 g_efd, 并将status置1
        ev->status = 1;
    }
    if(epoll_ctl(efd, op, ev->fd, &epv) < 0) // 添加一个节点
        printf("event add failed [fd=%d],events[%d]\n", ev->fd, events);
    else
        printf("event add OK [fd=%d],events[%0X]\n", ev->fd, events);
    return;
}

/* 从epoll 监听的 红黑树中删除一个文件描述符*/
void eventdel(int efd,struct myevent_s *ev)
{
    struct epoll_event epv = {0, {0}};
    if(ev->status != 1) //如果fd没有添加到监听树上,就不用删除,直接返回
        return;
    epv.data.ptr = NULL;
    ev->status = 0;
    epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv);
    return;
}

/*  当有文件描述符就绪, epoll返回, 调用该函数与客户端建立链接 */
void acceptconn(int lfd,int events,void *arg)
{
    struct sockaddr_in cin;
    socklen_t len = sizeof(cin);
    int cfd, i;
    if((cfd = accept(lfd, (struct sockaddr *)&cin, &len)) == -1)
    {
        if(errno != EAGAIN && errno != EINTR)
        {
            sleep(1);
        }
        printf("%s:accept,%s\n",__func__, strerror(errno));
        return;
    }
    do
    {
        for(i = 0; i < MAX_EVENTS; i++) //从全局数组g_events中找一个空闲元素,类似于select中找值为-1的元素
        {
            if(g_events[i].status ==0)
                break;
        }
        if(i == MAX_EVENTS) // 超出连接数上限
        {
            printf("%s: max connect limit[%d]\n", __func__, MAX_EVENTS);
            break;
        }
        int flag = 0;
        if((flag = fcntl(cfd, F_SETFL, O_NONBLOCK)) < 0) //将cfd也设置为非阻塞
        {
            printf("%s: fcntl nonblocking failed, %s\n", __func__, strerror(errno));
            break;
        }
        eventset(&g_events[i], cfd, recvdata, &g_events[i]); //找到合适的节点之后,将其添加到监听树中,并监听读事件
        eventadd(g_efd, EPOLLIN, &g_events[i]);
    }while(0);

    printf("new connect[%s:%d],[time:%ld],pos[%d]",inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), g_events[i].last_active, i);
    return;
}

/*读取客户端发过来的数据的函数*/
void recvdata(int fd, int events, void *arg)
{
    struct myevent_s *ev = (struct myevent_s *)arg;
    int len;

    len = recv(fd, ev->buf, sizeof(ev->buf), 0);    //读取客户端发过来的数据

    eventdel(g_efd, ev);                            //将该节点从红黑树上摘除

    if (len > 0) 
    {
        ev->len = len;
        ev->buf[len] = '\0';                        //手动添加字符串结束标记
        printf("C[%d]:%s\n", fd, ev->buf);                  

        eventset(ev, fd, senddata, ev);             //设置该fd对应的回调函数为senddata    
        eventadd(g_efd, EPOLLOUT, ev);              //将fd加入红黑树g_efd中,监听其写事件   
    } 
    else if (len == 0) 
    {
        close(ev->fd);
        /* ev-g_events 地址相减得到偏移元素位置 */
        printf("[fd=%d] pos[%ld], closed\n", fd, ev-g_events);
    } 
    else 
    {
        close(ev->fd);
        printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
    }   
    return;
}

/*发送给客户端数据*/
void senddata(int fd, int events, void *arg)
{
    struct myevent_s *ev = (struct myevent_s *)arg;
    int len;

    len = send(fd, ev->buf, ev->len, 0);    //直接将数据回射给客户端

    eventdel(g_efd, ev);                    //从红黑树g_efd中移除

    if (len > 0) 
    {
        printf("send[fd=%d], [%d]%s\n", fd, len, ev->buf);
        eventset(ev, fd, recvdata, ev);     //将该fd的回调函数改为recvdata
        eventadd(g_efd, EPOLLIN, ev);       //重新添加到红黑树上,设为监听读事件
    }
    else 
    {
        close(ev->fd);                      //关闭链接
        printf("send[fd=%d] error %s\n", fd, strerror(errno));
    }
    return ;
}

/*创建 socket, 初始化lfd */

void initlistensocket(int efd, short port)
{
    struct sockaddr_in sin;

    int lfd = socket(AF_INET, SOCK_STREAM, 0);
    fcntl(lfd, F_SETFL, O_NONBLOCK);                //将socket设为非阻塞

    memset(&sin, 0, sizeof(sin));               //bzero(&sin, sizeof(sin))
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = INADDR_ANY;
    sin.sin_port = htons(port);

    bind(lfd, (struct sockaddr *)&sin, sizeof(sin));

    listen(lfd, 20);

    /* void eventset(struct myevent_s *ev, int fd, 
    	void (*call_back)(int, int, void *), void *arg);  */
    eventset(&g_events[MAX_EVENTS], lfd, acceptconn, &g_events[MAX_EVENTS]);    

    /* void eventadd(int efd, int events, struct myevent_s *ev) */
    eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]);  //将lfd添加到监听树上,监听读事件

    return;
}

int main()
{
    int port=SERV_PORT;

    g_efd = epoll_create(MAX_EVENTS + 1); //创建红黑树,返回给全局 g_efd
    if(g_efd <= 0)
            printf("create efd in %s err %s\n", __func__, strerror(errno));
    
    initlistensocket(g_efd, port); //初始化监听socket
    
    struct epoll_event events[MAX_EVENTS + 1];  
    //定义这个结构体数组,用来接收epoll_wait传出的满足监听事件的fd结构体
    printf("server running:port[%d]\n", port);

    int checkpos = 0;
    int i;
    while(1)
    {
        long now = time(NULL);
        for(i=0; i < 100; i++, checkpos++)
        {
            if(checkpos == MAX_EVENTS);
                checkpos = 0;
            if(g_events[checkpos].status != 1)
                continue;
            long duration = now -g_events[checkpos].last_active;
            if(duration >= 60)
            {
                close(g_events[checkpos].fd);
                printf("[fd=%d] timeout\n", g_events[checkpos].fd);
                eventdel(g_efd, &g_events[checkpos]);
            }
        } 
        //调用eppoll_wait等待接入的客户端事件,
        //epoll_wait传出的是满足监听条件的那些fd的struct epoll_event类型
        int nfd = epoll_wait(g_efd, events, MAX_EVENTS+1, 1000);
        if (nfd < 0)
        {
            printf("epoll_wait error, exit\n");
            exit(-1);
        }
        for(i = 0; i < nfd; i++)
        {
		    //evtAdd()函数中,添加到监听树中监听事件的时候将myevents_t结构体类型给了ptr指针
            //这里epoll_wait返回的时候,同样会返回对应fd的myevents_t类型的指针
            struct myevent_s *ev = (struct myevent_s *)events[i].data.ptr;
            //如果监听的是读事件,并返回的是读事件
            if((events[i].events & EPOLLIN) &&(ev->events & EPOLLIN))
            {
                ev->call_back(ev->fd, events[i].events, ev->arg);
            }
            //如果监听的是写事件,并返回的是写事件
            if((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT))
            {
                ev->call_back(ev->fd, events[i].events, ev->arg);
            }
        }
    }
    return 0;
}

线程池

线程池概念

把一个或多个线程通过统一的方式进行调度和重复使用的技术,避免了因为线程过多而带来使用上的开销。线程不断的去取任务来执行,如果没有任务休眠。

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  • 提高线程的可管理性。

创建一定数量的线程,和一个任务队列,线程池中的线程不停的去任务队列中取任务,又任务到来直接往任务队列中添加。其实也就是生产者消费者模型。线程池处理的任务所需要的事件必须要很短,线程池中线程也不宜太多。

  • 任务队列中需要线程锁,队列一次只能由单个线程访问。
  • 条件变量1,限制任务队列为空,阻塞消费者消费(线程抢到锁,当任务队列中没有任务的时候,线程中调用pthread_cond_wait等待任务,在pthread_cond_wait函数中的执行步骤为解锁、等待条件、给线程发送信号(pthread_cond_signal或者pthread_cond_broadcast),然后线程抢锁,其中前面的两个步骤是原子操作,不可分割的)。
  • 条件变量2,限制任务队列为满,阻塞生产者添加任务。
  • 队列需要考虑使用循环队列

threadpoll.c

#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include "threadpool.h"

#define DEFAULT_TIME 10                 /*10s检测一次*/
#define MIN_WAIT_TASK_NUM 10            /*如果queue_size > MIN_WAIT_TASK_NUM 添加新的线程到线程池*/ 
#define DEFAULT_THREAD_VARY 10          /*每次创建和销毁线程的个数*/
#define true 1
#define false 0

typedef struct {
    void *(*function)(void *);          /* 函数指针,回调函数 */
    void *arg;                          /* 上面函数的参数 */
} threadpool_task_t;                    /* 各子线程任务结构体 */

/* 描述线程池相关信息 */

struct threadpool_t {
    pthread_mutex_t lock;               /* 用于锁住本结构体 */    
    pthread_mutex_t thread_counter;     /* 记录忙状态线程个数de琐 -- busy_thr_num */

    pthread_cond_t queue_not_full;      /* 当任务队列满时,添加任务的线程阻塞,等待此条件变量 */
    pthread_cond_t queue_not_empty;     /* 任务队列里不为空时,通知等待任务的线程 */

    pthread_t *threads;                 /* 存放线程池中每个线程的tid。数组 */
    pthread_t adjust_tid;               /* 存管理线程tid */
    threadpool_task_t *task_queue;      /* 任务队列(数组首地址) */

    int min_thr_num;                    /* 线程池最小线程数 */
    int max_thr_num;                    /* 线程池最大线程数 */
    int live_thr_num;                   /* 当前存活线程个数 */
    int busy_thr_num;                   /* 忙状态线程个数 */
    int wait_exit_thr_num;              /* 要销毁的线程个数 */

    int queue_front;                    /* task_queue队头下标 */
    int queue_rear;                     /* task_queue队尾下标 */
    int queue_size;                     /* task_queue队中实际任务数 */
    int queue_max_size;                 /* task_queue队列可容纳任务数上限 */

    int shutdown;                       /* 标志位,线程池是否销毁,true或false */
};

void *threadpool_thread(void *threadpool);

void *adjust_thread(void *threadpool);

int is_thread_alive(pthread_t tid);
int threadpool_free(threadpool_t *pool);

//threadpool_create(3,100,100);  
threadpool_t *threadpool_create(int min_thr_num, int max_thr_num, int queue_max_size)
{
    int i;
    threadpool_t *pool = NULL;          /* 线程池 结构体 */

    do {
        if((pool = (threadpool_t *)malloc(sizeof(threadpool_t))) == NULL) {  
            printf("malloc threadpool fail");
            break;                                      /*跳出do while*/
        }

        pool->min_thr_num = min_thr_num;
        pool->max_thr_num = max_thr_num;
        pool->busy_thr_num = 0;
        pool->live_thr_num = min_thr_num;               /* 活着的线程数 初值=最小线程数 */
        pool->wait_exit_thr_num = 0;
        pool->queue_size = 0;                           /* 有0个产品 */
        pool->queue_max_size = queue_max_size;          /* 最大任务队列数 */
        pool->queue_front = 0;
        pool->queue_rear = 0;
        pool->shutdown = false;                         /* 不关闭线程池 */

        /* 根据最大线程上限数, 给工作线程数组开辟空间, 并清零 */
        pool->threads = (pthread_t *)malloc(sizeof(pthread_t)*max_thr_num); 
        if (pool->threads == NULL) {
            printf("malloc threads fail");
            break;
        }
        memset(pool->threads, 0, sizeof(pthread_t)*max_thr_num);

        /* 给 任务队列 开辟空间 */
        pool->task_queue = (threadpool_task_t *)malloc(sizeof(threadpool_task_t)*queue_max_size);
        if (pool->task_queue == NULL) {
            printf("malloc task_queue fail");
            break;
        }

        /* 初始化互斥琐、条件变量 */
        if (pthread_mutex_init(&(pool->lock), NULL) != 0
                || pthread_mutex_init(&(pool->thread_counter), NULL) != 0
                || pthread_cond_init(&(pool->queue_not_empty), NULL) != 0
                || pthread_cond_init(&(pool->queue_not_full), NULL) != 0)
        {
            printf("init the lock or cond fail");
            break;
        }

        /* 启动 min_thr_num 个 work thread */
        for (i = 0; i < min_thr_num; i++) {
            pthread_create(&(pool->threads[i]), NULL, threadpool_thread, (void *)pool);   /*pool指向当前线程池*/
            printf("start thread 0x%x...\n", (unsigned int)pool->threads[i]);
        }
        pthread_create(&(pool->adjust_tid), NULL, adjust_thread, (void *)pool);     /* 创建管理者线程 */

        return pool;

    } while (0);

    threadpool_free(pool);      /* 前面代码调用失败时,释放poll存储空间 */

    return NULL;
}

/* 向线程池中 添加一个任务 */
//threadpool_add(thp, process, (void*)&num[i]);   /* 向线程池中添加任务 process: 小写---->大写*/

int threadpool_add(threadpool_t *pool, void*(*function)(void *arg), void *arg)
{
    pthread_mutex_lock(&(pool->lock));

    /* ==为真,队列已经满, 调wait阻塞 */
    while ((pool->queue_size == pool->queue_max_size) && (!pool->shutdown)) {
        pthread_cond_wait(&(pool->queue_not_full), &(pool->lock));
    }

    if (pool->shutdown) {
        pthread_cond_broadcast(&(pool->queue_not_empty));
        pthread_mutex_unlock(&(pool->lock));
        return 0;
    }

    /* 清空 工作线程 调用的回调函数 的参数arg */
    if (pool->task_queue[pool->queue_rear].arg != NULL) {
        pool->task_queue[pool->queue_rear].arg = NULL;
    }

    /*添加任务到任务队列里*/
    pool->task_queue[pool->queue_rear].function = function;
    pool->task_queue[pool->queue_rear].arg = arg;
    pool->queue_rear = (pool->queue_rear + 1) % pool->queue_max_size;       /* 队尾指针移动, 模拟环形 */
    pool->queue_size++;

    /*添加完任务后,队列不为空,唤醒线程池中 等待处理任务的线程*/
    pthread_cond_signal(&(pool->queue_not_empty));
    pthread_mutex_unlock(&(pool->lock));

    return 0;
}

/* 线程池中各个工作线程 */
void *threadpool_thread(void *threadpool)
{
    threadpool_t *pool = (threadpool_t *)threadpool;
    threadpool_task_t task; // 用于拷贝得到任务队列中的任务

    while (true) {
        /* Lock must be taken to wait on conditional variable */
        /*刚创建出线程,等待任务队列里有任务,否则阻塞等待任务队列里有任务后再唤醒接收任务*/
        pthread_mutex_lock(&(pool->lock));

        /*queue_size == 0 说明没有任务,调 wait 阻塞在条件变量上, 若有任务,跳过该while*/
        while ((pool->queue_size == 0) && (!pool->shutdown)) {  
            printf("thread 0x%x is waiting\n", (unsigned int)pthread_self());
            pthread_cond_wait(&(pool->queue_not_empty), &(pool->lock));

            /*清除指定数目的空闲线程,如果要结束的线程个数大于0,结束线程*/
            if (pool->wait_exit_thr_num > 0) {
                pool->wait_exit_thr_num--;

                /*如果线程池里线程个数大于最小值时可以结束当前线程*/
                if (pool->live_thr_num > pool->min_thr_num) {
                    printf("thread 0x%x is exiting\n", (unsigned int)pthread_self());
                    pool->live_thr_num--;
                    pthread_mutex_unlock(&(pool->lock));

                    pthread_exit(NULL);
                }
            }
        }

        /*如果指定了true,要关闭线程池里的每个线程,自行退出处理---销毁线程池*/
        if (pool->shutdown) {
            pthread_mutex_unlock(&(pool->lock));
            printf("thread 0x%x is exiting\n", (unsigned int)pthread_self());
            pthread_detach(pthread_self());
            pthread_exit(NULL);     /* 线程自行结束 */
        }

        /*从任务队列里获取任务, 是一个出队操作*/
        task.function = pool->task_queue[pool->queue_front].function;
        task.arg = pool->task_queue[pool->queue_front].arg;

        pool->queue_front = (pool->queue_front + 1) % pool->queue_max_size;       /* 出队,模拟环形队列 */
        pool->queue_size--;

        /*通知可以有新的任务添加进来*/
        pthread_cond_broadcast(&(pool->queue_not_full));

        /*任务取出后,立即将 线程池琐 释放*/
        pthread_mutex_unlock(&(pool->lock));

        /*执行任务*/ 
        printf("thread 0x%x start working\n", (unsigned int)pthread_self());
        pthread_mutex_lock(&(pool->thread_counter));                            /*忙状态线程数变量琐*/
        pool->busy_thr_num++;                                                   /*忙状态线程数+1*/
        pthread_mutex_unlock(&(pool->thread_counter));

        (*(task.function))(task.arg);                                           /*执行回调函数任务*/
        //task.function(task.arg);                                              /*执行回调函数任务*/

        /*任务结束处理*/ 
        printf("thread 0x%x end working\n", (unsigned int)pthread_self());
        pthread_mutex_lock(&(pool->thread_counter));
        pool->busy_thr_num--;                                       /*处理掉一个任务,忙状态数线程数-1*/
        pthread_mutex_unlock(&(pool->thread_counter));
    }

    pthread_exit(NULL);
}

/* 管理线程 */
void *adjust_thread(void *threadpool)
{
    int i;
    threadpool_t *pool = (threadpool_t *)threadpool;
    while (!pool->shutdown) {

        sleep(DEFAULT_TIME);                                    /*定时 对线程池管理*/

        pthread_mutex_lock(&(pool->lock));
        int queue_size = pool->queue_size;                      /* 关注 任务数 */
        int live_thr_num = pool->live_thr_num;                  /* 存活 线程数 */
        pthread_mutex_unlock(&(pool->lock));

        pthread_mutex_lock(&(pool->thread_counter));
        int busy_thr_num = pool->busy_thr_num;                  /* 忙着的线程数 */
        pthread_mutex_unlock(&(pool->thread_counter));

        /* 创建新线程 算法: 任务数大于最小线程池个数, 且存活的线程数少于最大线程个数时 如:30>=10 && 40<100*/
        if (queue_size >= MIN_WAIT_TASK_NUM && live_thr_num < pool->max_thr_num) {
            pthread_mutex_lock(&(pool->lock));  
            int add = 0;

            /*一次增加 DEFAULT_THREAD 个线程*/
            for (i = 0; i < pool->max_thr_num && add < DEFAULT_THREAD_VARY
                    && pool->live_thr_num < pool->max_thr_num; i++) {
                if (pool->threads[i] == 0 || !is_thread_alive(pool->threads[i])) {
                    pthread_create(&(pool->threads[i]), NULL, threadpool_thread, (void *)pool);
                    add++;
                    pool->live_thr_num++;
                }
            }

            pthread_mutex_unlock(&(pool->lock));
        }

        /* 销毁多余的空闲线程 算法:忙线程X2 小于 存活的线程数 且 
        	存活的线程数 大于 最小线程数时*/
        if ((busy_thr_num * 2) < live_thr_num  &&  live_thr_num > pool->min_thr_num) {

            /* 一次销毁DEFAULT_THREAD个线程, 隨機10個即可 */
            pthread_mutex_lock(&(pool->lock));
            pool->wait_exit_thr_num = DEFAULT_THREAD_VARY; /* 要销毁的线程数 设置为10 */
            pthread_mutex_unlock(&(pool->lock));

            for (i = 0; i < DEFAULT_THREAD_VARY; i++) {
                /* 通知处在空闲状态的线程, 他们会自行终止*/
                pthread_cond_signal(&(pool->queue_not_empty));
            }
        }
    }

    return NULL;
}

int threadpool_destroy(threadpool_t *pool)
{
    int i;
    if (pool == NULL) {
        return -1;
    }
    pool->shutdown = true;

    /*先销毁管理线程*/
    pthread_join(pool->adjust_tid, NULL);

    for (i = 0; i < pool->live_thr_num; i++) {
        /*通知所有的空闲线程*/
        pthread_cond_broadcast(&(pool->queue_not_empty));
    }
    for (i = 0; i < pool->live_thr_num; i++) {
        pthread_join(pool->threads[i], NULL);
    }
    threadpool_free(pool);

    return 0;
}

int threadpool_free(threadpool_t *pool)
{
    if (pool == NULL) {
        return -1;
    }

    if (pool->task_queue) {
        free(pool->task_queue);
    }
    if (pool->threads) {
        free(pool->threads);
        pthread_mutex_lock(&(pool->lock));
        pthread_mutex_destroy(&(pool->lock));
        pthread_mutex_lock(&(pool->thread_counter));
        pthread_mutex_destroy(&(pool->thread_counter));
        pthread_cond_destroy(&(pool->queue_not_empty));
        pthread_cond_destroy(&(pool->queue_not_full));
    }
    free(pool);
    pool = NULL;

    return 0;
}

int threadpool_all_threadnum(threadpool_t *pool)
{
    int all_threadnum = -1;                 // 总线程数

    pthread_mutex_lock(&(pool->lock));
    all_threadnum = pool->live_thr_num;     // 存活线程数
    pthread_mutex_unlock(&(pool->lock));

    return all_threadnum;
}

int threadpool_busy_threadnum(threadpool_t *pool)
{
    int busy_threadnum = -1;                // 忙线程数

    pthread_mutex_lock(&(pool->thread_counter));
    busy_threadnum = pool->busy_thr_num;    
    pthread_mutex_unlock(&(pool->thread_counter));

    return busy_threadnum;
}

int is_thread_alive(pthread_t tid)
{
    int kill_rc = pthread_kill(tid, 0);     //发0号信号,测试线程是否存活
    if (kill_rc == ESRCH) {
        return false;
    }
    return true;
}

/*测试*/ 

#if 1

/* 线程池中的线程,模拟处理业务 */
void *process(void *arg)
{
    printf("thread 0x%x working on task %d\n ",(unsigned int)pthread_self(),(int)arg);
    sleep(1);                           //模拟 小---大写
    printf("task %d is end\n",(int)arg);

    return NULL;
}

int main(void)
{
	/*threadpool_t *threadpool_create(int min_thr_num, 
		int max_thr_num, int queue_max_size);*/
	/*创建线程池,池里最小3个线程,最大100,队列最大100*/
    threadpool_t *thp = threadpool_create(3,100,100);   
    printf("pool inited");

    //int *num = (int *)malloc(sizeof(int)*20);
    int num[20], i;
    for (i = 0; i < 20; i++) {
        num[i] = i;
        printf("add task %d\n",i);
        
   	/*int threadpool_add(threadpool_t *pool, 
   		void*(*function)(void *arg), void *arg) */
        threadpool_add(thp, process, (void*)&num[i]);   /* 向线程池中添加任务 */
    }

    sleep(10);                                          /* 等子线程完成任务 */
    threadpool_destroy(thp);

    return 0;
}

#endif

threadpoll.h

#ifndef __THREADPOOL_H_
#define __THREADPOOL_H_

typedef struct threadpool_t threadpool_t;

/**
 * @function threadpool_create
 * @descCreates a threadpool_t object.
 * @param thr_num  thread num
 * @param max_thr_num  max thread size
 * @param queue_max_size   size of the queue.
 * @return a newly created thread pool or NULL
 */
threadpool_t *threadpool_create(int min_thr_num, int max_thr_num, int queue_max_size);

/**
 * @function threadpool_add
 * @desc add a new task in the queue of a thread pool
 * @param pool     Thread pool to which add the task.
 * @param function Pointer to the function that will perform the task.
 * @param argument Argument to be passed to the function.
 * @return 0 if all goes well,else -1
 */
int threadpool_add(threadpool_t *pool, void*(*function)(void *arg), void *arg);

/**
 * @function threadpool_destroy
 * @desc Stops and destroys a thread pool.
 * @param pool  Thread pool to destroy.
 * @return 0 if destory success else -1
 */
int threadpool_destroy(threadpool_t *pool);

/**
 * @desc get the thread num
 * @pool pool threadpool
 * @return # of the thread
 */
int threadpool_all_threadnum(threadpool_t *pool);

/**
 * desc get the busy thread num
 * @param pool threadpool
 * return # of the busy thread
 */
int threadpool_busy_threadnum(threadpool_t *pool);

#endif

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值