携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第3天,[点击查看活动详情]
今天的内容笔者将基于源码,从what,why,how三个角度展开讲解epoll这个概念。
1. 什么是epoll
首先来抽象理解一下socket网络编程模型
可以将该过程理解为一个去餐厅消费的过程。
客户端:客人
服务端:消费的餐厅
listenfd:酒店门口迎宾的人,连接来了就给酒店内部的服务员(connfd)。
accept():一个行为,把客人带到大厅中,介绍给connfd服务员,后面服务员和客户之间的recv/send,都是由这个clientfd工作
int listenfd, connfd, n;
struct sockaddr_in servaddr;
char buff[MAXLNE];
// 创建socketfd,即这里的listenfd
if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
//创建五元组
memset(&servaddr, 0, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
servaddr.sin_port = htons(9999);
//绑定listenfd与当前的socket
if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
if (listen(listenfd, 10) == -1) {
printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
//通过accept(),获得connfd
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("========waiting for client's request========\n");
while (1) {
n = recv(connfd, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(connfd, buff, n, 0);
} else if (n == 0) {
close(connfd);
}
//close(connfd);
}
再来看一下epoll的使用模型
区别于上面的一请求一线程模型,epoll能给我们带来单线程实现同时接收多请求的使用场景。
理解模型:快递员--蜂巢--住户模型
小区中有N多住户,对应一个住户所有人的大集合----就是fd集合
蜂巢对应另一个子集---今天需要寄快递的用户
epoll_create()--创建快递员
epoll_ctl()--往小区里搬进/搬出/移动位置住户
epoll_wait()--快递员多久来蜂巢取一次快递这个过程
events--快递员取快递需要拿的袋子,需要指定大小,设置的大小决定了跑几次,无非就是多跑几次,是个就绪队列,双向链表,可变的,一百万的并发,大约有一万个活跃的连接,所以不用担心数据量大了。
2. 为什么要用epoll
答案很简单,因为epoll性能高
libevent对这几个IO多路复用技术做过对比
这是一个限制了 100 个活跃连接的基准测试,每个连接发生 1000 次读写操作为止。纵轴是请求的响应时间,横轴是持有的 socket 句柄数量。
可以看出来,epoll 性能是很高的,并且随着监听的文件描述符的增加,epoll 的优势更加明显
不过,这里限制的 100 个连接很重要。epoll 在应对大量网络连接时,只有活跃连接很少的情况下才能表现的性能优异。
换句话说,epoll 在处理大量非活跃的连接时性能才会表现的优异。如果15000个 socket 都是活跃的,epoll 和 select 其实差不了太多。
3. 怎么用epoll
epoll的组成
三个耳熟能详的用户层API:
int epoll_create(int size);
函数实现功能:创建一个epoll的句柄,最新版本的内核实现中,这个size只要大于0即可。同时需要注意消耗一个fd。
返回值:返回值为一个int类型的文件描述符,作为后面两个函数的参数
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数:第一个参数是epoll_create()的返回值;第二个参数表示动作,用三个宏来表示;第三个参数是需要监听的fd;第四个参数是告诉内核需要监听什么事件;
函数实现功能:epoll的事件注册函数,epoll_ctl向 epoll对象中添加、修改或者删除感兴趣的事件
返回值:返回0表示成功,否则返回–1,此时需要根据errno错误码判断错误类型。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
函数实现功能:等待事件的产生,类似于select()调用。
参数:参数events用来从内核得到事件的集合;maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size;参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,永久阻塞)。
返回值:该函数返回需要处理的事件数目,如返回0表示已超时。如果返回–1,则表示出现错误,需要检查 errno错误码判断错误类型
这三个API的底层实现在Linux内核,所以从使用层面来讲,只需要弄清楚这三个API的功能即可
int epfd = epoll_create(1); //int size
struct epoll_event events[POLL_SIZE] = {0};
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listenfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
while (1) {
int nready = epoll_wait(epfd, events, POLL_SIZE, 5); //如果这里设置-1,表示有了再去,没有就不去
if (nready == -1) {
continue;
}
int i = 0;
for (i = 0;i < nready;i ++) {
int clientfd = events[i].data.fd;
if (clientfd == listenfd) {
struct sockaddr_in client;
socklen_t len = sizeof(client);
if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
return 0;
}
printf("accept\n");
ev.events = EPOLLIN;
ev.data.fd = connfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
} else if (events[i].events & EPOLLIN) {
n = recv(clientfd, buff, MAXLNE, 0);
if (n > 0) {
buff[n] = '\0';
printf("recv msg from client: %s\n", buff);
send(clientfd, buff, n, 0);
} else if (n == 0) { //
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd, EPOLL_CTL_DEL, clientfd, &ev);
close(clientfd);
}
}
}
}
close(listenfd);
return 0;
}