套接字I/O模型-epoll
epoll
是Linux
下多路复用IO接口select/poll
的增强版本,它能显著提高程序在大量并
发连接中只有少量活跃的情况下的系统CPU利用率
,因为它会复用文件描述符集合来传递结
果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点
原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件 异步唤醒而加入Ready队列的描述符集合
就行了。
epoll的设计:
红黑树
来构建,红黑树在增加和删除上面的效率极高
,因此是epoll高效
的原因之一。有兴趣可以百度红黑树了解,但在这里你只需知道其算法效率超高即可。事件异步唤醒
,内核监听I/O
,事件发生后内核搜索红黑树
并将对应节点数据
放入异步唤醒的事件队列
中。数据
从用户空间
到内核空间
采用mmap存储I/O映射
来加速。该方法是目前Linux进程间通信中传递最快,消耗最小,传递数据过程不涉及系统调用的方法。select
最不能忍受的是一个进程所打开的FD是有一定限制
的,由FD_SETSIZE
设置,默认值是1024。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏
然后重新编译服务器代码,不过资料也同时指出这样会带来网络效率的下降
,二是可以选择多进程的解决方案
(传统的Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。epoll
则没有这个限制,它所支持的FD上限是最大可以打开文件的数目
,这个数字一般远大于1024,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以下面语句查看,一般来说这个数目和系统内存关系很大。系统最大打开文件描述符数
cat /proc/sys/fs/file-max
进程最大打开文件描述符数
ulimit -n
修改系统文件 /etc/security/limits.conf
sudo vi /etc/security/limits.conf
写入以下配置,soft软限制,hard硬限制
* soft nofile 65536
* hard nofile 100000
epoll只有epoll_create、epoll_ctl和epoll_wait这三个系统调用。其定义如下:
#include
int epoll_create(int size);
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);
作用: 创建一个epoll句柄,参数size用来告诉内核监听的文件描述符个数
int epoll_create(int size)
size:告诉内核监听的数目
返回值: 可以理解为epoll_create创建了可以红黑树的数据结构来存储数据,而epoll_create
返回的值为红黑树的树根
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
第一个参数是epoll_create()的返回值
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD
:注册新的fd到epfd中;
EPOLL_CTL_MOD
:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL
:从epfd中删除一个fd;
第三个参数是需要监听的fd
第四个参数是告诉内核需要监听什么事
struct epoll_event结构
如下:
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队列里。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
该函数用于轮询I/O事件的发生, 等待事件的产生,类似于select()调用。
参数:
epfd
:由epoll_create 生成的epoll专用的文件描述符;epoll_event
:用于回传代处理事件的数组;maxevents
:每次能处理的事件数;timeout
:等待I/O事件发生的超时值(单位我也不太清楚);-1相当于阻塞,0相当于非阻塞。一般用-1即可返回发生事件数。epoll_wait运行的原理是
-等侍注册在epfd上的socket fd的事件的发生
,如果发生则将发生的sokct fd和事件类型
放入到events数组中
。
epoll_server.c
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 80
#define SERV_PORT 8000
#define OPEN_MAX 1024
//自定义的出错的处理函数
void perr_exit(char *str)
{
perror(str);
exit(-1);
}
int main(int argc, char *argv[]) {
int i, j, maxi, listenfd, connfd, sockfd;
int nready, efd, res;
ssize_t n;
char buf[MAXLINE], str[INET_ADDRSTRLEN];
socklen_t clilen;
int client[OPEN_MAX];
struct sockaddr_in cliaddr, servaddr;
struct epoll_event tep, ep[OPEN_MAX];
//创建监听套接字
listenfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(SERV_PORT);
//绑定监听套接字和 ip/端口号
bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr));
listen(listenfd, 20);
/* 用-1初始化client[] */
for (i = 0; i < OPEN_MAX; i++)
client[i] = -1;
/* client[]的下标 */
maxi = -1;
//创建一颗红黑树(用来存储所有客户端信息) efd->树根
efd = epoll_create(OPEN_MAX);
//红黑树创建失败
if (efd == -1)
perr_exit("epoll_create");
//把监听套接字加入红黑树
tep.events = EPOLLIN;
tep.data.fd = listenfd;
res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep);
//listenfd加入红黑树失败
if (res == -1)
perr_exit("epoll_ctl");
for ( ; ; )
{
/* 阻塞监听红黑树 */
nready = epoll_wait(efd, ep, OPEN_MAX, -1);
//监听出错
if (nready == -1)
perr_exit("epoll_wait");
//遍历红黑树上需要操作的节点
for (i = 0; i < nready; i++)
{
//表示红黑树上的文件描述符不可以读则跳过
if (!(ep[i].events & EPOLLIN))
continue;
//红黑树上节点的值 == listenfd,说明有新的客户端连接
if (ep[i].data.fd == listenfd)
{
clilen = sizeof(cliaddr);
//获取客户端套接字、ip/端口号信息
connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);
//打印新连接得客户端 ip/端口号
printf("received from %s at PORT %d\n",
inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));
printf("客户端新连接");
//新连接得客户端套接字 加入client数组
for (j = 0; j < OPEN_MAX; j++)
if (client[j] < 0)
{
client[j] = connfd; /* save descriptor */
break;
}
//连接得客户端过多报错
if (j == OPEN_MAX)
perr_exit("too many clients");
//更新maxi的值(maxi代表最后一个连接得的客户端的套接字)
if (j > maxi)
maxi = j; /* max index in client[] array */
//新连接的客户端套接字加入红黑数,继续监听。
tep.events = EPOLLIN;
tep.data.fd = connfd;
res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep);
//新客户端加入失败
if (res == -1)
perr_exit("epoll_ctl");
}
//红黑树上节点的值 != listenfd,说明有有客户端想要读/写数据
else
{
//客户端套接字
sockfd = ep[i].data.fd;
//读取缓存区数据到buf
n = read(sockfd, buf, MAXLINE);
//设置客户端终止条件(当发送数据为空时)
if (n == 0)
{
for (j = 0; j <= maxi; j++)
{
if (client[j] == sockfd)
{
client[j] = -1;
break;
}
}
res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
if (res == -1)
perr_exit("epoll_ctl");
close(sockfd);
printf("client[%d] closed connection\n", j);
}
//给客户端发送答应数据(小写——>大写)
else
{
for (j = 0; j < n; j++)
buf[j] = toupper(buf[j]);
write(sockfd, buf, n);
}
}
}
}
close(listenfd);
close(efd);
return 0;
}
epoll_client.c
/* client.c */
#include
#include
#include
#include
#define MAXLINE 80
#define SERV_PORT 8000
int main(int argc, char *argv[])
{
struct sockaddr_in servaddr;
char buf[MAXLINE];
int sockfd, n;
sockfd = socket(AF_INET, SOCK_STREAM, 0);
bzero(&servaddr, sizeof(servaddr));
servaddr.sin_family = AF_INET;
inet_pton(AF_INET, "127.0.0.1", &servaddr.sin_addr);
servaddr.sin_port = htons(SERV_PORT);
//连接服务端
connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
//从终端输入数据
while (fgets(buf, MAXLINE, stdin) != NULL)
{
//发送数据
write(sockfd, buf, strlen(buf));
//接收服务端的答应数据
n = read(sockfd, buf, MAXLINE);
if (n == 0)
printf("the other side has been closed.\n");
else
write(STDOUT_FILENO, buf, n);
}
close(sockfd);
return 0;
}