本文供自己复习使用,更加详细的信息可以参见:
大丙哥文章:IO多路转接(复用)之epoll
大丙哥视频:Linux编程 - 网络篇【IO多路转接 - 提升】
epoll边沿触发模式ET
边沿模式可以简称为ET模式,ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当文件描述符从未就绪变为就绪时,内核会通过epoll通知使用者。然后它会假设使用者知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知(only once)。如果我们对这个文件描述符做IO操作,从而导致它再次变成未就绪,当这个未就绪的文件描述符再次变成就绪状态,内核会再次进行通知,并且还是只通知一次。ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。
边沿模式的特点:
- 这里重点介绍读事件:当读缓冲区有新的数据进入,读事件被触发一次,没有新数据不会触发该事件
- 如果有新数据进入到读缓冲区,读事件被触发,epoll_wait()解除阻塞
- 读事件被触发,可以通过调用read()/recv()函数将缓冲区数据读出
- 如果数据没有被全部读走,并且没有新数据进入,读事件不会再次触发,只通知一次
- 如果数据被全部读走或者只读走一部分,此时有新数据进入,读事件被触发,并且只通知一次
但是边沿触发模式一定要进行非阻塞处理,比如我们每次在与服务器建立连接后,如果服务器代码的buf不够大,如下代码所示:
while(1){
int num = epoll_wait(epfd, evs, size, -1);
for(int i=0; i<num; ++i)
{
int curfd = evs[i].data.fd;
if(curfd == lfd){
int cfd = accept(curfd, NULL, NULL);
struct epoll_event ev;
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = cfd;
ret = epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
if(ret == -1){
perror("epoll_ctl-accept");
exit(0);
}
}else{
char buf[5];
int len = recv(cfd, buf, sizeof(buf), 0);
if(len == 0) {
printf("断开链接\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, curfd, NULL);
close(curfd);
}
else if(len > 0){
printf("客户端say: %s\n", buf);
send(curfd, buf, len, 0);
}else{
perror("recv");
exit(0);
}
}
}
}
如果我们在客户端输入“hello world oh my god !”
服务端这里只能读出“hello”
会导致服务器无法把读缓冲区的数据全部读干净,只有你再次从客户端写到服务器端到读缓冲区,他就会把上次剩的再读出来。" worl"
那么我们如何才能解决这个问题呢?
首先一个我们可以把buf改大一点,但是这个治标不治本,而且如果申请的空间太大,内核也并不会给我们分配这么大的空间。
下面介绍第二个解决方法:
epoll边沿模式的非阻塞处理,并且在非阻塞情况下,接受数据的代码也是比较有特点的。
epoll边沿模式的非阻塞处理
在介绍边沿模式的非阻塞处理前,我先介绍一个别的方法,那就是把读缓冲区的代码放到一个循环里边。
char buf[5];
while(1) {
int len = recv(cfd, buf, sizeof(buf), 0);
}
然而,我们的recv()函数是阻塞的,如果我们读完所有的数据之后,文件描述符读缓冲区为NULL
,也就是说我们读完第一次,程序就会一直阻塞到这里。程序就不能再干别的事情了,这种情况并不是我们想看到的。
那这又该如何解决呢?
我们把文件描述符设置为非阻塞,等读完数据后,我们继续往下执行,判断是否已经读完缓冲区的数据,读完跳出循环,这样我们就可以继续做别的事情了。
for (int i = 0; i < num; ++i) { //找到被触发的文件描述符
int fd = evs[i].data.fd;
if(fd == lfd){
int cfd = accept(fd, NULL, NULL);
//为文件描述符设置非阻塞属性
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = cfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
}else{
char buf[5];
while(1) {
//接受数据部分请看下一节
}
}
}
epoll在边沿模式下非阻塞接收数据
recv
的错误号EAGAIN
表示我们操作过程中读缓冲区是没有数据的,所以为了防止我们操作空的读缓冲区导致代码报错,我们把错误号等于EAGAIN的时候作为数据被读完的标志。
while(1) {
int len = recv(fd, buf, sizeof(buf), 0);
if (len == -1) {
if (error == EAGAIN) {
printf("数据已经读完了");
break;
}else{
perror("recv error");
exit(1);
}
}
else if (len == 0) {
printf("客户端已断开链接。。。\n");
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
close(fd);
break;
}
printf("read buf = %s\n", buf);
//小写转大写
for (int i = 0; i < len; ++i) {
buf [i] = toupper(buf[i]);
}
//这样输出在终端没有乱码,因为我们指定了buf的长度
write(STDOUT_FILENO, buf, len);
//printf("%s", buf) 这样会有乱码,因为没有地方存“\0”
//大写串发送给客户端
if (send(fd, buf, strlen(buf) + 1, 0) == -1) {
perror("send error");
exit(1);
}
}