刚好前两天学长给我们讲座中提到服务器客户的超时处理,刚好又在看《高性能linux服务器编程》有发现这方面的知识,就拿出来总结一下。
第一,我们为什么需要自制计时器,c++ /c 的alarm不是可以实现定时操作吗,还有sleep…???
linux下一个进程共享一个alarm闹钟定时器,然而服务器肯定是多用户的,我们肯定得想方法给每个客户整一个定时器
也就是接下来要介绍的升序时间链表,除此之外还有时间轮,时间最小堆等更好的方法。
util_timer类代表着每一个节点,利用time()存储绝对时间,client_data是用户的数据
util_timer.h
//
// Created by adl on 2020/7/22.
//
#ifndef TEST2_UTIL_TIMER_H
#define TEST2_UTIL_TIMER_H
#include <time.h>
class client_data;
class util_timer{
public:
util_timer();
public:
time_t expire;
void(*cb_func)(client_data*);
client_data*user_data;
util_timer*prev;
util_timer*next;
};
#endif //TEST2_UTIL_TIMER_H
util_timer.cpp
//
// Created by adl on 2020/7/22.
//
```cpp
#include "util_timer.h"
util_timer::util_timer() : prev(nullptr), next(nullptr) {
}
这个客户类中也会有一个util_timer指针可以说是客户信息类和时间节点类相互捆绑了
client_data.h
//
// Created by adl on 2020/7/22.
//
#ifndef TEST2_CLIENT_DATA_H
#define TEST2_CLIENT_DATA_H
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <libgen.h>
#include <stdlib.h>
#include <errno.h>
#define BUFFER_SIZE 64
class util_timer;
class tw_timer;
class heap_timer;
struct client_data {
sockaddr_in address;
int sockfd;
char buf[BUFFER_SIZE];
union {
// tw_timer *timer;
util_timer *timer;
// heap_timer*timer3;
};
};
#endif //TEST2_CLIENT_DATA_H
链表类
sort_timer_lst.h
//
// Created by adl on 2020/7/22.
//
#ifndef TEST2_SORT_TIMER_LST_H
#define TEST2_SORT_TIMER_LST_H
class util_timer;
class sort_timer_lst{
public:
sort_timer_lst();
virtual ~sort_timer_lst();
void add_timer(util_timer*timer);
void adjust_timer(util_timer*timer);
void del_timer(util_timer*timer);
void tick();
private:
void add_timer(util_timer*timer,util_timer*lst_head);
util_timer*head;
util_timer*tail;
};
#endif //TEST2_SORT_TIMER_LST_H
sort_timer_lst.cpp
//
// Created by adl on 2020/7/22.
//
#include <cstdio>
#include "sort_timer_lst.h"
#include "util_timer.h"
sort_timer_lst::sort_timer_lst() : head(nullptr), tail(nullptr) {
}
sort_timer_lst::~sort_timer_lst() {
util_timer *tmp = head;
while (tmp) {
head = tmp->next;
delete tmp;
tmp = head;
}
}
void sort_timer_lst::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 sort_timer_lst::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 = nullptr;
timer->next = nullptr;
add_timer(timer, head);
} else {
//删并插入
timer->prev->next = timer->next;
timer->next->prev = timer->prev;
//插
add_timer(timer, timer->next);
}
}
void sort_timer_lst::del_timer(util_timer *timer) {
if (!timer) {
return;
}
if ((timer == head) && (timer == tail)) {
delete timer;
head = tail = nullptr;
return;
}
if (timer == head) {
head = head->next;
head->prev = nullptr;
delete timer;
return;
}
if (timer == tail) {
tail = tail->prev;
tail->next = nullptr;
delete timer;
return;
}
timer->prev->next = timer->next;
timer->next->prev = timer->prev;
delete timer;
}
/*
* 这个升序时间链表的关键就是这个tick 比较是比当前时间(使用绝对时间比较)早的所有节点并执行他们的回调函数cb_func
* 比如可以让客户超时退出*/
void sort_timer_lst::tick() {
if (!head) {
return;
}
printf("timer tick\n");
time_t cur =time(nullptr);
util_timer*tmp =head;
while (tmp){
if (cur<tmp->expire){
break;
}
tmp->cb_func(tmp->user_data);
head = tmp->next;
if (head){
head->prev = nullptr;
}
delete tmp;
tmp =head ;
}
}
/*
* 调整新位置
*/
void sort_timer_lst::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;
}
prev=tmp;
tmp=tmp->next;
}
if (!tmp){
prev->next=timer;
timer->prev=prev;
timer->next= nullptr;
tail=timer;
}
}
用升序链表实现的关闭非活跃客户连接
Handleinactiveconnections.cpp
//
// Created by adl on 2020/7/25.
//
//
// Created by adl on 2020/7/22.
//
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <libgen.h>
#include <stdlib.h>
#include <errno.h>
#include <iostream>
//#include "MyHeadFile.h"
#include "sort_timer_lst.h"
#include "util_timer.h"
#include "client_data.h"
#include <signal.h>
#define FD_LIMIT 65535
#define MAX_EVENT_NUMBER 1024
#define TIMESLOT 5
static int pipefd[2];
static sort_timer_lst timerLst;
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;
}
/*
epoll ET监听
*/
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);
}
/*
信号处理函数,通过管道发生给主函数处理(epoll_wait),统一事件源,也就是将信号的处理和普通套接字或者别的文件描述符统一用epoll_wait来同步,
*/
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);
}
/*
回调函数:关闭连接
*/
void cb_func(client_data *user_data) {
assert(user_data);
epoll_ctl(epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);
close(user_data->sockfd);
printf("close fd %d\n", user_data->sockfd);
}
/*这个函数每次epoll_wait轮完一轮就用一次,调用tick将超时的用户清理,并重新定时*/
void timer_handler() {
timerLst.tick();
// timerLst.tick();
alarm(TIMESLOT);
}
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]);
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);
int op = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &op, sizeof(op));
int 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]);
/*
前面的都不说,就是基本的建立连接,从这里开始添加SIGALRM信号
*/
addsig(SIGALRM);
addsig(SIGTERM);
bool stop_server = false;
/*动态分配用户数组*/
client_data *users = new client_data[FD_LIMIT];
bool timeout = false;
/*启动闹钟 ,5秒后会响
如果epoll_wait上没有客户发来消息或者其他事件以结束epoll_wait的阻塞,这个alarm会唤醒epoll_wait
可以试着打印下面epoll_wait的返回值,如果客户端不发数据,你可以看到-1,1交替出现的现象,而这个现象出现的原因是
第一次alarm将epoll_wait打断,并触发信号函数,epoll会接受到管道来的数据,在下一轮epoll_wait反映出来。
*/
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;
users[connfd].sockfd = connfd;
/*
连接上来的套接字绑定一个时间节点,这个时间按节点的超时时间在当前时间按+3*TIMEOUT之后*/
util_timer *timer = new util_timer;
timer->user_data = &users[connfd];
timer->cb_func = cb_func;
time_t cur = time(nullptr);
timer->expire = cur + 3 * TIMESLOT;
users[connfd].timer = timer;
timerLst.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设置成true,之后会tick检查链表上全部的超时事件
*/
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) {
timerLst.del_timer(timer);
// timeWheel.del_timer(timer);
}
}
} else if (ret == 0) {
cb_func(&users[sockfd]);
if (timer) {
// timeWheel.del_timer(timer);
timerLst.del_timer(timer);
}
} else {
if (timer) {
/*如果客户发来信息了,那么修改超时时间,并修改链表上时间节点的位置*/
time_t cur = time(nullptr);
timer->expire = cur + 3 * TIMESLOT;
// timeWheel.adjust_timer(timer,/*cur +*/ 3 * TIMESLOT);
printf("adjust timer once\n");
timerLst.adjust_timer(timer);
}
}
} else {
}
}
if (timeout) {
/*调用tick,重新设置闹钟时间*/
timer_handler();
timeout = false;
}
}
close(listenfd);
close(pipefd[1]);
close(pipefd[0]);
delete[]users;
return 0;
}
g++和用telnet模拟客户端一下可以看到:每5秒一次的tick在发送4次以后服务器关闭了连接(虽然设置了超时时间是3×5秒,但是与客户建立连接的时候都会是调用alarm中前或之后一段时间,因此会多触发一次)
而客户端发送了一条sad消息之后socket的关闭时间重置,过一段时间不发消息也被关了
时间升序链表缺点:添加时间节点的效率低,遍历查找复杂度 O(n)
但说实话,书上的这个c++代码太c了一点,之后用到了再改进吧!
定时器之时间轮,下回分解…