双向链表实现定时器
#ifndef LST_TIMER
#define LST_TIMER#include <stdio.h>
#include <time.h>
#include <arpa/inet.h>#define BUFFER_SIZE 64
class util_timer; // 前向声明// 用户数据结构
struct client_data
{sockaddr_in address; // 客户端socket地址int sockfd; // socket文件描述符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;}}// 将目标定时器timer添加到链表中void add_timer( util_timer* timer ) {if( !timer ) {return;}if( !head ) {head = tail = timer;return; }/* 如果目标定时器的超时时间小于当前链表中所有定时器的超时时间,则把该定时器插入链表头部,作为链表新的头节点,否则就需要调用重载函数 add_timer(),把它插入链表中合适的位置,以保证链表的升序特性 */if( timer->expire < head->expire ) {timer->next = head;head->prev = timer;head = timer;return;}add_timer(timer, head);}/* 当某个定时任务发生变化时,调整对应的定时器在链表中的位置。这个函数只考虑被调整的定时器的超时时间延长的情况,即该定时器需要往链表的尾部移动。*/void adjust_timer(util_timer* timer){if( !timer ) {return;}util_timer* tmp = timer->next;// 如果被调整的目标定时器处在链表的尾部,或者该定时器新的超时时间值仍然小于其下一个定时器的超时时间则不用调整if( !tmp || ( timer->expire < tmp->expire ) ) {return;}// 如果目标定时器是链表的头节点,则将该定时器从链表中取出并重新插入链表if( timer == head ) {head = head->next;head->prev = NULL;timer->next = NULL;add_timer( timer, head );} else {// 如果目标定时器不是链表的头节点,则将该定时器从链表中取出,然后插入其原来所在位置后的部分链表中timer->prev->next = timer->next;timer->next->prev = timer->prev;add_timer( timer, timer->next );}}// 将目标定时器 timer 从链表中删除void del_timer( util_timer* timer ){if( !timer ) {return;}// 下面这个条件成立表示链表中只有一个定时器,即目标定时器if( ( timer == head ) && ( timer == tail ) ) {delete timer;head = NULL;tail = NULL;return;}/* 如果链表中至少有两个定时器,且目标定时器是链表的头节点,则将链表的头节点重置为原头节点的下一个节点,然后删除目标定时器。 */if( timer == head ) {head = head->next;head->prev = NULL;delete timer;return;}/* 如果链表中至少有两个定时器,且目标定时器是链表的尾节点,则将链表的尾节点重置为原尾节点的前一个节点,然后删除目标定时器。*/if( timer == tail ) {tail = tail->prev;tail->next = NULL;delete timer;return;}// 如果目标定时器位于链表的中间,则把它前后的定时器串联起来,然后删除目标定时器timer->prev->next = timer->next;timer->next->prev = timer->prev;delete timer;}/* SIGALARM 信号每次被触发就在其信号处理函数中执行一次 tick() 函数,以处理链表上到期任务。*/void tick() {if( !head ) {return;}printf( "timer tick\n" );time_t cur = time( NULL ); // 获取当前系统时间util_timer* tmp = head;// 从头节点开始依次处理每个定时器,直到遇到一个尚未到期的定时器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:/* 一个重载的辅助函数,它被公有的 add_timer 函数和 adjust_timer 函数调用该函数表示将目标定时器 timer 添加到节点 lst_head 之后的部分链表中 */void add_timer(util_timer* timer, util_timer* lst_head) {util_timer* prev = lst_head;util_timer* tmp = prev->next;/* 遍历 list_head 节点之后的部分链表,直到找到一个超时时间大于目标定时器的超时时间节点并将目标定时器插入该节点之前 */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;}/* 如果遍历完 lst_head 节点之后的部分链表,仍未找到超时时间大于目标定时器的超时时间的节点,则将目标定时器插入链表尾部,并把它设置为链表新的尾节点。*/if( !tmp ) {prev->next = timer;timer->prev = prev;timer->next = NULL;tail = timer;}}private:util_timer* head; // 头结点util_timer* tail; // 尾结点
};#endif
/* SIGALARM 信号每次被触发就在其信号处理函数中执行一次 tick() 函数,以处理链表上到期任务。*/void tick() {if( !head ) {return;}printf( "timer tick\n" );time_t cur = time( NULL ); // 获取当前系统时间util_timer* tmp = head;// 从头节点开始依次处理每个定时器,直到遇到一个尚未到期的定时器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;}}
这段代码是用C语言编写的,其主要功能是在一个基于链表的定时器管理系统中处理到期的定时任务。这个过程是通过一个名为
tick
的函数来实现的,该函数会被定期调用,通常是响应SIGALRM信号的结果。每当这个信号发生时,就会执行tick
函数以检查并处理到期的定时任务。以下是对这段代码的详细解释:
功能描述:
tick
函数的主要目的是遍历一个定时器链表,查找并执行所有已到期的定时器任务。这些定时器是通过绝对时间(即具体的时间点,而不是经过的时间量)来设置的,这意味着每个定时器都有一个expire
属性,表示任务应当执行的时间。链表遍历:函数首先检查链表的头节点
head
是否为空,如果为空,则直接返回,因为没有定时器需要处理。然后,函数获取当前的系统时间,使用time(NULL)
来完成。之后,从链表的头节点开始遍历链表。定时器检查和处理:对于链表上的每个定时器节点,函数比较定时器的
expire
值(到期时间)和当前时间。如果当前时间已经达到或超过了定时器的到期时间,这意味着定时器已到期,需要执行相应的任务:
- 使用
tmp->cb_func(tmp->user_data)
调用定时器指定的回调函数,执行到期任务。这里,cb_func
是一个函数指针,指向定时任务的实际处理函数,而user_data
是传递给该处理函数的参数。- 任务执行完成后,将该定时器节点从链表中删除,并更新链表的头节点(如果有必要)。
- 如果链表的头节点被更新(即第一个定时器被删除),确保更新后的头节点的
prev
指针设置为NULL
,因为它现在是链表的第一个节点。链表更新和内存管理:在执行了定时器的回调函数并从链表中删除定时器节点后,需要释放该节点占用的内存资源,以避免内存泄露。使用
delete tmp;
来释放内存(假设这是在C++环境中,或应为free(tmp);
如果是纯C环境),然后将tmp
指针更新为下一个节点,继续遍历链表直到找到一个未到期的定时器或遍历完整个链表。继续处理:如果在链表中找到一个尚未到期的定时器,
tick
函数将停止处理并返回,因为链表是根据定时器的到期时间排序的。这意味着在这个未到期的定时器之后的所有定时器都还没有到期。总之,这段代码实现了一个定时器的周期性检查和处理机制,确保到期的定时任务得到及时执行。这种机制在需要定时执行任务的应用程序中非常有用,如网络服务器中处理超时事件等场景。
绝对时间是指从某一固定时刻到另一时刻的时间点,通常是相对于一个标准时间(如Unix纪元时间,即1970年1月1日00:00:00 UTC)的秒数。在C语言中,绝对时间通常通过
time_t
类型来表示,它是标准C库中用于时间表示的数据类型。当调用time(NULL)
函数时,它会返回当前时间与Unix纪元时间的秒数差,这个返回值也是time_t
类型的。在你的代码示例中,
cur
和tmp->expire
都是time_t
类型的变量,它们表示的是从1970年1月1日至今的秒数。因此,比较cur < tmp->expire
实际上是在比较两个时间点,以确定当前时间点cur
是否早于定时器到期时间点tmp->expire
。举例说明
假设当前时间是2024年3月7日12:00:00 UTC,那么
time(NULL)
调用会返回一个time_t
值,假设它是1715596800
(这个值是示意的,实际值会根据实际时间有所不同)。如果有一个定时器,其到期时间
tmp->expire
被设置为2024年3月7日12:05:00 UTC,这意味着它的expire
值可能是1715597100
(假设每过一秒,time_t
的值就增加1)。当执行比较
cur < tmp->expire
时,如果当前时间正是12:00:00,那么cur
的值为1715596800
,而tmp->expire
的值为1715597100
。因为1715596800 < 1715597100
,这说明当前时间早于定时器的到期时间,所以这个定时器还没有到期。如果检查发生在12:05:00或之后,
cur
的值将会等于或大于1715597100
,此时cur < tmp->expire
的结果为假(false),表示定时器已经到期或者过期,需要执行相应的回调函数。通过这种方式,
tick
函数能够检测并处理所有已到期的定时任务。
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 ) {struct sigaction sa;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 ); }
这两段代码是用于在Linux环境下处理信号的一种常见模式,尤其是在编写多线程或基于事件循环的网络服务程序时。这种模式通过将信号传递到主事件循环中处理,来避免信号处理函数中的许多限制和潜在的不安全操作。下面分别解释这两段代码的功能和工作原理。
sig_handler
函数
sig_handler
是一个信号处理函数,它被设计为在接收到特定信号时执行。这个函数的工作原理很简单:
保存和恢复
errno
:首先保存当前的errno
值,并在函数最后恢复它。这是因为信号处理函数可能会在程序的任意点被中断和执行,保存和恢复errno
可以防止信号处理对程序其他部分的干扰。发送信号值:函数通过
send
系统调用,将接收到的信号编号sig
发送到一个管道(pipe)的写端pipefd[1]
。这里pipefd
是一个文件描述符数组,通过某种方式(通常在程序的其他部分)创建并与某个事件循环或消息队列关联。这样做的目的是将信号事件“转发”到程序的主循环中处理,而不是在信号处理函数中直接进行复杂操作。这行代码是一个
send
系统调用,用于将数据发送到一个已经创建好的管道(pipe)的写端。下面是对这行代码的详细解释:
send
:send
是一个系统调用,通常用于在网络编程中发送数据。在这个上下文中,它被用于发送数据到管道中。
pipefd[1]
:pipefd
是一个数组,用于存储管道的文件描述符。pipefd[0]
表示管道的读端,而pipefd[1]
表示管道的写端。这里,pipefd[1]
用于将数据发送到管道的写端。
(char*)&msg
: 这是要发送的数据的地址。在这里,msg
是一个整数,被强制转换成char*
类型的指针,以便将其作为字节序列发送。由于send
函数需要一个指向发送数据的缓冲区的指针,因此需要使用类型转换将msg
的地址转换成char*
类型。
1
: 这是要发送的数据的大小,以字节数表示。在这里,我们只发送一个字节的数据,因此大小为1。
0
: 这是发送操作的附加选项。在这里,传递了0,表示不使用任何特定的选项。
addsig
函数
addsig
函数用于设置信号sig
的处理方式。其步骤如下:
初始化
sigaction
结构:通过memset
将sigaction
结构体的内存清零,并设置信号处理函数为sig_handler
。设置
sa_flags
:通过sa.sa_flags |= SA_RESTART;
设置sigaction
的标志,SA_RESTART
使得因信号中断的系统调用能够自动重启,这减少了信号处理对正常系统调用的干扰。设置信号掩码:通过
sigfillset(&sa.sa_mask);
将所有信号添加到sa_mask
中,这意味着在执行信号处理函数sig_handler
期间,几乎所有的信号都将被阻塞,以防止信号处理函数被其他信号中断,这提高了代码的可靠性。应用信号处理设置:最后,使用
sigaction
系统调用将sa
结构体中的设置应用于信号sig
。如果调用失败,则断言以提示错误。总结
通过
sig_handler
和addsig
函数的配合使用,程序可以把信号异步地转发到主事件循环中处理,这是处理信号的一种更安全和更灵活的方法。在网络编程和多线程程序中,这种方法可以有效避免信号处理中的阻塞调用和不安全操作,同时简化信号与程序逻辑的集成。在Unix和类Unix操作系统中,当信号处理函数被执行时,可能会中断正常的系统调用。如果系统调用被信号打断,那么默认行为通常是让系统调用失败并返回-1,并且设置
errno
为EINTR
(表示调用被中断)。对于某些应用程序来说,这种行为可能会引起问题,因为需要额外的逻辑来处理或重启被中断的系统调用。为了解决这个问题,POSIX标准引入了
SA_RESTART
标志。当为信号处理器设置了这个标志,大多数被该信号中断的系统调用会自动重启,而不是失败并返回EINTR
。这简化了错误处理逻辑,因为不需要显式地检查EINTR
错误并重新发起系统调用。
sa.sa_flags |= SA_RESTART;
的作用在这个语句中,
sa
是一个struct sigaction
结构体,用于指定信号的处理行为。sa_flags
是struct sigaction
的一个成员,用于设置一组标志以修改信号的行为。
sa.sa_flags |= SA_RESTART;
这行代码的作用是将SA_RESTART
标志添加到sa_flags
字段。这是通过位或操作(|=
)实现的,它确保了SA_RESTART
标志被设置,而sa_flags
中的其他已设置标志不会被影响。影响
设置了
SA_RESTART
标志后,如果信号处理期间发生了如read()
,write()
,select()
,poll()
等系统调用的中断,这些调用会自动重启而不是失败。这意味着程序不需要编写额外的代码来处理这种情况,从而简化了程序逻辑。不适用情况
虽然
SA_RESTART
对许多系统调用有效,但并非所有因信号中断的系统调用都会自动重启。有一些特定的系统调用,如wait()
,waitpid()
等,即使设置了SA_RESTART
也不会自动重启。此外,某些系统调用可能有自己特定的行为或需要特定的错误处理逻辑来处理被中断的情况。总之,
sa.sa_flags |= SA_RESTART;
通过自动重启被信号处理器中断的系统调用,简化了程序对信号干扰的处理。然而,开发者仍需了解哪些系统调用会受此标志影响,以及在何种情况下需要采取特殊处理。
void timer_handler() {// 定时处理任务,实际上就是调用tick()函数timer_lst.tick();// 因为一次 alarm 调用只会引起一次SIGALARM 信号,所以我们要重新定时,以不断触发 SIGALARM信号。alarm(TIMESLOT); }
这段代码是一个定时器处理函数
timer_handler
,它主要用于处理定时任务。以下是对这段代码的详细解释:
timer_handler()
:这是一个自定义的定时器处理函数,它会被周期性地调用来处理定时任务。
timer_lst.tick()
:调用timer_lst
对象的tick()
函数,实际上是在处理定时任务。这个函数通常用于检查定时器链表中是否有定时任务到期,并执行这些到期的任务。
alarm(TIMESLOT)
:重新设置定时器,以便在一段时间后再次触发SIGALARM
信号。alarm()
函数用于设置一个定时器,当定时器计时到期时,会向进程发送SIGALARM
信号。参数TIMESLOT
是下一次定时器到期前的时间间隔,通常以秒为单位。这个操作是为了保持定时器的周期性,确保timer_handler()
函数能够被周期性地调用。通过这段代码,可以实现周期性地执行定时任务,以及在每次定时任务执行后重新设置定时器,以便下一次定时任务的到来。这种机制常见于需要周期性执行任务的系统中,比如网络服务器中的定时器管理。
// 定时器回调函数,它删除非活动连接socket上的注册事件,并关闭之。 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 ); }
这段代码定义了一个名为
cb_func
的回调函数,用于在定时器到期时执行相应的任务。以下是对这段代码的详细解释:
cb_func(client_data* user_data)
: 这是一个回调函数,接受一个指向client_data
结构体的指针作为参数,其中包含了需要处理的客户端数据。通常,回调函数会在某个事件发生时被调用,这里的cb_func
函数是在定时器到期时被调用。
epoll_ctl(epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0)
: 这行代码用于从 epoll 实例中移除对应的文件描述符,以便停止监听该描述符上的事件。epollfd
是一个 epoll 实例的文件描述符,user_data->sockfd
是要从 epoll 实例中移除的文件描述符,EPOLL_CTL_DEL
表示要移除描述符的操作。这个操作通常在关闭对应的套接字后执行,以确保不再监听已关闭的套接字上的事件。
assert(user_data)
: 这行代码使用assert
断言来确保user_data
指针不为空,以避免潜在的空指针错误。
close(user_data->sockfd)
: 这行代码用于关闭套接字文件描述符,释放资源。关闭套接字后,将不再能够在该套接字上进行读写操作。
printf("close fd %d\n", user_data->sockfd)
: 这行代码简单地打印关闭的套接字文件描述符的信息,以便进行调试或日志记录。综合起来,这个
cb_func
回调函数主要用于在定时器到期时执行一些操作,包括从 epoll 实例中移除对应的文件描述符,并关闭套接字。这种操作通常用于管理网络服务器中的连接资源,以及释放已经不再需要的资源。
// 创建管道ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd);assert( ret != -1 );setnonblocking( pipefd[1] );addfd( epollfd, pipefd[0] );
这段代码执行了以下操作:
socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd)
: 这个函数调用创建了一个 UNIX 域套接字对,该套接字对使用流式(SOCK_STREAM)套接字类型。它们是一对连接的套接字,一个用于读取(pipefd[0]
),另一个用于写入(pipefd[1]
)。它们允许通过内核的缓冲区进行进程间通信。如果成功创建套接字对,则返回0,并将描述符存储在pipefd
数组中。
assert(ret != -1)
: 这个语句使用assert
断言来确保socketpair
函数调用成功。如果socketpair
返回值为 -1(即失败),则程序会中止,并打印出错误信息。
setnonblocking(pipefd[1])
: 这个函数调用设置了pipefd[1]
的文件描述符为非阻塞模式。在非阻塞模式下,对于读取和写入操作,如果没有数据可用或无法立即执行写入操作,函数调用将立即返回而不是阻塞等待。这通常用于提高程序的并发性和响应性。
addfd(epollfd, pipefd[0])
: 这个函数调用将pipefd[0]
的文件描述符添加到指定的 epoll 实例epollfd
中进行监听。这通常用于监视文件描述符上发生的事件,比如可读事件(EPOLLIN
),以便在文件描述符上有数据可读时得到通知。综上所述,这段代码的作用是创建了一个 UNIX 域套接字对,并将其中一个套接字设置为非阻塞模式,并将另一个套接字的文件描述符添加到 epoll 实例中进行监听。这种设置通常用于在程序中实现非阻塞的进程间通信。
nonactive_conn.cpp代码
#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 5static int pipefd[2];
static sort_timer_lst timer_lst;
static int epollfd = 0;int setnonblocking( int 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;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;//首先保存当前的errno值,并在函数最后恢复它。//这是因为信号处理函数可能会在程序的任意点被中断和执行,保存和恢复errno可以防止信号处理对程序其他部分的干扰。
}void addsig( int sig )
{struct sigaction sa;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()
{// 定时处理任务,实际上就是调用tick()函数timer_lst.tick();// 因为一次 alarm 调用只会引起一次SIGALARM 信号,所以我们要重新定时,以不断触发 SIGALARM信号。alarm(TIMESLOT);
}// 定时器回调函数,它删除非活动连接socket上的注册事件,并关闭之。
void cb_func( client_data* user_data )
{epoll_ctl( epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0 );assert( user_data ); //来确保 user_data 指针不为空,以避免潜在的空指针错误。close( user_data->sockfd );printf( "close fd %d\n", user_data->sockfd );
}int main( int argc, char* argv[] ) {if( argc <= 1 ) {printf( "usage: %s port_number\n", basename( argv[0] ) );return 1;}int port = atoi( argv[1] );int ret = 0;struct sockaddr_in address;bzero( &address, sizeof( address ) );address.sin_family = AF_INET;address.sin_addr.s_addr = INADDR_ANY;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] );// 设置信号处理函数addsig( SIGALRM ); //通常用于定时器和超时处理,SIGALRM 会被用来触发定时器,当定时器到期时,操作系统会向进程发送 SIGALRM 信号addsig( SIGTERM ); //通常用于请求进程正常终止,进程应当执行清理工作并退出bool stop_server = false;client_data* users = new client_data[FD_LIMIT]; bool timeout = false;alarm(TIMESLOT); // 定时,5秒后产生SIGALARM信号while( !stop_server ){int number = epoll_wait( epollfd, events, MAX_EVENT_NUMBER, -1 );if ( ( number < 0 ) && ( errno != EINTR ) ) {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;// 创建定时器,设置其回调函数与超时时间,然后绑定定时器与用户数据,最后将定时器添加到链表timer_lst中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 ) {continue;} else if( ret == 0 ) {continue;} else {for( int i = 0; i < ret; ++i ) {switch( signals[i] ) {case SIGALRM:{// 用timeout变量标记有定时任务需要处理,但不立即处理定时任务// 这是因为定时任务的优先级不是很高,我们优先处理其他更重要的任务。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 ){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{// 如果某个客户端上有数据可读,则我们要调整该连接对应的定时器,以延迟该连接被关闭的时间。if( timer ) {time_t cur = time( NULL );timer->expire = cur + 3 * TIMESLOT;printf( "adjust timer once\n" );timer_lst.adjust_timer( timer );}}}}// 最后处理定时事件,因为I/O事件有更高的优先级。当然,这样做将导致定时任务不能精准的按照预定的时间执行。if( timeout ) {timer_handler();timeout = false;}}close( listenfd );close( pipefd[1] );close( pipefd[0] );delete [] users;return 0;
}