這篇是基於linux高性能服務器編程:
書中說:我們可以利用定時器來處理非活動連接,服務器通常要定期處理非活動連接:給客戶端發一個重連請求,或者關閉它,或者其他。linux內核中提供了對連接是否處於活動狀態的定期檢查機制,我們可以通過socket選項KEEPLIVE來激活它,不過使用這種方式將使得應用程序對連接的管理變得複雜。因此,我們可以考慮在應用層實現類似於KEEPLIVE的機制,以管理所有長時間處於非活動狀態的連接。比如,利用alarm函數週期性地觸發SIGALRM信號,該信號的信號處理函數利用管道通知主循環執行定時器鏈表上的定時任務---關閉非活動連接。
其下面code主要是這麼實現的:
- 首先創建一個管道,其用來寫入信號
- 爲每個連接的客戶端都分配一個定時器,設置超時時間(絕對時間),和超時回調函數(回調函數關閉連接),並將定時器加入到升序雙向鏈表中
- 爲了統一事件源,我們將信號管道的讀端加入到epoll中,這樣就方便監聽所有客戶連接和超時事件發生,然後進行處理
- 在主循環中,判斷是否是信號管道的讀端文件描述符,如果是超時信號,就調用超時處理函數,不過是在所有I/O事件處理完成之後
- 超時處理函數,檢測定時器升序鏈表,若有超時事件,則調用回調函數,關閉連接,刪除定時器
//定時器節點
class util_timer
{
public:
time_t expire; /* 任務的超時時間 */
void (*cb_func)(cli_data* ); /* 任務回調函數 */
util_timer* pre;
util_timer* next;
cli_data *user_data;
util_timer() : pre(NULL), next(NULL) {}
};
//用戶數據結構
struct cli_data
{
sockaddr_in addr;
int sockfd;
char buf[BUFFER_SZ];
util_timer* timer;
};
主函數:
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <libgen.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <errno.h>
#include "lst_timer.h"
#define ERRP(con, ret, ...) do \
{ \
if (con) \
{ \
perror(__VA_ARGS__); \
ret; \
} \
}while(0)
#define FD_LIMIT 65535
#define TIMESLOT 5
#define MAX_EVENT_NUMBER 1024
static const char* ip = "192.168.239.136";
static int port = 9660;
static int pipe_fd[2];
static int epfd;
static sort_timer_lst timer_lst;
//信號處理函數,只是將該信號值寫入管道寫端
void sig_handler(int sig)
{
int errno_bak = errno;
int msg = sig;
send(pipe_fd[1], (char*)&msg, 1, 0);
errno = errno_bak;
}
void add_signal(int sig)
{
struct sigaction sa;
bzero(&sa, sizeof(sa));
sa.sa_handler = sig_handler;
sa.sa_flags |= SA_RESTART;
sigfillset(&sa.sa_mask);
sigaction(sig, &sa, NULL);
}
int set_fd_nonblock(int fd)
{
int old_opt = fcntl(fd, F_GETFL);
int new_opt = old_opt | O_NONBLOCK;
fcntl(fd, F_SETFL, new_opt);
return old_opt;
}
void add_fd_to_epoll(int epfd, int fd)
{
struct epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET; //可讀事件 | 邊沿觸發
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
set_fd_nonblock(fd);
}
void cb_func(cli_data* user_data)
{
//關閉非活動連接前先將該連接從epoll監聽表中移除
epoll_ctl(epfd, EPOLL_CTL_DEL, user_data->sockfd, 0);
close(user_data->sockfd);
printf("close fd %d\n", user_data->sockfd);
}
void timer_handler()
{
timer_lst.tick();
//一次alarm,只會引起一次SIGALRM,所以要重新定時,不斷觸發SIGALRM信號
alarm(TIMESLOT);
}
int main(void)
{
int ret;
bool is_stop = false;
bool timeout = false;
cli_data* users = new cli_data[FD_LIMIT];
epoll_event events[MAX_EVENT_NUMBER] = {0};
//創建監聽套接字
int listen_fd = socket(PF_INET, SOCK_STREAM, 0);
ERRP(listen_fd < 0, return -1, "socket");
//命名套接字
struct sockaddr_in addr;
bzero(&addr, sizeof(addr));
addr.sin_family = AF_INET;
inet_pton(AF_INET, ip, &addr.sin_addr);
addr.sin_port = htons(port);
ret = bind(listen_fd, (struct sockaddr*)&addr, sizeof(struct sockaddr));
ERRP(ret < 0, goto ERR1, "bind");
//創建監聽隊列
ret = listen(listen_fd, 5);
ERRP(ret < 0, goto ERR1, "listen");
//創建epoll內核事件列表
epfd = epoll_create(5);
ERRP(epfd < 0, goto ERR1, "epoll_create");
//將描述符的可讀事件加入epoll內核時間表
add_fd_to_epoll(epfd, listen_fd);
//創建管道,在信號處理函數中通過該管道和程序主循環通信,以快速完畢信號處理事件
//PF_UNIX本地區域
socketpair(PF_UNIX, SOCK_STREAM, 0, pipe_fd);
ERRP(epfd < 0, goto ERR2, "socketpair");
set_fd_nonblock(pipe_fd[1]); //設置管道寫端爲非阻塞
add_fd_to_epoll(epfd, pipe_fd[0]); //將管道讀端加入epoll內核事件表
//註冊相關信號的處理函數
add_signal(SIGALRM);
add_signal(SIGTERM);
alarm(TIMESLOT); //鬧鐘設置
while (!is_stop)
{
int num = epoll_wait(epfd, events, MAX_EVENT_NUMBER, -1);
//系統產生SIGALRM超時信號,會中斷epoll的監聽,即errno=EINTR,num=-1,此時不break
//隨後sig_handler(int sig)得到執行,該函數向管道寫端pipe_fd[1]寫入信號值,epoll再次返回,此時num=1
if ((num < 0) && (errno != EINTR))
{
printf("epoll failer\n");
break;
}
for (int i = 0; i < num; i++)
{
int fd = events[i].data.fd;
//有新的客戶端連接
if (fd == listen_fd)
{
struct sockaddr_in cli_addr;
socklen_t len = sizeof(struct sockaddr_in);
int connfd = accept(listen_fd, (struct sockaddr* )&cli_addr, &len);
//將客戶端連接描述符加入epoll監聽表中
add_fd_to_epoll(epfd, connfd);
users[connfd].addr = cli_addr;
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; //一開始定義超時事件爲15s,後面將是5s
users[connfd].timer = timer;
timer_lst.add_timer(timer);
}
//信號處理函數往管道寫端寫入信號值
else if ((fd == pipe_fd[0]) && (events[i].events & EPOLLIN))
{
char signals[1024] = {0};
ret = recv(pipe_fd[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;
break;
case SIGTERM:
is_stop = true;
}
}
}
}
//客戶端發來數據
else if (events[i].events & EPOLLIN)
{
bzero(users[fd].buf, BUFFER_SZ);
ret = recv(fd, users[fd].buf, BUFFER_SZ - 1, 0);
printf("get %d bytes of client data %s from %d\n", ret, users[fd].buf, fd);
util_timer* timer = users[fd].timer;
if (ret < 0)
{
if (errno != EAGAIN)
{
cb_func(&users[fd]);
if (timer)
{
timer_lst.del_timer(timer);
}
}
}
else if (ret == 0)
{
cb_func(&users[fd]);
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);
}
}
}
else {
printf("noting\n");
}
}
//通過timer_handler()執行定時器的tick(),進而調用回調函數,回調函數中關閉連接
if (timeout)
{
timer_handler();
timeout = false;
}
}
close(pipe_fd[1]);
close(pipe_fd[0]);
delete[] users;
ERR2:
close(epfd);
ERR1:
close(listen_fd);
return 0;
}