高并发服务器模型
- 1.高并发服务器模型--select
- 2.高并发服务器模型--poll
- 3.epoll模型
-
- [3.1 epoll原理](#3.1 epoll原理)
- 3.2epoll反应堆
1.高并发服务器模型--select
我们知道实现服务器的高并发,可以用多线程或多进程去实现。但还可以利用多路IO技术:select来实现,它可以同时监听多个文件描述符,将监控的文件描述符交给内核去处理。
现在来看看这个函数的原型:
c
int select(int nfds,fd_set*readflds,fd_writefds,set*exceptfds,struct timeval*timeout)
nfds:最大的文件描述符+1;
readflds:读集合,是一个传入传出参数;
传入:指的是告诉内核哪些文件描述符需要监控;
传出:指的是告诉内核那些文件描述符发生了变化;
writefds:写文件描述符的集合(传入传出参数);
execptfds:异常文件描述符集合(传入传出参数);
timeout(超出时间):
NULL:表示永久阻塞,直到有事件发生。
0: 表示不阻塞,不管有无事件发生,立即返回。
>0: (1)表示阻塞,若没有超过时长就会一直阻塞。
(2)超过阻塞时间,没有事件发生,会返回。
(3)阻塞时间之内,有事件发生,返回。
过程如下图:
(1)通过操作得到监听文件描述符lfd,并且加入集合中。
(2)通过内核来监视lfd,如果lfd有行为,就说明有客户链接到来,把得到的通信文件描述符就交给内核监控。
内核发现哪个文件描述符有读行为,取出它进行通信,并把它取出集合。
下面是实现的代码:
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/fcntl.h>
// 初始化服务端的监听端口。
int initserver(int port);
int main(int argc,char *argv[])
{
if (argc != 2)
{
printf("usage: ./tcpselect port\\n"); return -1;
}
// 初始化服务端用于监听的socket。
int listensock = initserver(atoi(argv[1]));
printf("listensock=%d\\n",listensock);
if (listensock < 0)
{
printf("initserver() failed.\\n"); return -1;
}
fd_set readfdset; // 读事件的集合,包括监听socket和客户端连接上来的socket。
int maxfd; // readfdset中socket的最大值。
// 初始化结构体,把listensock添加到集合中。
FD_ZERO(&readfdset);
FD_SET(listensock,&readfdset);
maxfd = listensock;
while (1)
{
// 调用select函数时,会改变socket集合的内容,所以要把socket集合保存下来,传一个临时的给select。
fd_set tmpfdset = readfdset;
int infds = select(maxfd+1,&tmpfdset,NULL,NULL,NULL);
// printf("select infds=%d\\n",infds);
// 返回失败。
if (infds < 0)
{
printf("select() failed.\\n"); perror("select()"); break;
}
// 超时,在本程序中,select函数最后一个参数为空,不存在超时的情况,但以下代码还是留着。
if (infds == 0)
{
printf("select() timeout.\\n"); continue;
}
// 检查有事情发生的socket,包括监听和客户端连接的socket。
// 这里是客户端的socket事件,每次都要遍历整个集合,因为可能有多个socket有事件。
for (int eventfd=0; eventfd <= maxfd; eventfd++)
{
if (FD_ISSET(eventfd,&tmpfdset)<=0) continue;
if (eventfd==listensock)
{
// 如果发生事件的是listensock,表示有新的客户端连上来。
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientsock = accept(listensock,(struct sockaddr*)&client,&len);
if (clientsock < 0)
{
printf("accept() failed.\\n"); continue;
}
printf ("client(socket=%d) connected ok.\\n",clientsock);
// 把新的客户端socket加入集合。
FD_SET(clientsock,&readfdset);
if (maxfd < clientsock) maxfd = clientsock;
continue;
}
else
{
// 客户端有数据过来或客户端的socket连接被断开。
char buffer[1024];
memset(buffer,0,sizeof(buffer));
// 读取客户端的数据。
ssize_t isize=read(eventfd,buffer,sizeof(buffer));
// 发生了错误或socket被对方关闭。
if (isize <=0)
{
printf("client(eventfd=%d) disconnected.\\n",eventfd);
close(eventfd); // 关闭客户端的socket。
FD_CLR(eventfd,&readfdset); // 从集合中移去客户端的socket。
// 重新计算maxfd的值,注意,只有当eventfd==maxfd时才需要计算。
if (eventfd == maxfd)
{
for (int ii=maxfd;ii>0;ii--)
{
if (FD_ISSET(ii,&readfdset))
{
maxfd = ii; break;
}
}
printf("maxfd=%d\\n",maxfd);
}
continue;
}
printf("recv(eventfd=%d,size=%d):%s\\n",eventfd,isize,buffer);
// 把收到的报文发回给客户端。
write(eventfd,buffer,strlen(buffer));
}
}
}
return 0;
}
// 初始化服务端的监听端口。
int initserver(int port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if (sock < 0)
{
printf("socket() failed.\\n"); return -1;
}
// Linux如下
int opt = 1; unsigned int len = sizeof(opt);
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,len);
setsockopt(sock,SOL_SOCKET,SO_KEEPALIVE,&opt,len);
struct sockaddr_in servaddr;
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(port);
if (bind(sock,(struct sockaddr *)&servaddr,sizeof(servaddr)) < 0 )
{
printf("bind() failed.\\n"); close(sock); return -1;
}
if (listen(sock,5) != 0 )
{
printf("listen() failed.\\n"); close(sock); return -1;
}
return sock;
}
2.高并发服务器模型--poll
除了select模型,还有poll模型,poll的做法和selec几乎是一样的。
poll 和 select 的实现非常类似,本质上的区别就是存放 fd 集合的数据结构不一样。select 在一个进程内可以维持最多 1024 个连接,poll 在此基础上做了加强,可以维持任意数量的连接。但本质上都是将需要检测的集合拷贝到内核中去,内核来轮询遍历整个集合,反复从头到尾的去查询,再将发生事件的所有的文件描述符拷贝到用户区,这样就导致随着并发量的增大,效率也会随之下降。
c
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
功能:监视并等待多个文件描述符的属性变化
参数:
fds:指向一个结构体数组的第0个元素的指针,每个数组元素都是一个struct pollfd结构,用于指定测试某个给定的fd的条件
struct pollfd{
int fd; //文件描述符
short events; //等待的事件
short revents; //实际发生的事件
};
fds结构体参数说明:
fd:每一个 pollfd 结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示 poll() 监视多个文件描述符。
events:指定监测fd的事件(输入、输出、错误),每一个事件有多个取值,如下:
revents:revents 域是文件描述符的操作结果事件,内核在调用返回时设置这个域。events 域中请求的任何事件都可能在 revents 域中返回.
注意:每个结构体的 events 域是由用户来设置,告诉内核我们关注的是什么,而 revents 域是返回时内核设置的,以说明对该描述符发生了什么事件
nfds:用来指定第一个参数数组元素个数
timeout:指定等待的毫秒数,无论 I/O 是否准备好,poll() 都会返回.
返回值:
成功时,poll() 返回结构体中 revents 域不为 0 的文件描述符个数;如果在超时前没有任何事件发生,poll()返回 0;
失败时,poll() 返回 -1,并设置 errno 为下列值之一:
EBADF:一个或多个结构体中指定的文件描述符无效。
EFAULT:fds 指针指向的地址超出进程的地址空间。
EINTR:请求的事件之前产生一个信号,调用可以重新发起。
EINVAL:nfds 参数超出 PLIMIT_NOFILE 值。
ENOMEM:可用内存不足,无法完成请求。
下面是代码的实现
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <poll.h>
/* 定义服务器初始化函数 */
int server_init(char *ip, short port)
{
int ret;
int listenfd;
struct sockaddr_in srvaddr;
/* 创建套接字文件 */
listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1) {
perror("server_init->socket");
return -1;
}
printf("listenfd = %d\n", listenfd);
/* 绑定服务器的ip地址和端口号 */
memset(&srvaddr, 0, sizeof(srvaddr));
srvaddr.sin_family = AF_INET;
srvaddr.sin_port = htons(port);
if (ip == NULL)
srvaddr.sin_addr.s_addr = htonl(INADDR_ANY);
else
srvaddr.sin_addr.s_addr = inet_addr(ip);
ret = bind(listenfd, (const struct sockaddr *)&srvaddr, sizeof(srvaddr));
if (ret == -1) {
perror("server_init->bind");
return -1;
}
printf("bind success\n");
/* 启动监听 */
ret = listen(listenfd, 1024);
if (ret == -1) {
perror("server_init->listen");
return -1;
}
return listenfd;
}
/* 定义服务器等待客户端的连接请求,建立连接 */
int server_wait_client_connect(int listenfd)
{
int connfd;
socklen_t addrlen;
struct sockaddr_in cltaddr;
//accept(listenfd, NULL, NULL);
addrlen = sizeof(cltaddr);
connfd = accept(listenfd, (struct sockaddr *)&cltaddr, &addrlen);
if (connfd == -1) {
perror("accept");
return -1;
}
printf("IP : %s connet success connfd = %d\n", inet_ntoa(cltaddr.sin_addr), connfd);
return connfd;
}
int main()
{
int ret;
int listenfd;
int connfd;
char buf[256];
int nfds;
int i;
int j;
int fd;
struct pollfd fds[128];
/* 1. 服务器的初始化 */
listenfd = server_init("127.0.0.1", 8888);
//listenfd = server_init(NULL, 8888);
if (listenfd == -1)
exit(EXIT_FAILURE);
printf("server init success\n");
/* 创建集合,清空集合 */
for (i = 0; i < 128; i++)
fds[i].fd = -1;
/* 将文件描述符listenfd及其对应事件添加到集合fds中 */
fds[0].fd = listenfd;
fds[0].events = POLLIN;
nfds = 1;
while(1) {
/* c, 调用poll函数,检测是否有准备就绪的事件,如果没有事件准备就绪,函数一直阻塞。如果有事件准备就绪函数返回;*/
ret = poll(fds, nfds, 5000);
if (ret == -1) {
perror("poll");
return -1;
} else if (ret == 0) {
printf("timeout ......\n");
continue;
}
for (i = 0; i < nfds; i++) {
/* 读资源准备就绪,进行读IO操作 */
if (fds[i].revents == POLLIN) {
fd = fds[i].fd;
if (fd == listenfd) { /* 监听套接字 */
/* 2. 服务器等待客户端的连接请求,建立连接 */
connfd = server_wait_client_connect(listenfd);
if (connfd == -1)
exit(EXIT_FAILURE);
/* 连接成功,将文件描述符connfd及其对应事件添加到集合fds中 */
fds[nfds].fd = connfd;
fds[nfds].events = POLLIN;
nfds ++;
continue;
}
/* 通信套接字 */
/* 3. 服务器处理客户端的数据请求,并处理数据,反馈处理结果 */
memset(buf, 0, sizeof(buf));
ret = read(fd, buf, sizeof(buf));
if (ret == -1) {
perror("server->read");
return -1;
} else if (ret == 0) {
/* 客户端退出的时候,需要将套接字从集合中删除 */
for (j = i; j < nfds-1; j++)
fds[j] = fds[j+1];
close(fd);
break;
}
printf("buf : %s\n", buf);
ret = write(fd, buf, sizeof(buf));
if (ret == -1) {
perror("server->write");
return -1;
}
} //if (fds[i].revents == POLLIN) end
if (fds[i].events == POLLOUT) {
}
}
}
return 0;
}
3.epoll模型
select 和 poll 方式有一个很大的问题就是,我们不难看出来 select 是通过轮训的方式来查找是否可读或者可写,打个比方,如果同时有100万个连接都没有断开,而只有一个客户端发送了数据,所以这里它还是需要循环这么多次,造成资源浪费。所以后来出现了 epoll系统调用。
3.1 epoll原理
当某一个进程去调用epoll_creat()的时候,linux的内核会创建一个eventpoll的一个结构体,这个结构题中有两个成员和epoll的使用方式相关。
c
struct eventpoll
{
//红黑树的根节点,这颗树中存储这我们添加的所有的epoll中的事件。
struct rb_root rbr;
//双向链表rblist中存储的是要通过epoll_wait()返回个用户的满足的事件。
struct list_head rbllist;
}
我们在调用epoll_creat()的时候,内核出来帮我们在epoll文件系统中创建了一个file节点,在内核中还创建了一棵红黑树用来存储加入的socket以外,还会建立一个rbllist双向链表,用来存储准备就绪的事件,当调用epoll_wait()的时候,就只需要观察这个双向链表中有无数据,如果没用就sleep阻塞等待,当阻塞时间超过timeout的时候,就直接返回。用数据的话直接把双向链表中的数据返回给用户。所以epoll非常高效。
所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,也就是说相应事件的发生时会调用这里的回调方法。这个回调方法在内核中叫做ep_poll_callback,它会把这样的事件放到上面的rdllist双向链表中。
在epoll中对于每一个事件都会建立一个epitem结构体,如下所示:
c
struct epitem {
...
//红黑树节点
struct rb_node rbn;
//双向链表节点
struct list_head rdllink;
//事件句柄等信息
struct epoll_filefd ffd;
//指向其所属的eventepoll对象
struct eventpoll *ep;
//期待的事件类型
struct epoll_event event;
...
}; // 这里包含每一个事件对应着的信息。
当调用epoll_wait检查是否有发生事件的连接时,只是检查eventpoll对象中的rdllist双向链表是否有epitem元素而已,如果rdllist链表不为空,则这里的事件复制到用户态内存(使用共享内存提高效率)中,同时将事件数量返回给用户。因此epoll_waitx效率非常高。epoll_ctl在向epoll对象中添加、修改、删除事件时,从rbr红黑树中查找事件也非常快,也就是说epoll是非常高效的,它可以轻易地处理百万级别的并发连接。
当我们要把数据拷贝到用户区的时候,这就要涉及到用户态到内核态的转化, 会造成数据的多次拷贝。但是使用了mmap技术,建立了一片共享的内存,这样就不会造成内核态到用户态的内存拷贝,减小了开销。就如下图:
下面是相关的函数
c
//创建一颗epoll树
int epoll_create(int size);
功能:调用epoll_create方法创建一个epoll的句柄,该句柄代表着一个事件表
参数:size参数现在并不起作用,它只是给内核一个提示,告诉内核事件表需要多大
返回值:1. 成功:返回epoll句柄,它会占用一个fd值(使用完也需要关闭)
失败:返回-1并设置errno值
c
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event)
fd表示文件描述符
op参数则指定操作类型,操作类型有一下三种:
EPOLL_CTL_ADD //往事件表上注册fd的事件
EPOLL_CTL_MOD //修改fd上的注册事件
EPOLL_CTL_DEL //删除fd上的注册事件
event是需要传入一个结构体指针类型,表示一个事件,epoll_even结构体t定义如下:
c
struct epoll_event
{
_uint32_t events; //epoll事件
epolla_data_t data; //用户数据
}
events和poll支持的事件类型基本相同,还可以额外支持两个事件边缘触发和水平触发。epoll默认的触发方式是水平触发。
水平触发:水平触发就是只要缓冲区中有数据,就会一直触发,直到将缓冲区中的数据读完。
边沿触发:只有数据到来的时候才会触发,不管缓冲区中是否有数据。
当使用边沿触发(ET触发)的时候,我们要将监听的文件描述设置成非阻塞,因为我们每次不一定读完缓冲区的数据,而read函数是一个阻塞函数,就会一直阻塞住,导致死锁。
c
typedef union epoll_data
{
void* ptr; //指定与fd相关的用户数据
int fd; //指定事件所从属的目标文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
这里的ptr指针是可以存储用户的数据的,他是一个viod*的万能指针。
c
#include <sys/epoll.h>
int epoll_wait ( int epfd, struct epoll_event* events, int maxevents, int timeout );
epfd:epoll_create()返回的文件描述符
events:检测到事件,将所有就绪的事件从内核事件表中复制到它的第二个参数events指向的数组中。
maxevents:最多监听多少个事件。
timeout:为-1表示永久阻塞
=0 表示调用后立即返回
>0 表示在这个时间内无事件发生一直阻塞,有事件发生立即返回;超过时间也会返回。
3.2epoll反应堆
epoll反应堆利用了c++的封装的思想,封装了一个自己的结构体,每个结构体都用函数指针,通过不同的事件,来回调不同的函数。将ptr指针指向我们自己封装的结构体就可以实现自动回调函数啦。
下面是代码:
c
/*
*epoll基于非阻塞I/O事件驱动
*/
#include <stdio.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#define MAX_EVENTS 1024 //监听上限数
#define BUFLEN 4096
#define SERV_PORT 8080
void recvdata(int fd, int events, void *arg);
void senddata(int fd, int events, void *arg);
/* 描述就绪文件描述符相关信息 */
struct myevent_s {
int fd; //要监听的文件描述符
int events; //对应的监听事件
void *arg; //泛型参数
void (*call_back)(int fd, int events, void *arg); //回调函数
int status; //是否在监听:1->在红黑树上(监听), 0->不在(不监听)
char buf[BUFLEN];
int len;
long last_active; //记录每次加入红黑树 g_efd 的时间值
};
int g_efd; //全局变量, 保存epoll_create返回的文件描述符
struct myevent_s g_events[MAX_EVENTS+1]; //自定义结构体类型数组. +1-->listen fd
/*将结构体 myevent_s 成员变量 初始化*/
void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg)
{
ev->fd = fd;
ev->call_back = call_back;
ev->events = 0;
ev->arg = arg;
ev->status = 0;
memset(ev->buf, 0, sizeof(ev->buf));
ev->len = 0;
ev->last_active = time(NULL); //调用eventset函数的时间
return;
}
/* 向 epoll监听的红黑树 添加一个 文件描述符 */
//eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]);
void eventadd(int efd, int events, struct myevent_s *ev)
{
struct epoll_event epv = {0, {0}};
int op;
epv.data.ptr = ev;
epv.events = ev->events = events; //EPOLLIN 或 EPOLLOUT
if (ev->status == 0) { //已经在红黑树 g_efd 里
op = EPOLL_CTL_ADD; //将其加入红黑树 g_efd, 并将status置1
ev->status = 1;
}
if (epoll_ctl(efd, op, ev->fd, &epv) < 0) //实际添加/修改
printf("event add failed [fd=%d], events[%d]\n", ev->fd, events);
else
printf("event add OK [fd=%d], op=%d, events[%0X]\n", ev->fd, op, events);
return ;
}
/* 从epoll 监听的 红黑树中删除一个 文件描述符*/
void eventdel(int efd, struct myevent_s *ev)
{
struct epoll_event epv = {0, {0}};
if (ev->status != 1) //不在红黑树上
return ;
//epv.data.ptr = ev;
epv.data.ptr = NULL;
ev->status = 0; //修改状态
epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv); //从红黑树 efd 上将 ev->fd 摘除
return ;
}
/* 当有文件描述符就绪, epoll返回, 调用该函数 与客户端建立链接 */
void acceptconn(int lfd, int events, void *arg)
{
struct sockaddr_in cin;
socklen_t len = sizeof(cin);
int cfd, i;
if ((cfd = accept(lfd, (struct sockaddr *)&cin, &len)) == -1) {
if (errno != EAGAIN && errno != EINTR) {
/* 暂时不做出错处理 */
}
printf("%s: accept, %s\n", __func__, strerror(errno));
return ;
}
do {
for (i = 0; i < MAX_EVENTS; i++) //从全局数组g_events中找一个空闲元素
if (g_events[i].status == 0) //类似于select中找值为-1的元素
break; //跳出 for
if (i == MAX_EVENTS) {
printf("%s: max connect limit[%d]\n", __func__, MAX_EVENTS);
break; //跳出do while(0) 不执行后续代码
}
int flag = 0;
if ((flag = fcntl(cfd, F_SETFL, O_NONBLOCK)) < 0) { //将cfd也设置为非阻塞
printf("%s: fcntl nonblocking failed, %s\n", __func__, strerror(errno));
break;
}
/* 给cfd设置一个 myevent_s 结构体, 回调函数 设置为 recvdata */
eventset(&g_events[i], cfd, recvdata, &g_events[i]);
eventadd(g_efd, EPOLLIN, &g_events[i]); //将cfd添加到红黑树g_efd中,监听读事件
} while(0);
printf("new connect [%s:%d][time:%ld], pos[%d]\n",
inet_ntoa(cin.sin_addr), ntohs(cin.sin_port), g_events[i].last_active, i);
return ;
}
void recvdata(int fd, int events, void *arg)
{
struct myevent_s *ev = (struct myevent_s *)arg;
int len;
len = recv(fd, ev->buf, sizeof(ev->buf), 0); //读文件描述符, 数据存入myevent_s成员buf中
eventdel(g_efd, ev); //将该节点从红黑树上摘除
if (len > 0) {
ev->len = len;
ev->buf[len] = '\0'; //手动添加字符串结束标记
printf("C[%d]:%s\n", fd, ev->buf);
eventset(ev, fd, senddata, ev); //设置该 fd 对应的回调函数为 senddata
eventadd(g_efd, EPOLLOUT, ev); //将fd加入红黑树g_efd中,监听其写事件
} else if (len == 0) {
close(ev->fd);
/* ev-g_events 地址相减得到偏移元素位置 */
printf("[fd=%d] pos[%ld], closed\n", fd, ev-g_events);
} else {
close(ev->fd);
printf("recv[fd=%d] error[%d]:%s\n", fd, errno, strerror(errno));
}
return;
}
void senddata(int fd, int events, void *arg)
{
struct myevent_s *ev = (struct myevent_s *)arg;
int len;
len = send(fd, ev->buf, ev->len, 0); //直接将数据 回写给客户端。未作处理
eventdel(g_efd, ev); //从红黑树g_efd中移除
if (len > 0) {
printf("send[fd=%d], [%d]%s\n", fd, len, ev->buf);
eventset(ev, fd, recvdata, ev); //将该fd的 回调函数改为 recvdata
eventadd(g_efd, EPOLLIN, ev); //从新添加到红黑树上, 设为监听读事件
} else {
close(ev->fd); //关闭链接
printf("send[fd=%d] error %s\n", fd, strerror(errno));
}
return ;
}
/*创建 socket, 初始化lfd */
void initlistensocket(int efd, short port)
{
struct sockaddr_in sin;
int lfd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(lfd, F_SETFL, O_NONBLOCK); //将socket设为非阻塞
memset(&sin, 0, sizeof(sin)); //bzero(&sin, sizeof(sin))
sin.sin_family = AF_INET;
sin.sin_addr.s_addr = INADDR_ANY;
sin.sin_port = htons(port);
bind(lfd, (struct sockaddr *)&sin, sizeof(sin));
listen(lfd, 20);
/* void eventset(struct myevent_s *ev, int fd, void (*call_back)(int, int, void *), void *arg); */
eventset(&g_events[MAX_EVENTS], lfd, acceptconn, &g_events[MAX_EVENTS]);
/* void eventadd(int efd, int events, struct myevent_s *ev) */
eventadd(efd, EPOLLIN, &g_events[MAX_EVENTS]);
return ;
}
int main(int argc, char *argv[])
{
unsigned short port = SERV_PORT;
if (argc == 2)
port = atoi(argv[1]); //使用用户指定端口.如未指定,用默认端口
g_efd = epoll_create(MAX_EVENTS+1); //创建红黑树,返回给全局 g_efd
if (g_efd <= 0)
printf("create efd in %s err %s\n", __func__, strerror(errno));
initlistensocket(g_efd, port); //初始化监听socket
struct epoll_event events[MAX_EVENTS+1]; //保存已经满足就绪事件的文件描述符数组
printf("server running:port[%d]\n", port);
int checkpos = 0, i;
while (1) {
/* 超时验证,每次测试100个链接,不测试listenfd 当客户端60秒内没有和服务器通信,则关闭此客户端链接 */
long now = time(NULL); //当前时间
for (i = 0; i < 100; i++, checkpos++) { //一次循环检测100个。 使用checkpos控制检测对象
if (checkpos == MAX_EVENTS)
checkpos = 0;
if (g_events[checkpos].status != 1) //不在红黑树 g_efd 上
continue;
long duration = now - g_events[checkpos].last_active; //客户端不活跃的世间
if (duration >= 60) {
close(g_events[checkpos].fd); //关闭与该客户端链接
printf("[fd=%d] timeout\n", g_events[checkpos].fd);
eventdel(g_efd, &g_events[checkpos]); //将该客户端 从红黑树 g_efd移除
}
}
/*监听红黑树g_efd, 将满足的事件的文件描述符加至events数组中, 1秒没有事件满足, 返回 0*/
int nfd = epoll_wait(g_efd, events, MAX_EVENTS+1, 1000);
if (nfd < 0) {
printf("epoll_wait error, exit\n");
break;
}
for (i = 0; i < nfd; i++) {
/*使用自定义结构体myevent_s类型指针, 接收 联合体data的void *ptr成员*/
struct myevent_s *ev = (struct myevent_s *)events[i].data.ptr;
if ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)) { //读就绪事件
ev->call_back(ev->fd, events[i].events, ev->arg);
//lfd EPOLLIN
}
if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) { //写就绪事件
ev->call_back(ev->fd, events[i].events, ev->arg);
}
}
}
/* 退出前释放所有资源 */
return 0;
}