关于EPOLL的LT与ET模式以及阻塞和非阻塞


结合《EPOLL模型详解》去理解

一 EPOLL的LT与ET模式简述

在EPOLLLT(水平触发)模式下,也就是默认的模式,epoll_wait返回可读事件,表明socket一定收到了数据,我们可以调用read函数来读取数据。如果指定读取的数据大于缓冲区数据,无论socket是阻塞还是非阻塞的,read不会阻塞,read返回读取的真实数据。在read之后再次调用read,如果socket是阻塞的,read将阻塞,再次收到数据read才返回。此时如果指定读取的数据大于缓冲区,epoll_wait则不再触发,否则epoll_wait将再次触发,因为还有未读完的数据在缓冲区。

在EPOLLET(电平触发)模式下,只有新的数据来到时才会触发,因此在这种情况下,有数据时必须循环读取数据直到read返回-1,并且错误码为EAGAIN,才算读取了全部的缓冲区数据。

二 关于在socket和EPOLL中的阻塞与非阻塞

关于socket中的阻塞与非阻塞,首先明白的是,阻塞与非阻塞是文件(文件描述符)的性质,而不是函数的性质!

1首先注意到的是SOCKET通信中用到的三个文件描述符

客户端:

  • connfd,客户端创建socket时候得到的文件描述符。connect使用这个描述符主动发起链接。

服务端:

  • listenfd,创建socket得到的文件描述符,同时bind和listen使用的也是这个文件描述符
  • clientfd:调用accept得到的文件描述符,也就是用于通信的文件描述符

注意:
accept函数并不参与三次握手过程,accept函数从已经连接的队列中去除连接,并返回clientfd,随后客户端和服务店通过connfd和clientfd进行通信。

2 socket通信中相关的文件描述符是否设置为阻塞模式对下列api造成的影响

1、当connfd被设置为阻塞模式的时候(默认),connect函数会一直阻塞到连接成功或超时出错,超时值需要修改内核参数。

注意:connfd的阻塞与否影响的不仅是connect,还有客户端的read以及write(send、recv等)函数族

2、 当connfd被设置成非阻塞模式,无论连接是否成功,connect都会立刻返回,那如何判断connect是否成功呢?接下来使用select和poll函数去判断socket是否可写即可,当然,linux上还需要加上额外的一步—使用getsockopt函数判断此时的socket是否有错,这就是所谓的异步connect或者叫做非阻塞connect(这是实际网络编程中写的比较多的逻辑,也是高频面试题)

下面是使用select、poll、epoll实现异步connect的代码
https://link.zhihu.com/?target=https%3A//github.com/balloonwj/mybooksources/blob/master/Chapter04/code/linux_nonblocking_connect_poll.cpp

3、当listenfd设置成阻塞模式的时候(默认,无需设置),如果连接pending队列中有需要处理的连接,accet函数会立即返回,否则会一直阻塞下去,知道新的连接到来。
也就是说,listenfd的阻塞与否影响的是accept,而不会影响bind和listen

4、当listenfd设置成非阻塞的时候,无论连接pending是否有需要处理的连接,accpet都会立即返回,不会阻塞。如果有连接,则accept返回一个大于0的值,这个值即使我们上面所说的clientfd;如果没有连接,accept返回值小于0,错误码为EAGIN

5、当connfd或clientfd设置为阻塞模式的时候(默认),send会尝试发数据,如果对端因为TCP窗口太小导致本段无法发送出去,send函数会一直阻塞到对端TCP窗口变大足以发送数据或者超时;recv则相反,如果此时没有数据可获取,recv函数会一直阻塞直到收取到数据或者超时,有的话,去到数据后返回。send和recv函数的超时时间可以分别使用SO_SNDTIMEO和SO_RCTIMEO两个套接字选项来设置。

这种特性是与read和write是相同的。详见之前的博客
这两个函数与read的比较?区别?

6、当connfd和clientfd设置成非阻塞模式的时候,send和recv函数都会立即返回,send函数即使因为对端TCP窗口太小发送不出去也会立即返回,recv函数如果无数据可收也会立即返回,此时这两个函数的返回值都是-1,错误码都是EAGIN。这种情况下,send和recv函数的返回值有三种情况,分别是大于0,等于0,小于0.总结 如下:
在这里插入图片描述

三 几点结论与常识

参考自:https://blog.csdn.net/zxm342698145/article/details/80524331
在这里插入图片描述
(注这个图中的水平模式实际上是LT模式,电平模式实际上是ET模式)

总结一下:(具体还是要参照《EPOLL模型详解那篇博客》)

  1. 对于监听的 sockfd,一般设置成非阻塞。最好使用水平触发模式,边缘触发模式会导致高并发情况下,有的客户端会连接不上。如果非要使用边缘触发,可以用 while 来循环 accept()

  2. 对于读写的 connfd,水平触发模式下,阻塞和非阻塞效果都一样,因为在阻塞模式下,如果数据读取不完全则返回继续触发,反之读取完则返回继续等待。全建议设置非阻塞。

  3. 对于读写的 connfd,边缘触发模式下,必须使用非阻塞 IO,并要求一次性地完整读写全部数据。

  4. 使用LT编写程序完全可以,但是使用ET的目的就是减少系统调用提高效率。但是使用了ET,其他的一些东西也要改变,最典型的一点就是监听文件描述符和连接文件描述符最好是设置成非阻塞的。

最后要能回答出几个问题:

  • 为什么监听文件描述符要设置成非阻塞,阻塞可以吗。什么时候使用LT模式,什么时候使用ET模式
  • 连接文件描述符什么时候使用LT什么时候使用ET,为什么不能是阻塞的。

四 一个例子

下面的例子没有解决ET模式下读写的两个主要问题:
1 读的时候要一直读完缓冲区
2 写得时候要一直写完

解决方法可以参照《EPOLL模型详解》

#include <sys/socket.h>
#include <sys/epoll.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
 
 
#define MAX_LINE     10
#define MAX_EVENTS   500
#define MAX_LISTENFD 5
 
int createAndListen() {
	int on = 1;
	int listenfd = socket(AF_INET, SOCK_STREAM, 0);
	struct sockaddr_in servaddr;
	fcntl(listenfd, F_SETFL, O_NONBLOCK);
	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(5859);
 
	if (-1 == bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)))  {
		printf("bind errno, errno : %d \n", errno); 
	}
 
	if (-1 == listen(listenfd, MAX_LISTENFD))  {
		printf("listen error, errno : %d \n", errno); 
	}
	printf("listen in port 5859 !!!\n");
	return listenfd;
}
 
 
int main(int argc, char const *argv[])
{
	struct epoll_event ev, events[MAX_EVENTS];
	int epollfd = epoll_create(1);     //这个参数已经被忽略,但是仍然要大于
	if (epollfd < 0)  {
		printf("epoll_create errno, errno : %d\n", errno);
	}
	int listenfd = createAndListen();
	ev.data.fd = listenfd;
	ev.events = EPOLLIN;
	epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev);
 
	for ( ;; )  {
		int fds = epoll_wait(epollfd, events, MAX_EVENTS, -1);   //时间参数为0表示立即返回,为-1表示无限等待
		if (fds == -1)  {
			printf("epoll_wait error, errno : %d \n", errno);
			break;
		}
		else {
			printf("trig %d !!!\n", fds);
		}
 
		for (int i = 0; i < fds; i++)  {
			if (events[i].data.fd == listenfd)  {
				struct sockaddr_in cliaddr;
				socklen_t clilen = sizeof(struct sockaddr_in);
				int connfd = accept(listenfd, (sockaddr*)&cliaddr, (socklen_t*)&clilen);//因为监听文件描述符是LT模式的,所以这里不需要进行while包围
				if (connfd > 0)  {
					printf("new connection from %s : %d, accept socket fd: %d \n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), connfd);
				}
				else  {
					printf("accept error, connfd : %d, errno : %d \n", connfd, errno);
				}
				fcntl(connfd, F_SETFL, O_NONBLOCK); //将连接文件描述符设置为非阻塞
				ev.data.fd = connfd;
				ev.events = EPOLLIN | EPOLLET;
				if (-1 == epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev))  {
					printf("epoll_ctl error, errno : %d \n", errno);
				}
			}
			else if (events[i].events & EPOLLIN)  {
				int sockfd;
				if ((sockfd =events[i].data.fd) < 0)  {
					printf("EPOLLIN socket fd < 0 error \n");
					continue;
				}
				char szLine[MAX_LINE + 1] ;
				int readLen = 0;
				bzero(szLine, MAX_LINE + 1);
				if ((readLen = read(sockfd, szLine, MAX_LINE)) < 0)  {
					printf("readLen is %d, errno is %d \n", readLen, errno);
					if (errno == ECONNRESET)  {
						printf("ECONNRESET closed socket fd : %d \n", events[i].data.fd);
						close(sockfd);
					}
				}
				else if (readLen == 0)  {
					printf("read 0 closed socket fd : %d \n", events[i].data.fd);
					//epoll_ctl(epollfd, EPOLL_CTL_DEL, sockfd , NULL);  
					//close(sockfd);
				}
				else  {
					printf("read %d content is %s \n", readLen, szLine);
				}
 
				bzero(szLine, MAX_LINE + 1);
				if ((readLen = read(sockfd, szLine, MAX_LINE)) < 0)  {
					printf("readLen2 is %d, errno is %d , ECONNRESET is %d \n", readLen, errno, ECONNRESET);
					if (errno == ECONNRESET)  {
						printf("ECONNRESET2 closed socket fd : %d \n", events[i].data.fd);
						close(sockfd);
					}
				}
				else if (readLen == 0)  {
					printf("read2 0 closed socket fd : %d \n", events[i].data.fd);
				}
				else  {
					printf("read2 %d content is %s \n", readLen, szLine);
				}
			}
		}
 
	}
	return 0;
}
  • 6
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值