epoll是poll函数的加强版,因为针对同时监听大量文件描述符时poll函数比较低效率。select受到监听描述符个数的限制,比如我的示例三中的数值是1024,同时监听1025个描述符就不可能了。epoll模型不受这个限制,可打开的文件描述符远大于这个数字。IO效率也不会因为FD的增加而线性下降,我们来看看epoll怎么实现TCP并发服务器吧。
以下是服务器端代码,客户端代码仍用多进程的那个版本
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<sys/epoll.h>
#include<fcntl.h>
#define MAX_LISTEN 5
#define PORT 1987
#define IP "127.0.0.1"
#define MAX_EVENTS 100
int setnonblocking(int sock)
{
int opts;
opts = fcntl(sock,F_GETFL);
if (opts < 0) {
return 0;
}
opts = opts | O_NONBLOCK;
if (fcntl(sock,F_SETFL,opts) < 0) {
return 0;
}
return 1;
}
int main()
{
int conn_fd;
int sock_fd = socket(AF_INET,SOCK_STREAM,0);
if (sock_fd < 0) {
perror("create socket failed");
exit(1);
}
struct sockaddr_in addr_client;
socklen_t client_size = sizeof(struct sockaddr_in);
struct sockaddr_in addr_serv;
memset(&addr_serv, 0, sizeof(addr_serv));
addr_serv.sin_family = AF_INET;
addr_serv.sin_port = htons(PORT);
addr_serv.sin_addr.s_addr = inet_addr(IP);
if (bind(sock_fd,(struct sockaddr *)&addr_serv,sizeof(struct sockaddr_in)) < 0) {
perror("bind error");
exit(1);
}
if (listen(sock_fd,MAX_LISTEN) < 0) {
perror("listen failed");
exit(1);
}
//
int recv_num;
int send_num;
char recv_buf[100];
char send_buf[100];
//初始化epoll描述符
int epfd = epoll_create(MAX_EVENTS);
if (epfd <= 0) {
perror("create epoll failed!");
exit(1);
}
int i, nready;
struct epoll_event ev,events[MAX_EVENTS];
//注册sock_fd描述符监听事件
ev.data.fd = sock_fd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,sock_fd,&ev);
while (1) {
nready = epoll_wait(epfd, events, MAX_EVENTS, 500);
for (i = 0;i < nready;i ++) {
//如果是sock_fd描述符可读,则创建新连接,并添加监听事件
if (events[i].data.fd == sock_fd) {
conn_fd = accept(sock_fd, (struct sockaddr *)&addr_client, &client_size);
if (conn_fd < 0) {
perror("accept failed");
exit(1);
}
setnonblocking(conn_fd);
ev.data.fd = conn_fd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,conn_fd,&ev);
}
else if(events[i].events & EPOLLIN && events[i].data.fd >= 0) {
//如果描述符可读,则读取数据后修改描述符状态去监听可写状态
recv_num = recv(events[i].data.fd, recv_buf, sizeof(recv_buf), 0);
if (recv_num <= 0) {
close(events[i].data.fd);
events[i].data.fd = -1;
}
ev.data.fd = events[i].data.fd;
ev.events = EPOLLOUT | EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,events[i].data.fd,&ev);
}
else if(events[i].events & EPOLLOUT) {
sprintf(send_buf, "server proc got %d bytes\n", recv_num);
send_num = send(events[i].data.fd, send_buf, strlen(send_buf), 0);
if (send_num <= 0) {
close(events[i].data.fd);
events[i].data.fd = -1;
}
ev.data.fd = events[i].data.fd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,events[i].data.fd,&ev);
}
}
}
close(sock_fd);
return 0;
}
epoll模型也是比较好用的,主要是三个函数:epoll_create,epoll_ctl以及epoll_wait
int epoll_create(int size)函数生成一个epoll专用的文件描述符,size表示这个epoll描述符上能关注socket的个数,大小自己可根据内存情况设定。
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);函数可以控制某个文件描述符上的事件‘注册,修改,删除。
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout)函数等待I/O事件发生。
可以明显看出,操作系统仅仅把能够IO操作的描述符返回给我们,而不像poll以及select一样需要自己轮询查找,性能提升可见是明显的了。