2013-07-15,重构了epoll服务器模型的代码
两种不同的触发器模型:边沿触发(ET:Edge-Trigger)和水平触发(LT:Level-Trigger)
1. 边沿触发(ET:Edge-Trigger):当调用前条件是unready,并且调用过程中条件变成ready,才给予调用者通知。
假设调用代码是ret = check(condition);,如果调用时的condition是ready的,那么即使接下来condition还是ready,check函数返回给ret还是无效通知或者不返回(阻塞式)。当且仅当condition的值是unready时,然后在调用的过程中,condition变成了ready,那么此时check函数会返回一个有效通知给ret。
2. 水平触发(LT:Level-Trigger):当调用时条件是ready,就给予调用者通知。
假设调用代码是ret = check(condition);,只要调用时,condition是ready的,check函数就会返回一个有效通知给ret,否则返回无效通知或者不返回(阻塞式)。
首先,什么叫把condition的条件变成ready或者unready。假设有个这样的场景:我们监听一个网络套接字sockfd上是否有数据可读,假定有数据可读就是ready状态,没数据可读就是unready状态。
容易出错的往往是ET模型。假设sockfd上一开始是没有数据可读(unready),然后我调用ret = check(sockfd);,以阻塞式为例吧,那么此时线程会一直阻塞在check里面。某一时刻,远方有人往该sockfd上发来数据,那么该sockfd的状态就由unready变成了ready,显而易见,check应该返回一个有效通知,此时该线程收到通知,然后去读。注意看,接下来有两种情况:1)线程开始读取sockfd的数据,直到全部读取完毕,然后又调用check(sockfd);2)线程开始读取sockfd的数据,没有全部读取完毕,然后又调用check(sockfd)。首先说结果,1)是正确的,2)是错误的。然后说为什么。相信已经能够理解,把ready转换成unready的操作就是全部读完sockfd的数据,把unready转换成ready的操作就是远方有人发数据到该sockfd来了。2)错就错在,没有把sockfd的数据读干净,以至于没有把状态变成unready,那么即使后面又来了新数据,check(sockfd);还是会一直阻塞下去。所以使用ET模型的时候,一定要注意,每次收到有效通知,然后读取数据的时候,务必每次读干净(读到出错为止)。当再次调用check(sockfd);的时候才能正确返回。
那么LT模型却不会出现ET模型中的2)情况。为什么呢?设想一下,即使你没有读取完数据,又去调用check(sockfd);,因为LT的特性,只要是ready状态就发有效通知,所以,你懂的!
epoll()是什么,它诞生于什么时候,它在哪诞生,它诞生的意义是什么?
epoll()类似于poll()和select(),也是一种能是的I/O多路复用的机制,它诞生于Linux内核2.6版本,它在Linux可用,它是一种它综合后面二者的优势,剔除后面二者的弊端,它诞生的意义完全就是来取代后面二者的。
epoll()比起select()和poll()来说,好在哪?
1. select():一次性可以同时监控FD_SETSIZE个fds,FD_SETSIZE在编译阶段才被确定的,但是很明显,FD_SETSIZE是固定的、有限的。
2. poll():一次性可监控的fds个数是不固定的,看起来这是个好处,比select()灵活且更多,但是问题来了,每一次调用,我们需要对传进去的所有的fds进行一次线性扫描,将花掉O(n)的时间,这个消耗很大。
3. epoll():顾名思义——Enhanced POLL,poll()的加强版。epoll()对于一次性监控的文件描述符个数没有限制(poll()的长处),不需要做线性扫描(避免poll()的短处)。可想而知,epoll()的性能比二者更高。
为什么epoll()有两种工作模式,LT和ET,哪种更高端、大气?
ET比LT更高端、更装逼。当然装逼有风险,也意味着ET更难用,用不好更容易出错。
epoll()怎么用?
epoll机制包含以下几个操作方法和数据结构:
操作方法:
1. int epoll_create(int size);
返回值是一个epoll描述符,size是你想创建一个多大的监听队伍!
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
往epfd队列里注册一个op方式的fd,要求监控fd的event系列的事件
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待epfd队列中的所有注册的fd发生事件,一般timeout用-1,表示永久阻塞。
PS:本人最近在学习iOS开发,事实上,当一个函数的参数上升到4、5个之后,要记住每个参数的意义就显得麻烦了,除非你天天用。那么Objective-C的优势就体现出来了,它会标记出每个参数的意义,相当注释了。
数据结构:
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
从代码出发吧!!!已在Fedora-19-64bit通过编译,运行测试OK
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#define FAIL -1
#define SUCC 0
#define TRUE 1
#define FALSE -1
#define SERVER_PORT 7892
#define SERVER_IP_ADDRESS "192.168.1.100"
//epoll
#define MAX_EPOLL_QUEUE 1024
//listen
#define MAX_LISTEN_QUEUE 10
#define ylog(fmt, ...)\
do {\
fprintf(stdout, "ylog:[%s]:[%d]:"fmt, __FUNCTION__, __LINE__ , ##__VA_ARGS__);\
} while(0)
struct sockaddr_in serverSockaddr_in;
int setnonblocking(int sockfd)
{
int opts;
opts=fcntl(sockfd,F_GETFL); // 把sock的属性查询出来,存到opts中
if(opts<0)
{
ylog("fcntl(sockfd,GETFL)\n");
return FALSE;
}
opts = opts | O_NONBLOCK; // 把opts中的某一位O_NONBLOCK置为生效
if(fcntl(sockfd,F_SETFL,opts)<0) // 再设回去,搞定
{
ylog("fcntl(sockfd,SETFL,opts)\n");
return FALSE;
}
return SUCC;
}
int network_socket(int __domain, int __type, int __protocol)
{
int fd = -1;
fd = socket(__domain, __type, __protocol);
if (fd < 0)
{
ylog("socket error: [%s]\n", strerror(errno));
return FAIL;
}
else
{
return fd;
}
}
int network_sockaddr(struct sockaddr_in *__server, int __protocol_family, int __port, char *__ip_address, int __ip_address_len)
{
memset(__server, 0x00, sizeof(struct sockaddr_in));
__server->sin_family = __protocol_family;
inet_aton(__ip_address,&(__server->sin_addr));
__server->sin_port = htons(__port);
return SUCC;
}
int network_bind_init(int _fd, struct sockaddr* _sockaddr, size_t _size)
{
int ret = FAIL;
ret = bind(_fd, _sockaddr, _size);
if (ret != 0)
{
ylog("bind error: %s\n", strerror(errno));
return FAIL;
}
else
{
return SUCC;
}
}
int network_init(int *_listenFd)
{
int listenFd = FAIL, ret = FAIL;
if (FAIL == (listenFd = network_socket(AF_INET, SOCK_STREAM, 0)))
{
ylog("network_socket_init error\n");
return FAIL;
}
//ylog("finished network_socket\n");
if (SUCC != (ret = network_sockaddr(&serverSockaddr_in, PF_INET, SERVER_PORT, SERVER_IP_ADDRESS, sizeof(SERVER_IP_ADDRESS))))
{
ylog("network_sockaddr_init error, ret = [%d]\n", ret);
return ret;
}
//ylog("finished network_sockaddr\n");
if (SUCC != (ret = network_bind_init(listenFd, (struct sockaddr*)&serverSockaddr_in, sizeof(serverSockaddr_in))))
{
ylog("network_bind_init error, ret = [%d]\n", ret);
return ret;
}
//ylog("finished network_bind_init\n");
*_listenFd = listenFd;
return SUCC;
}
int case_new_client_event(int _fd, int _handle, struct epoll_event *_event)
{
int client_fd;
struct sockaddr_in client;
socklen_t client_len = sizeof(client);
struct epoll_event ev = {0};
ylog("new client event comes\n");
client_fd = accept(_fd, (struct sockaddr *)&client, &client_len); // 先accept才能知道客户端的信息
if (client_fd < 0)
{
ylog("accept faild: %s\n", strerror(errno));
return FALSE;
}
ylog("new client is comming from [%s]\n", inet_ntoa(client.sin_addr));
setnonblocking(client_fd);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = client_fd;
epoll_ctl(_handle, EPOLL_CTL_ADD, client_fd, &ev);
return SUCC;
}
int case_read_event(int _fd, int _handle, struct epoll_event *_event)
{
return SUCC;
}
int case_write_event(int _fd, int _handle, struct epoll_event *_event)
{
return SUCC;
}
int my_switch(int _fd, int _handle, struct epoll_event *_event)
{
if (_event->data.fd == _fd)
{
case_new_client_event(_fd, _handle, _event);
}
else if (_event->events & EPOLLIN)
{
case_read_event(_fd, _handle, _event);
}
else if (_event->events & EPOLLOUT)
{
case_write_event(_fd, _handle, _event);
}
else
{
ylog("nothing to be done\n");
}
return SUCC;
}
int loop(int _fd, int _handle)
{
struct epoll_event ev_queue[MAX_EPOLL_QUEUE];
int total = -1, i = -1;
for(;;)
{
total = epoll_wait(_handle, ev_queue, MAX_EPOLL_QUEUE, -1);
for(i = 0; i < total; i++)
{
if (FAIL == my_switch(_fd, _handle, &(ev_queue[i])))
{
return FAIL;
}
}
}
return SUCC; // program would never reach this line
}
int main()
{
int listenFd = -1, ret = FAIL;
int epoll_handle = -1;
struct epoll_event ev;
ylog("start!\n");
ret = network_init(&listenFd); // do all the trivial jobs for one only propose that get a listenFd
if (ret != SUCC)
{
ylog("network_init failed: ret = [%d]\n", ret);
return ret;
}
// codes below are very important
// have got the listenFd, set it to non-blocking property
setnonblocking(listenFd);
// create a epoll handle
epoll_handle = epoll_create(MAX_EPOLL_QUEUE); // MAX_EPOLL_QUEUE is 1024
// add the listenFd to the queue through the handle, with the events you are interested
ev.data.fd=listenFd; //设置与要处理的事件相关的文件描述符
ev.events=EPOLLIN | EPOLLET; //设置要处理的事件类型和使用的触发器的机制
epoll_ctl(epoll_handle, EPOLL_CTL_ADD, listenFd, &ev); //把listenfd注册到epfd队列中,监听EPOLLIN事件,使用EPOLLET机制的触发器
// start to listen the client's sync package
ret = listen(listenFd, MAX_LISTEN_QUEUE);
if (0 != ret)
{
ylog("listen failed: ret = [%d]\n", ret);
return ret;
}
// loop() function is a typical model of using epoll mechanism
loop(listenFd, epoll_handle);
return 0;
}