定时事件

    当服务器遇见一些idle连接的时候需要及时处理它们以保证资源的充足性,需要定期检测连接是否处于活动状态。每个连接一个定期事件,事件可能非常多,因此需要将这些时间组织起来,这些事件在预期时间达到时触发某种处理机制而不影响服务器的主要逻辑。每个定时事件封装为定时器,然后将这些定时器用某种容器统一管理(添加定时器、删除定时器、激活定时机制等)。前一篇博文的《非阻塞connect》是通过在socket描述符里设置SO_SNDTIMETO选项来设置处理超时连接的情形,更为普遍的方式是通过alarm系统调用产生SIGALRM信号激活定时处理函数。

    前面提到首先要封装定时器(一段时间后触发某段代码的事件),然后再将他们用定时器容器统一管理。容器的选择具有很大的灵活性比如:

            1、基于超时时间升序的链表,将每个定时器按照超时时间升序排列起来,一旦有SIGALARM产生就检查链表中哪些定时器超时了就激活这些定时器的超时处理代码,显然插入一个定时器时间复杂度为O(n)n为容器大小。

            2、容器采用时间轮的方式,时间轮就是一个圆盘分为N份,每份就是个时间槽,相邻的两个时间槽的时间间隔相等,然后每个槽指向的是一个链表,该链表的元素是定时器,形象的说看起来像下雨天撑开的雨伞在滴水。值得注意的是每个定时器还有个特殊的数据结构那就是rotation表示时间轮旋转多少圈该定时器到期。那么现在判断定时器超时的条件是:时间轮的指针指向一个槽,该槽对应上的所有定时器接受超时检查,而检查方法就是看rotation是否为0若为0表明本轮转到此处该定时器已经超时,那么执行超时逻辑,若rotation仍大于0则减一。alarm每次发送SIGALRM信号时间轮的指针就向前走一个时间槽,然后该槽上对应的链表的所有定时器接受定时检查操作。注意这里每个槽对应的定时器链表是无序的,所以才需要rotation辅助判断超时,所有该链上所有的定时器都接受检查,不像1那样只需要检查到链表的某个位置即可终止(定时器不超时的那个终止升序的缘故)。这看起来像散列,插入操作理论上还是O(n)其中n为定时器个数,但是实际上只要运气不是那么背不会出现所有定时器都散列到同一个槽上的悲惨情形吧,总之效率上比1不会劣。

          3、时间堆,采用小根堆将定时器组织起来,每次只需要获取堆顶元素判断其是否超时(这里堆调整的依据是定时器的超时时间大小)。这样alarm每次发送SIGALARM信号,就检查小根堆的堆顶直至堆顶的定时器没有超时为止,获得的这些堆顶元素都是超时定时器它们就该去执行它们自己相应的超时逻辑了。

         4、IO复用技术,前面有博文《统一事件源》的思想是将信号和IO事件统一起来。这里可以利用IO复用技术统一处理定时事件,具体做法是:以处理客户连接socke描述符为例,三组IO复用技术都有一个超时参数timeout,在超时间内timeout内等待各描述符上是否有事件发生(这里不是超时事件,而是发送数据之类的某种事件),若timeout内没有事件发生则说明超时了执行超时逻辑。

     接下来将给出上述4个的代码实例,其顺序分别为:升序链表定时器容器,基于升序链表的处理非活动连接;时间轮定时器容器,基于时间轮处理非活动连接;时间堆定时器容器,基于时间堆处理非活动连接;IO复用定时器伪码;客户端代码。


  1. 基于升序链表的定时器容器,将个定时器按照超时时间升序排列,每次超时只需要扫描链表哪些超时的定时器去执行相应的超时逻辑:

#ifndef LST_TIMER
#define LST_TIMER
#include<netinet/in.h>
#include <time.h>

#define BUFFER_SIZE 64
class util_timer;//定时器类前向声明
struct client_data//用户数据便于快速与用户连接交互
{
    sockaddr_in address;//用户连接socket地址
    int sockfd;//连接套接字描述符
    char buf[ BUFFER_SIZE ];//数据缓冲区
    util_timer* timer;//定时器
};

class util_timer//定时器类含有超时时间、超时处理逻辑、用户数据等
{
public:
    util_timer() : prev( NULL ), next( NULL ){}

public:
   time_t expire; //超时时间
   void (*cb_func)( client_data* );//定时处理函数
   client_data* user_data;//用户数据类
   util_timer* prev;//指向前一个定时器
   util_timer* next;//指向后一个定时器
};

class sort_timer_lst//定时器容器,将各个不同的定时器按超时时间升序排列(是个双向链表)
{
public:
    sort_timer_lst() : head( NULL ), tail( NULL ) {}
    ~sort_timer_lst()//释放内存
    {
        util_timer* tmp = head;
        while( tmp )
        {
            head = tmp->next;
            delete tmp;
            tmp = head;
        }
    }
    void add_timer( util_timer* timer )//添加一个定时器
    {
        if( !timer )//timer为NULL
        {
            return;
        }
        if( !head )//若链表为空时则直接插入timer
        {
            head = tail = timer;
            return; 
        }
        if( timer->expire < head->expire )//若timer的超时时间比链表头head超时时间还小则timer作为新的head
        {
            timer->next = head;
            head->prev = timer;
            head = timer;
            return;
        }
        add_timer( timer, head );//timer比head的超时时间大则调用重载函数add_time插入
    }
    void adjust_timer( util_timer* timer )//调整定时器的超时时间,这里只允许超时时间加大,即链表后调整
    {
        if( !timer )//timer为NULL
        {
            return;
        }
        util_timer* tmp = timer->next;//临时定时器指针tmp
        if( !tmp || ( timer->expire < tmp->expire ) )//若timer已为表尾或者timer定时器加大后仍比它后面的小则无需调整
        {
            return;
        }
        if( timer == head )//timer是表头head则要创建新的表头
        {
            head = head->next;//新的表头
            head->prev = NULL;
            timer->next = NULL;
            add_timer( timer, head );//调用add_timer添加timer
        }
        else//timer在链表中某个位置且需要调整
        {
            timer->prev->next = timer->next;//重新连接timer的前后定时器指向
            timer->next->prev = timer->prev;
            add_timer( timer, timer->next );//调用add_timer添加timer
        }
    }
    void del_timer( util_timer* timer )//删除一个定时器
    {
        if( !timer )//若为NULL直接返回
        {
            return;
        }
        if( ( timer == head ) && ( timer == tail ) )//链表中只有一个定时器且为待删除的timer需要特殊处理
        {
            delete timer;
            head = NULL;
            tail = NULL;
            return;
        }
        if( timer == head )//待删除timer为表头需要新建表头head
        {
            head = head->next;
            head->prev = NULL;
            delete timer;
            return;
        }
        if( timer == tail )//待删除timer为表尾更新表尾tail
        {
            tail = tail->prev;
            tail->next = NULL;
            delete timer;
            return;
        }
        timer->prev->next = timer->next;//待删除timer在链表中间位置时只需要重新连接timer前后的定时器即可
        timer->next->prev = timer->prev;
        delete timer;
    }
    void tick()//处理超时的定时器,相当于回搏函数
    {
        if( !head )//空的定时器链表
        {
            return;
        }
        printf( "timer tick\n" );
        time_t cur = time( NULL );//获取当前时间
        util_timer* tmp = head;//临时定时器指针tmp
        while( tmp )//遍历链表有超时时间的定时器则执行超时逻辑
        {
            if( cur < tmp->expire )//链表为升序,则一旦找到没有超时的定时器则后面的都没有超时
            {
                break;
            }
            tmp->cb_func( tmp->user_data );//调用定时器的超时逻辑
            head = tmp->next;
            if( head )//处理一个超时逻辑删除一个定时器
            {
                head->prev = NULL;
            }
            delete tmp;
            tmp = head;
        }
    }

private:
    void add_timer( util_timer* timer, util_timer* lst_head )//重载add_timer添加一个定时器
    {
        util_timer* prev = lst_head;
        util_timer* tmp = prev->next;
        while( tmp )//在链表中遍历找到合适插入位置
        {
            if( timer->expire < tmp->expire )
            {
                prev->next = timer;
                timer->next = tmp;
                tmp->prev = timer;
                timer->prev = prev;
                break;
            }
            prev = tmp;
            tmp = tmp->next;
        }
        if( !tmp )//若遍历到表尾则待插入的timer为tial
        {
            prev->next = timer;
            timer->prev = prev;
            timer->next = NULL;
            tail = timer;
        }
        
    }

private:
    util_timer* head;//链表头
    util_timer* tail;//链表尾
};

#endif

基于升序链表定时器的服务端代码,alarm定时发送SIGALRM信号,然后扫描链表哪些超时的定时器去执行相应的超时逻辑

#include<sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#include "lst_timer.h"

#define FD_LIMIT 65535//最大文件描述符
#define MAX_EVENT_NUMBER 1024//最大事件数
#define TIMESLOT 5//每次定时时间,相当于回搏时间

static int pipefd[2];//管道描述符用于将信号处理为统一事件源
static sort_timer_lst timer_lst;//定时器容器链表类
static int epollfd = 0;//epoll事件表描述符

int setnonblocking( int fd )//将描述符fd设置为非阻塞
{
    int old_option = fcntl( fd, F_GETFL );
    int new_option = old_option | O_NONBLOCK;
    fcntl( fd, F_SETFL, new_option );
    return old_option;
}

void addfd( int epollfd, int fd )//添加描述符到事件表
{
    epoll_event event;
    event.data.fd = fd;//就绪事件描述符
    event.events = EPOLLIN | EPOLLET;//可读事件和ET模式(事件只触发一次)
    epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
    setnonblocking( fd );
}

void sig_handler( int sig )//信号处理函数这里将信号通过管道写端发送到主程序(统一事件源)
{
    int save_errno = errno;
    int msg = sig;
    send( pipefd[1], ( char* )&msg, 1, 0 );
    errno = save_errno;
}

void addsig( int sig )//安装信号处理函数,sig为信号
{
    struct sigaction sa;//sigaction信号结构体
    memset( &sa, '\0', sizeof( sa ) );
    sa.sa_handler = sig_handler;
    sa.sa_flags |= SA_RESTART;//被信号中断的系统调用将自动重启
    sigfillset( &sa.sa_mask );//设置全部信号为进程信号掩码
    assert( sigaction( sig, &sa, NULL ) != -1 );//安装信号
}

void timer_handler()//超时处理逻辑
{
    timer_lst.tick();//调用回搏函数tick执行哪些超时的定时器
    alarm( TIMESLOT );//重置时钟
}

void cb_func( client_data* user_data )//超时连接处理逻辑
{
    epoll_ctl( epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0 );//从事件表中删除哪些已经超时的定时器
    assert( user_data );
    close( user_data->sockfd );//关闭连接
    printf( "close fd %d\n", user_data->sockfd );
}

int main( int argc, char* argv[] )
{
    if( argc <= 2 )
    {
        printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi( argv[2] );

    int ret = 0;
    struct sockaddr_in address;//服务器端地址
    bzero( &address, sizeof( address ) );
    address.sin_family = AF_INET;
    inet_pton( AF_INET, ip, &address.sin_addr );
    address.sin_port = htons( port );

    int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
    assert( listenfd >= 0 );

    ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
    assert( ret != -1 );

    ret = listen( listenfd, 5 );
    assert( ret != -1 );

    epoll_event events[ MAX_EVENT_NUMBER ];//用于存放就绪事件
    int epollfd = epoll_create( 5 );//创建事件表
    assert( epollfd != -1 );
    addfd( epollfd, listenfd );//监听端口添加到事件表

    ret = socketpair( PF_UNIX, SOCK_STREAM, 0, pipefd );//本地双向管道用于信号处理函数将信号回传给主程序
    assert( ret != -1 );
    setnonblocking( pipefd[1] );//设置为非阻塞
    addfd( epollfd, pipefd[0] );//添加管道读端到事件表

    // add all the interesting signals here
    addsig( SIGALRM );//添加超时信号
    addsig( SIGTERM );//添加中断信号
    bool stop_server = false;//服务器是否运行

    client_data* users = new client_data[FD_LIMIT]; //分配超大用户数据数组,以空间换取时间,用于给定一个客户连接描述符作为数据下标即可快速索引到用户数据信息
    bool timeout = false;//是否超时
    alarm( TIMESLOT );//时钟开始计时

    while( !stop_server )//服务器运行逻辑
    {
        int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );//epoll无限期等待就绪事件
        if ( ( number < 0 ) && ( errno != EINTR ) )//若非中断导致epoll_wait出错则终止
        {
            printf( "epoll failure\n" );
            break;
        }

        for ( int i = 0; i < number; i++ )//处理就绪事件
        {
            int sockfd = events[i].data.fd;//获取就绪事件的描述符
            if( sockfd == listenfd )//若就绪描述符为监听端口则表明有新的客户连接请求
            {
                struct sockaddr_in client_address;
                socklen_t client_addrlength = sizeof( client_address );
                int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );//允许客户连接
                addfd( epollfd, connfd );//注册新连接到事件表
                users[connfd].address = client_address;//初始化新连接的用户数据
                users[connfd].sockfd = connfd;
                util_timer* timer = new util_timer;
                timer->user_data = &users[connfd];
                timer->cb_func = cb_func;//定时器超时处理函数
                time_t cur = time( NULL );
                timer->expire = cur + 3 * TIMESLOT;//超时时间
                users[connfd].timer= timer;
                timer_lst.add_timer( timer );//添加新连接的定时器到定时器链表中去
            }
            else if( ( sockfd == pipefd[0] ) && ( events[i].events & EPOLLIN ) )//管道读端可读,说明有信号产生
            {
                int sig;
                char signals[1024];
                ret = recv( pipefd[0], signals, sizeof( signals ), 0 );//接收信号值
                if( ret == -1 )//接收出错
                {
                    // handle the error
                    continue;
                }
                else if( ret == 0 )//
                {
                    continue;
                }
                else
                {
                    for( int i = 0; i < ret; ++i )//每个信号值占1B,所以循环接收每个信号值
                    {
                        switch( signals[i] )
                        {
                            case SIGALRM://超时信号
                            {
                                timeout = true;//真的超时了
                                break;
                            }
                            case SIGTERM://中断信号,服务端该终止了
                            {
                                stop_server = true;
                            }
                        }
                    }
                }
            }
            else if(  events[i].events & EPOLLIN )//客户连接有数据发送到服务端,客户连接可读事件就绪
            {
                memset( users[sockfd].buf, '\0', BUFFER_SIZE );
                ret = recv( sockfd, users[sockfd].buf, BUFFER_SIZE-1, 0 );//接收客户数据
                printf( "get %d bytes of client data %s from %d\n", ret, users[sockfd].buf, sockfd );
                util_timer* timer = users[sockfd].timer;//获取客户连接的定时器
                if( ret < 0 )
                {
                    if( errno != EAGAIN )//非阻塞式EAGAIN不是网络错误
                    {
                        cb_func( &users[sockfd] );
                        if( timer )
                        {
                            timer_lst.del_timer( timer );//网络出错,客户连接需要关闭并从定时器链表中除名
                        }
                    }
                }
                else if( ret == 0 )//客户端连接已经关闭
                {
                    cb_func( &users[sockfd] );
                    if( timer )
                    {
                        timer_lst.del_timer( timer );//从定时器链表中除名
                    }
                }
                else//处理客户端发送来的数据
                {
                    //send( sockfd, users[sockfd].buf, BUFFER_SIZE-1, 0 );
                    if( timer )//该客户连接的定时并未超时则重置定时器超时时间
                    {
                        time_t cur = time( NULL );
                        timer->expire = cur + 3 * TIMESLOT;//新的超时时间
                        printf( "adjust timer once\n" );
                        timer_lst.adjust_timer( timer );//调整定时器链表(将重置了超时时间的定时器插入到链表中合适位置)
                    }
                }
            }
            else//其它事件逻辑,未定义
            {
                // others
            }
        }

        if( timeout )//真的超时了,该对那些超时连接动手了!更待何时?
        {
            timer_handler();//超时处理那些不听话的连接
            timeout = false;//既然处理了一波就等待下一波超时再收拾它们!
        }
    }

    close( listenfd );
    close( pipefd[1] );
    close( pipefd[0] );
    delete [] users;
    return 0;
}//总的逻辑就是:将信号与IO事件统一监听,一旦信号SIGALRM发生则调用超时处理逻辑



基于时间轮的定时器容器,时间轮相当于个有刻度的圆盘,每个刻度称为槽,每个槽指向一个元素为定时器链表,alarm超时发送SIGALRM信号时间轮指针向前移动一个槽,然后扫描这个槽上超时的定时器。

#ifndef TIME_WHEEL_TIMER
#define TIME_WHEEL_TIMER

#include <time.h>
#include <netinet/in.h>
#include <stdio.h>

#define BUFFER_SIZE 64//缓冲区大小
class tw_timer;//时间轮定时器
struct client_data//用户数据
{
    sockaddr_in address;//客户端地址
    int sockfd;//连接描述符
    char buf[ BUFFER_SIZE ];//缓冲区数据
    tw_timer* timer;//定时器指针
};

class tw_timer//时间轮定时器,每个定时器对应一个超时事件
{
public:
    tw_timer( int rot, int ts )
    : next( NULL ), prev( NULL ), rotation( rot ), time_slot( ts ){}

public:
    int rotation;//记录当下定时器在时间轮旋转多少圈后生效,时间轮每旋转一次该值减一直至为0时表示超时器到期
    int time_slot;//时间槽表示该定时器位于时间轮上的哪个槽
    void (*cb_func)( client_data* );//定时器回调函数
    client_data* user_data;//客户连接数据
    tw_timer* next;//指向前一个定时器(它们位于时间轮同一槽上)
    tw_timer* prev;//
};

class time_wheel//时间轮定时器容器,采用定长数组表示时间轮的槽,每个槽是个定时器为元素构成的链表,形状像个滴水的雨伞
{
public:
    time_wheel() : cur_slot( 0 )//当前槽初始化为0
    {
        for( int i = 0; i < N; ++i )
        {
            slots[i] = NULL;//时间轮上每个槽初始化为NULL
        }
    }
    ~time_wheel()//释放内存
    {
        for( int i = 0; i < N; ++i )
        {
            tw_timer* tmp = slots[i];
            while( tmp )
            {
                slots[i] = tmp->next;
                delete tmp;
                tmp = slots[i];
            }
        }
    }
    tw_timer* add_timer( int timeout )//创建一个timeout的定时器并添加到相应的槽的头部
    {
        if( timeout < 0 )
        {
            return NULL;
        }
        int ticks = 0;
        if( timeout < TI )//TI为时间轮上两个槽的间隔时间(若有N个槽则轮转一周需要N*TI时间)
        {//若timeout小于TI则插入下一个槽,否则计算需要前进多少个槽
            ticks = 1;
        }
        else
        {
            ticks = timeout / TI;//前进ticks个槽(这里也可能是1)
        }
        int rotation = ticks / N;//计算该定时器在此后时间轮旋转多少周定时器到期(这个非常重要,时间轮转到某个槽那个槽上所有定时器的rotation都需要减一,若减至0表示定时器到期了)
        int ts = ( cur_slot + ( ticks % N ) ) % N;//计算槽的位置(注意两次求余否则后果自负)
        tw_timer* timer = new tw_timer( rotation, ts );//新建定时器
        if( !slots[ts] )//若对应槽为空,则直接赋值
        {
            printf( "add timer, rotation is %d, ts is %d, cur_slot is %d\n", rotation, ts, cur_slot );
            slots[ts] = timer;
        }
        else//否则需要链接
        {
            timer->next = slots[ts];
            slots[ts]->prev = timer;
            slots[ts] = timer;
        }//直接在头部插入时间复杂度是O(1)
        return timer;
    }
    void adjust_timer(tw_timer* timer,int a){//更新当前定时器timer为新的超时时间用于该连接未超时则可以进行下次超时设定
        if(!timer||a<=0){
            return;
        }
        int ticks=0;
        if(a<TI){
            ticks=1;
        }
        else{
            ticks=a/TI;
        }
        int rotation=ticks/N;
        int ts=(cur_slot+(ticks%N))%N;
        timer->rotation=rotation;
        timer->time_slot=ts;
    }
    void del_timer( tw_timer* timer )//删除指定的定时器
    {
        if( !timer )
        {
            return;
        }
        int ts = timer->time_slot;//获取槽的位置
        if( timer == slots[ts] )//若为槽上链表的表头则需要更新表头head
        {
            slots[ts] = slots[ts]->next;
            if( slots[ts] )
            {
                slots[ts]->prev = NULL;
            }
            delete timer;
        }
        else//否则直接重新链接即可(链表删除时间复杂度O(1))
        {
            timer->prev->next = timer->next;
            if( timer->next )
            {
                timer->next->prev = timer->prev;
            }
            delete timer;
        }
    }
    void tick()//超时处理函数处理槽上所有超时的定时器,这里配合alarm时钟响应一次就执行一次tick驱动当前槽向前移动
    {
        tw_timer* tmp = slots[cur_slot];//获取时间轮上当前的槽位置上的链表头
        printf( "current slot is %d\n", cur_slot );
        while( tmp )//遍历这个槽上的所有定时器
        {
            printf( "tick the timer once\n" );
            if( tmp->rotation > 0 )//若rotation大于0表示这个定时器还没有超时(超时有两个判断条件:槽的位置和rotation)
            {
                tmp->rotation--;//时间轮没转到这个槽的时候rotation就减一一次这样超时判断条件才成立
                tmp = tmp->next;//继续遍历
            }
            else
            {
                tmp->cb_func( tmp->user_data );//否则,rotation<=0表示该槽的这个定时器确实超时了,想不枪毙都不行了!执行超时处理逻辑
                if( tmp == slots[cur_slot] )//若这个超时定时器是链表头则需要更新表头
                {
                    printf( "delete header in cur_slot\n" );
                    slots[cur_slot] = tmp->next;
                    delete tmp;//超时的定时器需要删除
                    if( slots[cur_slot] )
                    {
                        slots[cur_slot]->prev = NULL;
                    }
                    tmp = slots[cur_slot];
                }
                else//超时定时器在链表中
                {
                    tmp->prev->next = tmp->next;
                    if( tmp->next )
                    {
                        tmp->next->prev = tmp->prev;
                    }
                    tw_timer* tmp2 = tmp->next;
                    delete tmp;//删除超时定时器
                    tmp = tmp2;
                }
            }
        }
        cur_slot = ++cur_slot % N;//当前槽向前推进!这个是判断超时的第一个条件
    }

private:
    static const int N = 60;//时间轮的大小
    static const int TI = 1;//每个槽的时间片大小即该槽上停留的时间
    tw_timer* slots[N];//用数组模拟时间轮
    int cur_slot;//当前槽的位置
};//

#endif
基于时间轮的服务端程序,alarm超时发送SIGALRM信号时间轮指针向前移动一个槽,然后扫描这个槽上超时的定时器。

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#include "tw_timer.h"//采用时间轮定时器容器

#define FD_LIMIT 65535//最大文件描述符
#define MAX_EVENT_NUMBER 1024//最大事件数
#define TIMESLOT 5//每次定时时间,相当于回搏时间

static int pipefd[2];//管道描述符用于将信号处理为统一事件源
static time_wheel timer_lst;//时间轮定时器容器
static int epollfd = 0;//epoll事件表描述符

int setnonblocking( int fd )//将描述符fd设置为非阻塞
{
    int old_option = fcntl( fd, F_GETFL );
    int new_option = old_option | O_NONBLOCK;
    fcntl( fd, F_SETFL, new_option );
    return old_option;
}

void addfd( int epollfd, int fd )//添加描述符到事件表
{
    epoll_event event;
    event.data.fd = fd;//就绪事件描述符
    event.events = EPOLLIN | EPOLLET;//可读事件和ET模式(事件只触发一次)
    epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
    setnonblocking( fd );
}

void sig_handler( int sig )//信号处理函数这里将信号通过管道写端发送到主程序(统一事件源)
{
    int save_errno = errno;
    int msg = sig;
    send( pipefd[1], ( char* )&msg, 1, 0 );
    errno = save_errno;
}

void addsig( int sig )//安装信号处理函数,sig为信号
{
    struct sigaction sa;//sigaction信号结构体
    memset( &sa, '\0', sizeof( sa ) );
    sa.sa_handler = sig_handler;
    sa.sa_flags |= SA_RESTART;//被信号中断的系统调用将自动重启
    sigfillset( &sa.sa_mask );//设置全部信号为进程信号掩码
    assert( sigaction( sig, &sa, NULL ) != -1 );//安装信号
}

void timer_handler()//超时处理逻辑
{
    timer_lst.tick();//调用回搏函数tick执行哪些超时的定时器
    alarm( TIMESLOT );//重置时钟
}

void cb_func( client_data* user_data )//超时连接处理逻辑
{
    epoll_ctl( epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0 );//从事件表中删除哪些已经超时的定时器
    assert( user_data );
    close( user_data->sockfd );//关闭连接
    printf( "close fd %d\n", user_data->sockfd );
}

int main( int argc, char* argv[] )
{
    if( argc <= 2 )
    {
        printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi( argv[2] );

    int ret = 0;
    struct sockaddr_in address;//服务器端地址
    bzero( &address, sizeof( address ) );
    address.sin_family = AF_INET;
    inet_pton( AF_INET, ip, &address.sin_addr );
    address.sin_port = htons( port );

    int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
    assert( listenfd >= 0 );

    ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
    assert( ret != -1 );

    ret = listen( listenfd, 5 );
    assert( ret != -1 );

    epoll_event events[ MAX_EVENT_NUMBER ];//用于存放就绪事件
    int epollfd = epoll_create( 5 );//创建事件表
    assert( epollfd != -1 );
    addfd( epollfd, listenfd );//监听端口添加到事件表

    ret = socketpair( PF_UNIX, SOCK_STREAM, 0, pipefd );//本地双向管道用于信号处理函数将信号回传给主程序
    assert( ret != -1 );
    setnonblocking( pipefd[1] );//设置为非阻塞
    addfd( epollfd, pipefd[0] );//添加管道读端到事件表

    // add all the interesting signals here
    addsig( SIGALRM );//添加超时信号
    addsig( SIGTERM );//添加中断信号
    bool stop_server = false;//服务器是否运行

    client_data* users = new client_data[FD_LIMIT]; //分配超大用户数据数组,以空间换取时间,用于给定一个客户连接描述符作为数据下标即可快速索引到用户数据信息
    bool timeout = false;//是否超时
    alarm( TIMESLOT );//时钟开始计时

    while( !stop_server )//服务器运行逻辑
    {
        int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );//epoll无限期等待就绪事件
        if ( ( number < 0 ) && ( errno != EINTR ) )//若非中断导致epoll_wait出错则终止
        {
            printf( "epoll failure\n" );
            break;
        }

        for ( int i = 0; i < number; i++ )//处理就绪事件
        {
            int sockfd = events[i].data.fd;//获取就绪事件的描述符
            if( sockfd == listenfd )//若就绪描述符为监听端口则表明有新的客户连接请求
            {
                struct sockaddr_in client_address;
                socklen_t client_addrlength = sizeof( client_address );
                int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );//允许客户连接
                addfd( epollfd, connfd );//注册新连接到事件表
                users[connfd].address = client_address;//初始化新连接的用户数据
                users[connfd].sockfd = connfd;
                tw_timer* timer=timer_lst.add_timer(3*TIMESLOT);//添加新的定时器到时间轮中去
                timer->user_data = &users[connfd];
                timer->cb_func = cb_func;//定时器超时处理函数
                users[connfd].timer= timer;
            }
            else if( ( sockfd == pipefd[0] ) && ( events[i].events & EPOLLIN ) )//管道读端可读,说明有信号产生
            {
                int sig;
                char signals[1024];
                ret = recv( pipefd[0], signals, sizeof( signals ), 0 );//接收信号值
                if( ret == -1 )//接收出错
                {
                    // handle the error
                    continue;
                }
                else if( ret == 0 )//
                {
                    continue;
                }
                else
                {
                    for( int i = 0; i < ret; ++i )//每个信号值占1B,所以循环接收每个信号值
                    {
                        switch( signals[i] )
                        {
                            case SIGALRM://超时信号
                            {
                                timeout = true;//真的超时了
                                break;
                            }
                            case SIGTERM://中断信号,服务端该终止了
                            {
                                stop_server = true;
                            }
                        }
                    }
                }
            }
            else if(  events[i].events & EPOLLIN )//客户连接有数据发送到服务端,客户连接可读事件就绪
            {
                memset( users[sockfd].buf, '\0', BUFFER_SIZE );
                ret = recv( sockfd, users[sockfd].buf, BUFFER_SIZE-1, 0 );//接收客户数据
                printf( "get %d bytes of client data %s from %d\n", ret, users[sockfd].buf, sockfd );
                tw_timer* timer=users[sockfd].timer;
                if( ret < 0 )
                {
                    if( errno != EAGAIN )//非阻塞式EAGAIN不是网络错误
                    {
                        cb_func( &users[sockfd] );
                        if( timer )
                        {
                            timer_lst.del_timer( timer );//网络出错,客户连接需要关闭并从定时器链表中除名
                        }
                    }
                }
                else if( ret == 0 )//客户端连接已经关闭
                {
                    cb_func( &users[sockfd] );
                    if( timer )
                    {
                        timer_lst.del_timer( timer );//从定时器链表中除名
                    }
                }
                else//处理客户端发送来的数据
                {
                    //send( sockfd, users[sockfd].buf, BUFFER_SIZE-1, 0 );
                    if( timer )//该客户连接的定时并未超时则重置定时器超时时间
                    {
                        timer_lst.adjust_timer( timer,3*TIMESLOT);//将重置了超时时间的定时器插入到时间轮中合适位置)
                    }
                }
            }
            else//其它事件逻辑,未定义
            {
                // others
            }
        }

        if( timeout )//真的超时了,该对那些超时连接动手了!更待何时?
        {
            timer_handler();//超时处理那些不听话的连接
            timeout = false;//既然处理了一波就等待下一波超时再收拾它们!
        }
    }

    close( listenfd );
    close( pipefd[1] );
    close( pipefd[0] );
    delete [] users;
    return 0;
}//总的逻辑就是:将信号与IO事件统一监听,一旦信号SIGALRM发生则调用超时处理逻辑

基于小根堆的时间堆定时器容器,按照定时器超时时间大小调整堆

#ifndef intIME_HEAP
#define intIME_HEAP

#include <iostream>
#include <netinet/in.h>
#include <time.h>
using std::exception;

#define BUFFER_SIZE 64

class heap_timer;//时间堆定时器
struct client_data//客户数据
{
    sockaddr_in address;//客户端地址
    int sockfd;//客户连接描述符
    char buf[ BUFFER_SIZE ];//缓冲区
    heap_timer* timer;//定时器
};

class heap_timer//时间堆定时器
{
public:
    heap_timer( int delay )
    {
        expire = time( NULL ) + delay;
    }

public:
   time_t expire;//超时时间
   void (*cb_func)( client_data* );//超时处理函数
   client_data* user_data;//客户数据
};

class time_heap//时间堆定时器容器
{
public:
    time_heap( int cap ) throw ( std::exception )
        : capacity( cap ), cur_size( 0 )//容量capacity
    {
	array = new heap_timer* [capacity];
	if ( ! array )
	{
            throw std::exception();
	}
        for( int i = 0; i < capacity; ++i )//初始化每个定时器为空
        {
            array[i] = NULL;
        }
    }
    time_heap( heap_timer** init_array, int size, int capacity ) throw ( std::exception )
        : cur_size( size ), capacity( capacity )//用已有数组初始化堆
    {
        if ( capacity < size )
        {
            throw std::exception();
        }
        array = new heap_timer* [capacity];//指针数组
        if ( ! array )
        {
            throw std::exception();
        }
        for( int i = 0; i < capacity; ++i )
        {
            array[i] = NULL;
        }
        if ( size != 0 )
        {
            for ( int i =  0; i < size; ++i )
            {
                array[ i ] = init_array[ i ];
            }
            for ( int i = (cur_size-1)/2; i >=0; --i )//堆向下调整,从第一个非叶结点,采用数组模拟所有是(cur_size-1)/2为第一个非叶结点
            {
                percolate_down( i );
            }
        }
    }
    ~time_heap()
    {
        for ( int i =  0; i < cur_size; ++i )//删除每个定时器释放内存
        {
            delete array[i];
        }
        delete [] array; 
    }

public:
    void add_timer( heap_timer* timer ) throw ( std::exception )//添加一个定时器
    {
        if( !timer )
        {
            return;
        }
        if( cur_size >= capacity )//若当前容量大于等于数组容量则数组扩容2倍,类似于STL的vector容器扩容策略
        {
            resize();
        }
        int hole = cur_size++;//将新插入的节点放在最后一个然后向上调整堆
        int parent = 0;
        for( ; hole > 0; hole=parent )//若父亲节点小于该节点则一路向上调整,注意判断条件是hole>0不是hole>=0
        {
            parent = (hole-1)/2;//数组模拟需要先hole-1
            if ( array[parent]->expire <= timer->expire )
            {
                break;
            }
            array[hole] = array[parent];
        }
        array[hole] = timer;//最终的插入点
    }
    void del_timer( heap_timer* timer )//删除一个指定的定时器
    {
        if( !timer )
        {
            return;
        }
        // lazy delelte
        timer->cb_func = NULL;//延缓删除策略,并不是真正的删除这样使得时间复杂度为O(1),这样会使数组臃肿
    }
    heap_timer* top() const//获取堆顶元素
    {
        if ( empty() )
        {
            return NULL;
        }
        return array[0];
    }
    void pop_timer()//删除堆顶元素
    {
        if( empty() )
        {
            return;
        }
        if( array[0] )
        {
            delete array[0];
            array[0] = array[--cur_size];//将最后一个元素置于堆顶然后向下调整
            percolate_down( 0 );
        }
    }
    void tick()//回搏函数清理超时的定时器
    {
        heap_timer* tmp = array[0];
        time_t cur = time( NULL );
        while( !empty() )//直到某个不超时定时器终止循环
        {
            if( !tmp )
            {
                break;
            }
            if( tmp->expire > cur )//直到某个新的堆顶不再小于当前时间(即没有超时)终止
            {
                break;
            }
            if( array[0]->cb_func )
            {
                array[0]->cb_func( array[0]->user_data );
            }
            pop_timer();
            tmp = array[0];
        }
    }
    bool empty() const { return cur_size == 0; }

private:
    void percolate_down( int hole )//向下调整
    {
        heap_timer* temp = array[hole];
        int child = 0;
        for ( ; ((hole*2+1) <= (cur_size-1)); hole=child )
        {
            child = hole*2+1;
            if ( (child < (cur_size-1)) && (array[child+1]->expire < array[child]->expire ) )//注意越界条件的检查
            {
                ++child;
            }
            if ( array[child]->expire < temp->expire )
            {
                array[hole] = array[child];
            }
            else
            {
                break;
            }
        }
        array[hole] = temp;
    }
    void resize() throw ( std::exception )//2倍扩容策略
    {
        heap_timer** temp = new heap_timer* [2*capacity];
        for( int i = 0; i < 2*capacity; ++i )
        {
            temp[i] = NULL;
        }
        if ( ! temp )
        {
            throw std::exception();
        }
        capacity = 2*capacity;
        for ( int i = 0; i < cur_size; ++i )//指针直接复制
        {
            temp[i] = array[i];
        }
        delete [] array;
        array = temp;
    }

private:
    heap_timer** array;//二级指针指向一个指针数组
    int capacity;//数组的容量
    int cur_size;//堆中元素的个数即实际容量
};

#endif
基于时间堆的服务端处理非活动程序,alarm发送SIGALRM信号,然后获取堆中超时的哪些堆顶然后处理它们的超时逻辑

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#include "time_heap.h"

#define FD_LIMIT 65535//最大文件描述符
#define MAX_EVENT_NUMBER 1024//最大事件数
#define TIMESLOT 5//每次定时时间,相当于回搏时间

static int pipefd[2];//管道描述符用于将信号处理为统一事件源
static time_heap timer_lst(1024);//定时器容器链表类
static int epollfd = 0;//epoll事件表描述符

int setnonblocking( int fd )//将描述符fd设置为非阻塞
{
    int old_option = fcntl( fd, F_GETFL );
    int new_option = old_option | O_NONBLOCK;
    fcntl( fd, F_SETFL, new_option );
    return old_option;
}

void addfd( int epollfd, int fd )//添加描述符到事件表
{
    epoll_event event;
    event.data.fd = fd;//就绪事件描述符
    event.events = EPOLLIN | EPOLLET;//可读事件和ET模式(事件只触发一次)
    epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
    setnonblocking( fd );
}

void sig_handler( int sig )//信号处理函数这里将信号通过管道写端发送到主程序(统一事件源)
{
    int save_errno = errno;
    int msg = sig;
    send( pipefd[1], ( char* )&msg, 1, 0 );
    errno = save_errno;
}

void addsig( int sig )//安装信号处理函数,sig为信号
{
    struct sigaction sa;//sigaction信号结构体
    memset( &sa, '\0', sizeof( sa ) );
    sa.sa_handler = sig_handler;
    sa.sa_flags |= SA_RESTART;//被信号中断的系统调用将自动重启
    sigfillset( &sa.sa_mask );//设置全部信号为进程信号掩码
    assert( sigaction( sig, &sa, NULL ) != -1 );//安装信号
}

void timer_handler()//超时处理逻辑
{
    timer_lst.tick();//调用回搏函数tick执行哪些超时的定时器
    alarm( TIMESLOT );//重置时钟
}

void cb_func( client_data* user_data )//超时连接处理逻辑
{
    epoll_ctl( epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0 );//从事件表中删除哪些已经超时的定时器
    assert( user_data );
    close( user_data->sockfd );//关闭连接
    printf( "close fd %d\n", user_data->sockfd );
}

int main( int argc, char* argv[] )
{
    if( argc <= 2 )
    {
        printf( "usage: %s ip_address port_number\n", basename( argv[0] ) );
        return 1;
    }
    const char* ip = argv[1];
    int port = atoi( argv[2] );

    int ret = 0;
    struct sockaddr_in address;//服务器端地址
    bzero( &address, sizeof( address ) );
    address.sin_family = AF_INET;
    inet_pton( AF_INET, ip, &address.sin_addr );
    address.sin_port = htons( port );

    int listenfd = socket( PF_INET, SOCK_STREAM, 0 );
    assert( listenfd >= 0 );

    ret = bind( listenfd, ( struct sockaddr* )&address, sizeof( address ) );
    assert( ret != -1 );

    ret = listen( listenfd, 5 );
    assert( ret != -1 );

    epoll_event events[ MAX_EVENT_NUMBER ];//用于存放就绪事件
    int epollfd = epoll_create( 5 );//创建事件表
    assert( epollfd != -1 );
    addfd( epollfd, listenfd );//监听端口添加到事件表

    ret = socketpair( PF_UNIX, SOCK_STREAM, 0, pipefd );//本地双向管道用于信号处理函数将信号回传给主程序
    assert( ret != -1 );
    setnonblocking( pipefd[1] );//设置为非阻塞
    addfd( epollfd, pipefd[0] );//添加管道读端到事件表

    // add all the interesting signals here
    addsig( SIGALRM );//添加超时信号
    addsig( SIGTERM );//添加中断信号
    bool stop_server = false;//服务器是否运行

    client_data* users = new client_data[FD_LIMIT]; //分配超大用户数据数组,以空间换取时间,用于给定一个客户连接描述符作为数据下标即可快速索引到用户数据信息
    bool timeout = false;//是否超时
    alarm( TIMESLOT );//时钟开始计时

    while( !stop_server )//服务器运行逻辑
    {
        int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );//epoll无限期等待就绪事件
        if ( ( number < 0 ) && ( errno != EINTR ) )//若非中断导致epoll_wait出错则终止
        {
            printf( "epoll failure\n" );
            break;
        }

        for ( int i = 0; i < number; i++ )//处理就绪事件
        {
            int sockfd = events[i].data.fd;//获取就绪事件的描述符
            if( sockfd == listenfd )//若就绪描述符为监听端口则表明有新的客户连接请求
            {
                struct sockaddr_in client_address;
                socklen_t client_addrlength = sizeof( client_address );
                int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );//允许客户连接
                addfd( epollfd, connfd );//注册新连接到事件表
                users[connfd].address = client_address;//初始化新连接的用户数据
                users[connfd].sockfd = connfd;
                heap_timer* timer = new heap_timer(0);
                timer->user_data = &users[connfd];
                timer->cb_func = cb_func;//定时器超时处理函数
                time_t cur = time( NULL );
                timer->expire = cur + 3 * TIMESLOT;//超时时间
                users[connfd].timer= timer;
                timer_lst.add_timer( timer );//添加新连接的定时器到定时器链表中去
            }
            else if( ( sockfd == pipefd[0] ) && ( events[i].events & EPOLLIN ) )//管道读端可读,说明有信号产生
            {
                int sig;
                char signals[1024];
                ret = recv( pipefd[0], signals, sizeof( signals ), 0 );//接收信号值
                if( ret == -1 )//接收出错
                {
                    // handle the error
                    continue;
                }
                else if( ret == 0 )//
                {
                    continue;
                }
                else
                {
                    for( int i = 0; i < ret; ++i )//每个信号值占1B,所以循环接收每个信号值
                    {
                        switch( signals[i] )
                        {
                            case SIGALRM://超时信号
                            {
                                timeout = true;//真的超时了
                                break;
                            }
                            case SIGTERM://中断信号,服务端该终止了
                            {
                                stop_server = true;
                            }
                        }
                    }
                }
            }
            else if(  events[i].events & EPOLLIN )//客户连接有数据发送到服务端,客户连接可读事件就绪
            {
                memset( users[sockfd].buf, '\0', BUFFER_SIZE );
                ret = recv( sockfd, users[sockfd].buf, BUFFER_SIZE-1, 0 );//接收客户数据
                printf( "get %d bytes of client data %s from %d\n", ret, users[sockfd].buf, sockfd );
                heap_timer* timer = users[sockfd].timer;//获取客户连接的定时器
                if( ret < 0 )
                {
                    if( errno != EAGAIN )//非阻塞式EAGAIN不是网络错误
                    {
                        cb_func( &users[sockfd] );
                        if( timer )
                        {
                            timer_lst.del_timer( timer );//网络出错,客户连接需要关闭并从定时器链表中除名
                        }
                    }
                }
                else if( ret == 0 )//客户端连接已经关闭
                {
                    cb_func( &users[sockfd] );
                    if( timer )
                    {
                        timer_lst.del_timer( timer );//从定时器链表中除名
                    }
                }
                else//处理客户端发送来的数据
                {
                    //send( sockfd, users[sockfd].buf, BUFFER_SIZE-1, 0 );
                    if( timer )//该客户连接的定时并未超时则重置定时器超时时间
                    {
                        time_t cur = time( NULL );
                        timer->expire = cur + 3 * TIMESLOT;//新的超时时间
                        timer_lst.add_timer( timer );//调整定时器链表(将重置了超时时间的定时器插入到链表中合适位置)
                    }
                }
            }
            else//其它事件逻辑,未定义
            {
                // others
            }
        }

        if( timeout )//真的超时了,该对那些超时连接动手了!更待何时?
        {
            timer_handler();//超时处理那些不听话的连接
            timeout = false;//既然处理了一波就等待下一波超时再收拾它们!
        }
    }

    close( listenfd );
    close( pipefd[1] );
    close( pipefd[0] );
    delete [] users;
    return 0;
}//总的逻辑就是:将信号与IO事件统一监听,一旦信号SIGALRM发生则调用超时处理逻辑


基于IO复用的超时处理逻辑

#define TIMEOUT 5000

int timeout = TIMEOUT;
time_t start = time( NULL );//IO复用前时间
time_t end = time( NULL );//IO复用后事件
while( 1 )
{
    printf( "the timeout is now %d mill-seconds\n", timeout );
    start = time( NULL );//调用epoll_wait前的系统时间
    int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, timeout );//epoll指定超时时间等待事件发生
    if( ( number < 0 ) && ( errno != EINTR ) )//若非中断且epoll_wait出错则终止
    {
        printf( "epoll failure\n" );
        break;
    }
    if( number == 0 )//在超时时间timeout内没有事件发生说明该描述符上的超时事件了
    {
        // timeout这里定义超时处理逻辑
        timeout = TIMEOUT;//复位超时时间
        continue;
    }

    end = time( NULL );//获取有事件就绪后的系统时间
    timeout -= ( end - start ) * 1000;//计算从等待事件发生到事件就绪所损耗的时间,并更新超时事件timeout
    if( timeout <= 0 )//超时时间timeout仍大于0(就绪的事件还在超时时间内即它们没有超时就发生了),否则执行超时逻辑
    {
        // timeout这里定义就绪事件的超时逻辑
        timeout = TIMEOUT;//复位超时时间
    }

    // handle connections处理连接
}//可见每次IO复用都损耗了一定时间(IO复用本身是阻塞系统调用,只不过能监听多个描述符达到高效罢了),所以需要不断更新定时参数反应剩余的超时时间

一个客户端程序:

#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
#include<errno.h>
#include<iostream>
#define BUF_SIZE 64
using namespace std;
int main(int argc,char* argv[]){
    if(argc<=4){
        cout<<"argc<=4"<<endl;
        return 1;
    }
    const char* ip=argv[1];//服务端IP
    int port=atoi(argv[2]);//服务端端口号
    int time=atoi(argv[3]);//睡眠发送时间,即睡眠多少s后发送数据,模拟非活动连接
    const char* send_char=argv[4];//发送的数据内容,用于区分不同的连接
    struct sockaddr_in address;
    bzero(&address,sizeof(address));
    address.sin_family=AF_INET;
    inet_pton(AF_INET,ip,&address.sin_addr);
    address.sin_port=htons(port);
    int sockfd=socket(PF_INET,SOCK_STREAM,0);
    assert(sockfd>=0);
    if(connect(sockfd,(struct sockaddr*)&address,sizeof(address))<0){
        cout<<"connect error:"<<strerror(errno)<<endl;
        return 1;
    }
    else{
        while(1){
            sleep(time);//睡眠时间的设定区分活动连接和非活动连接
            int ret=send(sockfd,send_char,sizeof(send_char),0);
            if(ret<0){
                cout<<"send error"<<endl;
                break;
            }
        }
    }
    close(sockfd);
    return 0;
}






  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值