網路程式設計定時器一:使用升序連結串列
之前一直對定時器這塊不熟,今天來練練手。首先我們來看實現定時器的第一種方式,升序連結串列。
網路程式設計中應用層的定時器是很有必要的,這可以讓服務端主動關閉時間很久的非活躍連線。另外一種解決方案是TCP的keepalive,但它只能檢測真正的死連線,即客端主機斷電,或者網線被拔掉這種情況。如果客端連線上,但什麼都不做,keepalive是毫無辦法的,它只能一定時間後不斷的向客戶端傳送心跳包。
定時器通常至少要包含兩個成員:一個超時時間(相對時間或絕對時間)和一個任務毀掉函式。有的時候還可能包含回撥函式被執行時需要傳入的引數,以及是否重啟定時器等資訊。如果使用雙向連結串列,自然還需要指標成員。
我們將是用time函式作為定時器時間函式,它是相對時間,即從1970年某天到現在的描述,是一個數值,很容易用它和當前時間比較,後文分析。
下面實現了一個簡單的升序定時器連結串列。升序定時器連結串列將其中定時器按照超時時間做升序排序。
#ifndef LIST_TIMER_H
#define LIST_TIMER_H
#include <time.h>
#include <memory>
#include <netinet/in.h>
#include <assert.h>
static const int BUFFER_SIZE = 64;
class util_timer; //前向宣告
class client_data {
public:
sockaddr_in addr_; //客戶端地址
int sockfd_; //客戶端connfd
char buf_[BUFFER_SIZE]; //每個客戶端的緩衝區
std::shared_ptr<util_timer> timer_; //每個客戶端的定時器
};
class util_timer {
public: //default constructor
void (*timeout_callback_)(client_data* user_data); //超時回撥函式
public:
time_t expire_; //任務的超時時間,這裡使用絕對時間
client_data* user_data_; //FIXME: how to replace it as a smart pointer. //回撥函式處理的客戶資料,由電石氣的執行者傳給回撥函式
std::shared_ptr<util_timer> prev_; //指向前一個定時器
std::shared_ptr<util_timer> next_; //指向下一個定時器
};
//定時器連結串列,它是一個升序、雙向連結串列
class sort_timer_list {
public: //default constructor and destructor
void add_timer(const std::shared_ptr<util_timer>& timer);
void adjust_timer(const std::shared_ptr<util_timer>& timer);
void del_timer(const std::shared_ptr<util_timer>& timer);
void tick();
private:
void add_timer(const std::shared_ptr<util_timer>& timer,
const std::shared_ptr<util_timer>& lst_head);
private:
std::shared_ptr<util_timer> head_;
std::shared_ptr<util_timer> tail_;
};
//將目標定時器timer新增到連結串列中
void sort_timer_list::add_timer(const std::shared_ptr<util_timer>& timer)
{
assert(timer != NULL);
if(head_ == NULL){
head_ = tail_ = timer;
return;
}
//如果目標定時器的超時時間小於當前連結串列中所有定時器的超時時間,則把該定時器插入連結串列頭部,作為連結串列的頭節點。否則就需要呼叫過載函式add_timer把它插入到連結串列中合適的位置,以保證連結串列的升序特性
if(timer->expire_ < head_->expire_){
timer->next_ = head_;
head_->prev_ = timer;
head_ = timer;
}
else
add_timer(timer, head_); //invoke private add_timer
}
//一個過載的輔助函式,它被公有地add_timer函式和adjust_timer函式呼叫,該函式表示將目標定時器timer新增到節點lst_head之後的部分連結串列中
void sort_timer_list::add_timer(const std::shared_ptr<util_timer>& timer,
const std::shared_ptr<util_timer>& lst_head)
{
std::shared_ptr<util_timer> prev = lst_head;
std::shared_ptr<util_timer> tmp = prev->next_;
//遍歷lst_head節點之後的部分連結串列,直到找到一個超時時間大於目標定時器的節點,並將目標定時器插入該節點之前
while(tmp != NULL){
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 == NULL){
prev->next_ = timer;
timer->prev_ = prev;
timer->next_ = NULL;
tail_ = timer;
}
}
//當某個定時任務發生變化時,調整對應的定時器在連結串列中的位置。這個函式只考慮被調整的定時器的超時時間延長的情況,即該定時器需要往連結串列尾部方向移動
void sort_timer_list::adjust_timer(const std::shared_ptr<util_timer>& timer)
{
assert(timer != NULL);
const std::shared_ptr<util_timer>& tmp = timer->next_;
//如果被調整的目標定時器處在連結串列尾部,或者該定時器的超時值仍然小於其下一個定時器的超時值,那就不用調整啦,皆大歡喜
if(tmp == NULL || (timer->expire_ < tmp->expire_))
return ;
//如果目標定時器是連結串列的頭結點,則將該定時器從連結串列中取出並重新插入連結串列
if(timer == head_){
head_ = head_->next_;
head_->prev_ = NULL;
timer->next_ = NULL;
add_timer(timer, head_); //reinsert timer
}
else{ //in
//如果目標定時器不是連結串列的頭結點,則將該定時器從連結串列中取出,然後插入其原來所在位置之後的那一部分連結串列中
timer->prev_->next_ = timer->next_;
timer->next_->prev_ = timer->prev_;
add_timer(timer, timer->next_); //reinsert
}
}
//將目標定時器timer從連結串列中刪除
void sort_timer_list::del_timer(const std::shared_ptr<util_timer>& timer)
{
assert(timer != NULL);
//下面這個條件成立表示連結串列中只有一個定時器,即目標定時器
//由於使用shared_ptr,所以無需delete,下同
if(timer == head_ && timer == tail_){
head_ = NULL;
tail_ = NULL;
return;
}
//如果連結串列中至少有兩個定時器,且目標定時器是連結串列的頭結點,則將連結串列的頭結點重置為原頭結點的下一個節點,然後刪除目標定時器
if(timer == head_){
head_ = head_->next_;
head_->prev_ = NULL;
return;
}
//如果連結串列中至少有兩個定時器,且目標定時器是連結串列的尾節點,則將連結串列的尾節點重置為原尾節點的前一個節點,然後刪除目標定時器
if(timer == tail_){
tail_ = tail_->prev_;
tail_->next_ = NULL;
return;
}
timer->prev_->next_ = timer->next_;
timer->next_->prev_ = timer->prev_;
}
//SIGALRM訊號每次被觸發就在其訊號處理函式(如果使用同一事件源,則是主函式)中執行一次tick函式,以處理連結串列上到期的任務
//下文會給出使用SIGALRM測試的程式碼,當然也可以使用其他方式
void sort_timer_list::tick()
{
//assert(head_ != NULL);
if(head_ == NULL)
return ;
printf("timer tick\n");
time_t cur = time(NULL); //get time
std::shared_ptr<util_timer> tmp = head_;
//從頭結點開始一次處理每個定時器,直到遇到一個尚未到期的定時器,這就是定時器的核心邏輯!!!
while(tmp != NULL){
//因為每個定時器都使用絕對時間作為超時值,所以我們可以把定時器的超時值和系統當前時間,比較以判斷定時器是否到期
if(cur < tmp->expire_) //no member time out !
break;
//如果到這裡,說明到期。呼叫定時器超時回撥函式,以執行定時任務
tmp->timeout_callback_(tmp->user_data_); //callback
//執行完定時器中的定時任務之後,就將它從連結串列中刪除,並重置連結串列頭結點
head_ = tmp->next_;
if(head_ != NULL)
head_->prev_ = NULL;
tmp = head_; //go on
}
}
#endif
核心函式tick相當於一個心搏函式,它每隔一段固定的時間就執行一次,以檢測並處理到期任務。判斷定時任務到期的依據是根據定時器expire_值小於當前系統時間從執行效率上來看,新增定時器的時間複雜度是O(n),刪除定時器的時間複雜度是O(1)(因為是雙向連結串列),執行定時任務的時間複雜度是O(1)(只需執行連結串列前部幾個超時定時器即可,因為是升序,這時可知後面節點沒有超時,無需再遍歷)。
下面我們就利用alarm函式週期的觸發SIGALRM訊號。該訊號的訊號處理函式利用管道通知主迴圈執行定時器連結串列上的定時任務——關閉非活動的連結。
另一種關閉連線的情況是,發生了socket讀錯誤,我們也要刪除定時器。
下面看測試程式碼:
#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 <vector>
#include <memory>
#include "list_timer.h"
const int FD_LIMIT = 65535;
const int MAX_EVENT_NUMBER = 1024;
const int TIME_SLOT = 5;
static int pipefd[2];
static sort_timer_list timer_list; //使用升序連結串列來管理定時器
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 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); //統一訊號時間和I/O事件
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 timer_handler()
{
//定時處理任務,實際上就是呼叫tick函式
timer_list.tick();
//因為一次alarm呼叫只會引起一次SIGALRM訊號,所以我們要重新定時,以不斷觸發SIGALRM訊號
alarm(TIME_SLOT);
}
//定時器回撥函式,在socket發生讀錯誤的情況下也會執行。
//它刪除非活動連結socket上的註冊事件,並關閉之
void handle_callback(client_data* user_data)
{
epoll_ctl(epollfd, EPOLL_CTL_DEL, user_data->sockfd_, 0);
assert(user_data != NULL);
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 listenfd = socket(AF_INET, SOCK_STREAM, 0);
assert(listenfd != -1);
struct sockaddr_in address;
memset(&address, 0, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int on = 1;
int ret = setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
assert(ret != -1);
ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
if(ret == -1){
perror("what");
return -1;
}
ret = listen(listenfd, 5);
assert(ret != -1);
epoll_event events[MAX_EVENT_NUMBER];
epollfd = epoll_create(5);
assert(epollfd != -1);
addfd(listenfd);
//使用socketpair,和pipe的區別是pipe是半雙工,這個是全雙工
ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd);
assert(ret != -1);
setnonblocking(pipefd[1]);
addfd(pipefd[0]);
//設定訊號處理函式
addsig(SIGALRM);
addsig(SIGTERM);
std::vector<client_data> users(FD_LIMIT);
//定時
alarm(TIME_SLOT);
bool timeout = false;
bool stop_server = false;
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 len = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr*)&client_address, &len);
addfd(connfd);
users[connfd].addr_ = client_address;
users[connfd].sockfd_ = connfd;
//建立定時器,設定其回撥函式與超時時間,然後繫結定時器與使用者資料,最後再將定時器新增到連結串列timer_list中
std::shared_ptr<util_timer> timer(new util_timer);
timer->user_data_ = &users[connfd];
timer->timeout_callback_ = handle_callback;
time_t cur = time(NULL);
timer->expire_ = cur + 3 * TIME_SLOT;
users[connfd].timer_ = timer;
timer_list.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){
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);
std::shared_ptr<util_timer>& timer = users[sockfd].timer_;
if(ret < 0){
//如果發生讀錯誤,則關閉連線,並移出其對應的定時器
if(errno != EAGAIN){
handle_callback(&users[sockfd]);
if(timer != NULL)
timer_list.del_timer(timer);
}
}
else if(ret == 0){
//如果對方已關閉連線,則我們也關閉連線,並移除對應的定時器
handle_callback(&users[sockfd]);
if(timer != NULL)
timer_list.del_timer(timer);
}
else{
//如果某個客戶連線上有資料可讀,我們要調整該連線對應的定時器,以延遲該連線被關閉的時間,也就是所謂的,增加壽命
if(timer != NULL){}
time_t cur = time(NULL);
timer->expire_ = cur + 3 * TIME_SLOT;
printf("adjust timer once\n");
timer_list.adjust_timer(timer);
}
}
else{
//do something
}
}
//處理完上面的事情後,最後處理定時事件,因為I/O事件有更高的優先順序。當然,這樣做將導致定時任務不能精確的按照預期的時間執行
if(timeout){
timer_handler();
timeout = false;
}
}
close(listenfd);
close(pipefd[1]);
close(pipefd[0]);
close(epollfd);
return 0;
}
上述程式碼已經通過測試,接下來我繼續學習time wheel定時器。
相關文章
- 網路程式設計定時器二:使用時間輪程式設計定時器
- 【程式碼隨想錄】二、連結串列:2、設計連結串列
- 網路程式設計定時器三:使用最小堆程式設計定時器
- leecode.23. 合併K個升序連結串列
- 有a,b兩個已按學號升序排序的連結串列,每個連結串列中的結點包括學號、成績。要求把兩個連結串列合併,仍按學號升序排列。...排序
- Rust 程式設計,用連結串列實現棧Rust程式設計
- 單向連結串列介面設計
- 707_設計連結串列
- 程式碼隨想錄第3天 | 連結串列 203.移除連結串列元素,707.設計連結串列,206.反轉連結串列
- **203.移除連結串列元素****707.設計連結串列****206.反轉連結串列**
- JavaScript資料結構之連結串列--設計JavaScript資料結構
- 連結串列-雙向連結串列
- 連結串列-迴圈連結串列
- 淺談歸併排序:合併 K 個升序連結串列的歸併解法排序
- C語言/C++程式設計學習—資料結構—連結串列類的宣告及定義C語言C++程式設計資料結構
- 【C++ 資料結構:連結串列】二刷LeetCode707設計連結串列C++資料結構LeetCode
- 連結串列面試題(二)---連結串列逆序(連結串列反轉)面試題
- 【程式碼隨想錄】二、連結串列:1、移除連結串列元素
- 連結串列4: 迴圈連結串列
- 連結串列-單連結串列實現
- C++中的連結串列類的設計C++
- 設計單向迴圈連結串列的介面
- 雙向連結串列介面設計(C語言)C語言
- [程式設計題]從尾到頭列印連結串列 牛客網練習 java遞迴程式設計Java遞迴
- 設定Kali Linux虛擬機器連線網路Linux虛擬機
- 設定virtualBox讓虛擬機器連線網路虛擬機
- 虛擬機器NAT模式網路連線設定ssh虛擬機模式
- 連結串列操作源程式 (轉)
- 程式碼隨想錄演算法訓練營第三天|203(移除連結串列元素),707(設計連結串列),206(反轉連結串列)演算法
- 隨想錄day3:203.移除連結串列元素|707.設計連結串列 |206.反轉連結串列
- 資料結構實驗之連結串列一:順序建立連結串列資料結構
- 定義一個函式,輸入一個連結串列的頭節點,反轉該連結串列並輸出反轉後連結串列的頭節點函式
- 《Cracking the Coding Interview程式設計師面試金典》----連結串列分割View程式設計師面試
- 連結串列入門與插入連結串列
- (連結串列)連結串列的排序問題排序
- 程式設計之美(第3章 結構之法-字串及連結串列的探索)總結程式設計字串
- 程式碼隨想錄訓練營第三天 | 203.移處連結串列元素 707.設計連結串列 206.反轉連結串列
- 連結串列面試題(九)---判斷一個連結串列是否帶環面試題