网络编程定时器一:使用升序链表
https://blog.csdn.net/freeelinux/article/category/6338658
网络编程中应用层的定时器是很有必要的,这可以让服务端主动关闭时间很久的非活跃连接。
另外一种解决方案是TCP的keepalive (它的原理应该知道:如果客端连接上,但什么都不做,keepalive是毫无办法的,它只能一定时间后不断的向客户端发送心跳包。),但它只能检测真正的死连接,即客端主机断电,或者网线被拔掉这种情况。(上面的情况他无法解决即 非活跃的连接)
#ifndef LST_TIMER #define LST_TIMER #include <time.h> #define BUFFER_SIZE 64 class util_timer;//前项申明 struct client_data { sockaddr_in address; int sockfd; char buf[ BUFFER_SIZE ];//读取缓存 util_timer* timer;//定时器 }; class util_timer//定时器类 是用来初始化每个节点的这个很重要 {//里面包含成员client_data的指针 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 ) { return; } if( !head ) { head = tail = timer; return; } 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 ); } } 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; } void tick() {//SIGALRM信号每次被触发就在其信号处理函数(如果使用同一事件源,则是主函数) //中执行一次tick函数,以处理链表上到期的任务 //下文会给出使用SIGALRM测试的代码,当然也可以使用其他方式 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: void add_timer( util_timer* timer, util_timer* lst_head ) { 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 ) { prev->next = timer; timer->prev = prev; timer->next = NULL; tail = timer; } } private: util_timer* head; util_timer* tail; }; #endif
#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; 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 )//添加fd到红黑树 读、ET、非阻塞 { 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; } void addsig( int sig ) { struct sigaction sa; memset( &sa, '\0', sizeof( sa ) ); sa.sa_handler = sig_handler;//信号调用函数 //该标志位重启被该信号中断的当前系统调用 就是让系统调用函数恢复运行 //有些信号会中断当前的系统调用,如connect epoll_wait sa.sa_flags |= SA_RESTART; sigfillset( &sa.sa_mask );//屏蔽其他信号 assert( sigaction( sig, &sa, NULL ) != -1 ); } void timer_handler() { //定时处理任务,实际上就是调用tick函数 timer_lst.tick(); //因为一次alarm调用只会引起一次 SIGALRM 信号,所以要重新定时,以不断触发SIGALRM信号 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 ); //socketpair创建的管道和pip管道不同就是 他创建的管道两端都可读写 ///使用socketpair,和pipe的区别是pipe是半双工,这个是全双工 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; //用户数据的 指针 指向那么多的结构吧 指针加1 数组下标加1 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 ); 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;//address users[connfd].sockfd = connfd;//sockfd //定时器类 是用来初始化升序链表节点的这个很重要//里面包含成员client_data的指针等 //创建定时器,设置其回调函数与超时时间,然后绑定定时器与用户数据,最后再将定时器添加到链表timer_list中 util_timer* timer = new util_timer; timer->user_data = &users[connfd];//client_data的指针回填到链表节点上 timer->cb_func = cb_func;//函数指针回填 入参就是节点自己的user_data成员 time_t cur = time( NULL ); timer->expire = cur + 3 * TIMESLOT;//给这个已经连接的套接字定时 //前面创建定时、回掉函数、绑定socket信息、都是为下面一行服务的理解就好 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 ) {//还是那样继续干 有情况 不可能没有数据信息读取到因为是ET(个人理解)而且触发的是pipefd[0] continue; } else { for( int i = 0; i < ret; ++i ) {//这个貌似有点蠢轮循一个那么大的信号数组 switch( signals[i] ) { case SIGALRM: { //用timeout变量标记有定时任务需要处理, //但不立即处理定时任务,这是因为定时任务的优先级不是很高, //我们优先处理其他更重要的任务 timeout = true; break;//这个break仅仅是break switch } 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|EWOULDBOCK就要小心咯 { //该回掉函数就是处理这个socket如close()、从红黑树上拿掉关注的事件 cb_func( &users[sockfd] ); if( timer )//看这个有可读事件的socket有没有定时器在升序链表中 { timer_lst.del_timer( timer ); } }// } else if( ret == 0 ) {//同上 :如果对方已关闭连接,则我们也关闭连接,并移除对应的定时器 cb_func( &users[sockfd] ); if( timer ) { timer_lst.del_timer( timer ); } } else { //将error等于EAGAIN|EWOULDBOCK的情况放到这里来就是说还有数据需要处理 //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 } } //处理完上面的事情后,最后处理定时事件,因为I/O事件有更高的优先级。 //当然,这样做将导致定时任务不能精确的按照预期的时间执行 if( timeout ) { timer_handler(); timeout = false; } } close( listenfd ); close( pipefd[1] ); close( pipefd[0] ); delete [] users; return 0; }
测试 ok 怎么测试自己想 功能良好的实现了 但是 有bug个人测试 就是因为ET模式的问题