Part 2 Linux programming:chapter 17:优于select的epoll(IO复用功能)

第十七章:优于select的epoll(含epoll使用特性、触发方式与select差异以及回声服务器客户端实现)

之前我们在第十二章中介绍了select函数。传统的I/O复用的方法有select函数和epoll函数。但是其性能存在缺点(这点在下面会说到)。因此有了不同操作系统下的I/O复用技术,Linux下的epoll、BSD的kqueue、Win的IOCP等。

17.1 epoll理解及应用

select函数的复用方法,无论如何优化程序性能也无法实现接入上百个客户端。这种方式并不适合当前以Web服务器端开发的主流环境。因此我们这里学习Linux下的epoll

17.1.1 基于select的I/O复用技术为什么速度慢?

12章 基于select的IO复用服务器端,最主要有两点设计不合理(如果忘记代码上方有12章链接):

  1. 调用select函数后常见的针对所有文件描述符的循环语句
  2. 每次调用select函数时都需要向该函数传递监视对象信息

调用select函数后并非把所有发生变化的文件描述符单独集中到一起,而是通过观察作为监视对象的fd_set结构变量的变化,找出发生变化的文件描述符

同时,调用select函数前要对监控对象的fd_set变量进行保存复制,因为变量会发生变化~

并且需要再每次调用select函数时传递新的监视对象信息。

在这些过程中,性能提高的主要障碍是:每次传递监视对象信息。

每次调用select函数时向操作系统传递监视对象信息
这里的关键字是操作系统

应用程序将数据传递给操作系统有很大负担,但是为什么要把监视对象的信息传递给操作系统呢?

因为套接字是由操作系统管理的,而我们需要监视套接字的变化,需要借助操作系统的才能完成

优化的方式就是:仅向操作系统传递一次监视对象,监视范围或内容发生变化时只通知发生变化的事项
这样就不需要每次调用select函数都要向操作系统传递监视对象信息了,这在linux下就是epoll

17.1.2 select的特点(优点)

下面情况下应该考虑使用select函数

  1. 服务器端接入者少
  2. 程序需要高兼容性
17.1.3 实现epoll时必要的函数和结构体

epoll的特点:

  1. 无需编写用来监视状态变化,针对所有文件描述符的循环语句
  2. 调用对用于select函数的epoll_wait函数时无需每次传递监视对象信息

下面说下使用epoll服务器的3个函数,结合epoll的优点理解下面函数功能:

  • epoll_create:创建保存epoll文件描述符的空间。
  • epoll_ctl:向空间注册并注销文件描述符
  • epoll_wait:以select函数类似,等待文件描述符发生变化

下面解释一下:

  1. select方式中为了保存监视对象文件描述符,直接声明了fd_set变量。epoll方式下由操作系统负责保存监视对象文件描述符,因此需要向操作系统请求创建保存文件描述符的空间,此时使用的函数就是epoll_create
  2. 为了添加和删除监视对象文件描述符,select方式中使用FD_SET FD_CLR函数。但在epoll函数中,通过epoll_ctl函数请求操作系统完成。
  3. select方式下调用select函数等待文件描述符变化,epoll中调用epoll_wait函数。还有select方式中通过fd_set变量查看监视对象的状态变化(事件发生与否),epoll中通过结构体epoll_event将发生变化的(发生事件的)文件描述符单独集中到一起。(如下所示)
struct epoll_event
{
	__uint32_t events;
	epoll_data_t data;
}

typedef union epoll_data
{
	void * prt;
	int fd;
	__uint32_t u32;
	__uint64_t u64;
}epoll_data_t;

声明足够大的epoll_event结构体数组后,传递给epoll_wait函数时,发生变化的文件描述符,将被填入该数组。因此无需像select函数那样针对所有文件描述符进行循环。
下面对函数进行详细说明

17.1.4 epoll_create

从Linux内核从2.5.44开始引入了epoll,输入cat /proc/sys/kernel/osrelease查看自己的Linux版本。

epoll_create函数
调用epoll_create函数时创建的文件描述符保存空间称为epoll例程有些时候名称不同,需要稍加注意。
通过参数size传递的值决定epoll例程的大小,但该值只是向操作系统提的建议,换言之,size并非用来决定epoll例程的大小,仅供操作系统参考。

#include <sys/epoll.h>

int epoll_create(int size);
->成功时返回epoll文件描述符,失败时返回-1

size:epoll实例的大小。

(在Linux2.6.8之后,操作系统将完全忽略传入epoll_create函数的参数size,内核会根据情况调整epoll例程的大小。)
epoll_create函数创建的资源与套接字相同,也由操作系统管理。因此该函数和创建套接字的情况相同,也会返回文件描述符。
也就是说,该函数的文件描述符主要用于区分epoll例程。需要终止时,与其他文件描述符相同,也要调用close函数。

17.1.5 epoll_ctl

生成epoll例程后,应在其内部注册监视对象文件描述符,此时使用epoll_ctl函数。

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
->成功时返回0,失败时返回-1.

epfd:用于注册监视对象的epoll例程的文件描述符。
op:用于指定监视对象的添加、删除或更改等操作。
fd:需要注册的监视对象文件描述符
event:监视对象的事件类型

理解一下:

epoll_ctl(A, EPOLL_CTL_ADD, B, C);

epoll例程A中注册文件描述符B,主要目的是监视参数C中保存的事件


epoll_ctl(A, EPOLL_CTL_DEL, B, NULL);

从epoll例程A中删除文件描述符B
从监视对象中删除时,不需要监视类型(事件信息),因此向第四个参数传递NULL。


接下来介绍epoll_ctl第二个参数传递的常量及含义。

  • EPOLL_CTL_ADD:将文件描述符注册到epoll例程
  • EPOLL_CTL_DEL:从epoll例程中删除文件描述符
  • EPOLL_CTL_MOD:更改注册的文件描述符的关注事件发生情况

(Linux2.6.9之前的内核不允许传递NULL,向epoll_ctl第二个参数传递EPOLL_CTL_DEL时,应同时向第四个参数传递NULL,所以正常传递epoll_event结构体变量的地址值就行了。)

epoll_ctl的第四个参数:epoll_event event
“这里是用于保存发生变化(发生事件)的文件描述符。但其作用远不止保存一个文件描述符而已”

struct epoll_event
{
	__uint32_t events;
	epoll_data_t data;
}

typedef union epoll_data
{
	void * prt;
	int fd;
	__uint32_t u32;
	__uint64_t u64;
}epoll_data_t;

我们从例子中去理解这个结构体里面乱七八糟的东西

struct epoll_event event;
...
event.events = EPOLLIN;	// 发生需要 读取数据 的事件时
event.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);
...

上面代码:将sockfd注册到epoll例程epfd中,并在需要读取数据时产生相应事件;
下面给出epoll_event的成员events中可以保存的常量及所指的事件类型

  • EPOLLIN:需要读取数据的情况
  • EPOLLOUT:输出缓冲为空,可以理解发送数据的情况
  • EPOLLPRI:收到OOB数据的情况
  • EPOLLRDHUP:断开连接或半关闭的情况(在边缘触发方式下非常有用)
  • EPOLLERR:发生错误的情况
  • EPOLLET:以边缘触发的方式得到事件通知
  • EPOLLONESHOT:发生一次事件后,相应文件描述符不再收到事件通知。因此需要向epoll_ctl函数的第二个参数传递EPOLL_CTL_MOD,再次设置。
    可以通过位或运算同时传递多个上述参数。
    目前只需了解EPOLLIN即可。
17.1.6 epoll_wait

epoll相关函数中默认最后调用该函数。

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
->成功时返回发生事件的文件描述符数,失败时返回-1

epfd:表示事件发生监视范围的 epoll例程的文件描述符
events:保存发生事件的文件描述符结合的 结构体变量地址值
maxevents:第二个参数中可以保存的最大事件数
timeout:以1/1000秒为单位的等待事件,传递-1表示一直等待到事件发生

epoll_wait函数调用方式及动态分配缓冲

int event_cnt;
struct epoll_event* ep_events;
...
ep_events = malloc(sizeof(struct epoll_event)*EPOLL_EIZE); 
// epoll_size是宏定义常量
...
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
...

调用函数后,返回发生事件的文件描述符数,同时在第二个参数指向的缓冲中保存发生事件的文件描述符集合。因此,无需像select那样插入针对所有文件描述符的循环。

17.1.7 基于epoll的回声服务器端

这里通过修改第12章的echo_selectserv.c实现了基于epoll的回声服务器端
我们先看看之前的echo_selectserv.c,详看注释

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>			// select函数需要用到的两个头文件1
#include <sys/select.h>			// 头文件2

#define BUF_SIZE 100
void error_handling(char *buf);	// 异常处理函数

int main(int argc, char *argv[])
{
	// 定义变量
	int serv_sock, clnt_sock;		// 套接字文件描述符
	struct sockaddr_in serv_adr, clnt_adr;	// 套接字地址结构体变量
	struct timeval timeout;	// 用于select函数中设置超时时间,过时没有问价描述符发生事件,则返回0
	fd_set reads, cpy_reads;	// select函数中存储文件描述符的变量结构体

	socklen_t adr_sz;
	int fd_max, str_len, fd_num, i;
	char buf[BUF_SIZE];
	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}

	// 初始化服务器端套接字和地址信息。
	serv_sock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_adr.sin_port=htons(atoi(argv[1]));
	
	// 分配地址并设置接听
	if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
		error_handling("bind() error");
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");

	// 初始化 结构体变量内数值,
	FD_ZERO(&reads);
	FD_SET(serv_sock, &reads);	//将serv_sock注册到reads中
	fd_max=serv_sock;	// 最大文件描述符数量 (-1)

	while(1)	// 主循环
	{
		cpy_reads=reads;	// 复制reads结构体变量
		timeout.tv_sec=5;	// 设置超时时间
		timeout.tv_usec=5000;

		// 调用select函数进行监视并阻塞,直至有文件描述符发生变化(事件发生)or超时
			// 如果出错则退出循环
		if((fd_num=select(fd_max+1, &cpy_reads, 0, 0, &timeout))==-1)
			break;
			// 如果超时重新循环
		if(fd_num==0)
			continue;
			// 正常情况:for循环 从0开始检查每一个文件描述符的变化
		for(i=0; i<fd_max+1; i++)
		{
			if(FD_ISSET(i, &cpy_reads))	// 如果cpy_reads中的i号文件描述符发生事件
			{
				if(i==serv_sock)     // 如果i是服务器端套接字:代表listen函数接收到了请求
				{
					// 受理请求  并 将新产生的 服务器端套接字注册到监视器reads中
					adr_sz=sizeof(clnt_adr);
					clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
					FD_SET(clnt_sock, &reads);
					if(fd_max<clnt_sock)	// 更新文件描述符最大数量
						fd_max=clnt_sock;
					printf("connected client: %d \n", clnt_sock);
				}
				else    			// 如果i不是服务器端套接字:代表是客户端套接字收到客户端发来的消息
				{
					str_len=read(i, buf, BUF_SIZE);
					if(str_len==0)  // 接收到eof,代表发送结束
					{
						FD_CLR(i, &reads);	// 清除监视器中客户端套接字
						close(i);			// 关闭客户端套接字
						printf("closed client: %d \n", i);
					}
					else			// 未接收到eof:代表接收到了信息,回声服务器把接收到的再传回去。
					{
						write(i, buf, str_len);    // echo!
					}
				}
			}
		}
	}
	close(serv_sock);		// 关闭服务器端套接字
	return 0;
}

void error_handling(char *buf)
{
	fputs(buf, stderr);
	fputc('\n', stderr);
	exit(1);
}

下面是修改后的利用epoll进行io复用的回声服务器客户端
echo_epollserv.c
仔细看注释!!!!!!!!!

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *buf);

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	struct sockaddr_in serv_adr, clnt_adr;
	socklen_t adr_sz;
	int str_len, i;
	char buf[BUF_SIZE];

	struct epoll_event *ep_events;	 // 为了 epoll_wait函数定义的变量指针
	struct epoll_event event;		// 为了epoll_ctl函数定义的结构体变量
	int epfd, event_cnt;

	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}

	serv_sock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_adr.sin_port=htons(atoi(argv[1]));
	
	if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
		error_handling("bind() error");
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");

	epfd=epoll_create(EPOLL_SIZE);	// 创建epoll例程文件描述符epfd
	// 为指针分配空间(大小为50*epoll_event大小),在epoll_wait函数中使用
	// 用来在缓冲中保存发生事件的文件描述符集合
	ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);	

	// event这个变量在epoll_ctl中使用
	event.events=EPOLLIN;	// 设置事件为:读取数据
	event.data.fd=serv_sock;	// 设置事件数据信息中,文件描述符为:serv_sock表示用于监视该描述符

	// epoll监视器设置:设置epfd监视器,添加监视对象:serv_sock,监视方式及相关信息:event
	epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

	while(1)
	{
		// 开启监视器epfd,将发生事件的套接字相关信息存入ep_events缓存中, 最大可存入事件数:50
		// -1:阻塞直至有事件发生
		event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
		if(event_cnt==-1)
		{
			puts("epoll_wait() error");
			break;
		}

		// 循环访问发生事件的文件描述符信息
		for(i=0; i<event_cnt; i++)
		{
			if(ep_events[i].data.fd==serv_sock)	// 缓存中的数据信息中 文件描述符为 : serv_sock时
			{									// 代表有listen函数接收到请求
				// 受理 连接请求
				adr_sz=sizeof(clnt_adr);
				clnt_sock=
					accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);

				// 继续添加监视对象!这里重复利用events变量进行设置,添加客户端文件描述符
				event.events=EPOLLIN;
				event.data.fd=clnt_sock;
				epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
				printf("connected client: %d \n", clnt_sock);
			}
			else
			{	// 缓冲数据中 ep_events[i].data.fd != serv_sock
				// 代表客户端套接字发生事件:需要接收数据
					str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);
					if(str_len==0)    // 若收到eof代表 连接结束,注销监视对象并关闭套接字
					{
						epoll_ctl(
							epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
						close(ep_events[i].data.fd);
						printf("closed client: %d \n", ep_events[i].data.fd);
					}
					else 			// 若未收到eof表示当前只是发送结束,并未断开连接,echo即可
					{
						write(ep_events[i].data.fd, buf, str_len);    // echo!
					}
	
			}
		}
	}
	close(serv_sock);	// 关闭服务器套接字
	close(epfd);		// 关闭epoll监视
	return 0;
}

void error_handling(char *buf)
{
	fputs(buf, stderr);
	fputc('\n', stderr);
	exit(1);
}

运行结果与之前一样,不展示了

17.2条件触发与边缘触发

epoll的核心之一就是这两个概念,我们需要真正理解这两个概念的含义。

17.2.1 条件触发与边缘触发的区别在于发生事件的时间点

首先我们看看什么是条件触发与边缘触发

  1. 条件触发方式中,只要输入缓冲有数据就会 一直通知该事件。
    例如:服务器端输入缓冲收到50字节数据,服务器端操作系统将通知该事件(注册到发生变化的文件描述符)。但是服务器读取20字节后还剩30字节,仍会再次注册时间。也就是说,条件触发方式中,只要输入缓冲中还剩数据,就将以时间方式再次注册。

  2. 边缘触发中输入缓冲收到数据时仅注册一次该事件。即使输入缓冲中还留有数据,也不会再进行注册。

17.2.2 条件触发的事件特性

首先我们要明白 条件触发与边缘触发的注册方式不同,导致其内部的工作内容不同。

epoll默认以条件触发方式工作,因此可以通过下面的示例验证够条件触发的特性。

echo_EPLTserv.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

#define BUF_SIZE 4					// 注意这里:使用的字符数组大小为4个字节
#define EPOLL_SIZE 50
void error_handling(char *buf);

int main(int argc, char *argv[])
{
	// 定义一些变量
	int serv_sock, clnt_sock;		// 服务器套接字 和 用于连接客户端的套接字(简称客户端套接字)
	struct sockaddr_in serv_adr, clnt_adr;	// 声明服务器和客户端地址结构体变量
	socklen_t adr_sz;				// accept函数要用
	int str_len, i;					
	char buf[BUF_SIZE];

	struct epoll_event *ep_events;	// 声明监视器参数变量,用于epoll_wait函数
	struct epoll_event event;		// 声明监视器参数变量,用于epoll_ctl函数
	int epfd, event_cnt;			// 声明监视器文件描述符 以及 变化的文件描述符个数(分别用于 epoll_create  epoLL_wait)

	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}

	// 初始化服务器端套接字
	serv_sock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_adr.sin_port=htons(atoi(argv[1]));
	
	// 分配服务器端套接字地址信息 并 listen请求
	if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
		error_handling("bind() error");
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");

	// epoll监视器初始化,返回epoll监视器文件描述符
	epfd=epoll_create(EPOLL_SIZE);
	// 为保存监视器中发生变化的文件描述符 分配缓冲空间:大小为:每个保存信息的结构体的大小 * 数量
	ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);

	// 设置event变量:填入servsock相关信息;向epfd监视器中添加serv_sock文件描述符,并使用event设置监视方式
	event.events=EPOLLIN;	// 监视方式为:需要读取数据(每当有数据来临时注册事件)
	event.data.fd=serv_sock;	
	epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);

	// 上面设置好了相关操作的变量,循环中开始监听,并根据监听得到的信息,判断如何进行处理
	while(1)
	{	
		// 开始监听:使用epfd监视器,将注册(发生)的事件信息存入ep_events指向的缓冲中,最大监听注册的事件数为:50,阻塞直至发生事件
		event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);

		// 监听期间发生了一些事件
		// 1. 监听发生错误: 退出大循环,不再继续监听
		if(event_cnt==-1)
		{
			puts("epoll_wait() error");
			break;
		}

		// 2. 监听正常:
		puts("return epoll_wait");
		// for循环查看每一个发生变化的事件,查看其缓冲中的信息。
		for(i=0; i<event_cnt; i++)
		{
			if(ep_events[i].data.fd==serv_sock)		// 如果当前缓冲中的第i个注册事件为:serv_sock相关事件,代表listen接收到了连接请求
			{	// 接受连接请求,并将clnt_sock添加到epfd监视器中
				adr_sz=sizeof(clnt_adr);
				clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
				// 设置event变量:填入clnt_sock相关信息;向epfd监视器中添加clnt_sock文件描述符,并使用event设置监视方式
				event.events=EPOLLIN;	// 监视方式为:可能需要读取数据时(每当有数据来临时注册事件)
				event.data.fd=clnt_sock;
				epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
				printf("connected client: %d \n", clnt_sock);
			}
			else 									// 如果当前缓冲中的第i个注册事件不为:serv_sock相关事件,代表只能是注册的客户端套接字发生了事件,也就是说有数据来临时注册事件,需要这边进行读取数据
			{
					str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);
					if(str_len==0)    // 客户端套接字接收到eof,也就是服务器断开了连接
					{	// 注销监视器epfd中的当前套接字,并关闭客户端套接字
						epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
						close(ep_events[i].data.fd);
						printf("closed client: %d \n", ep_events[i].data.fd);
					}
					else 				// 也就是str_len > 0 时,只要echo就行了
					{
						write(ep_events[i].data.fd, buf, str_len);    // echo!
					}
			}
		}
	}
	close(serv_sock);
	close(epfd);
	return 0;
}

void error_handling(char *buf)
{
	fputs(buf, stderr);
	fputc('\n', stderr);
	exit(1);
}

注意这里服务器端代码中,BUF_SIZE大小变为 4
同时,通过打印“return epoll_wait”字符串次数,判断调用该函数调用次数。

测试结果:
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

正如我们之前所说,条件触发方式下的epoll函数,在每次接收到客户端数据时,都会注册该事件(发生事件),并且多次调用epoll_wait函数。
(select函数也是以条件触发的方式工作的)

17.2.3 边缘触发的服务器端准备

在实现边缘触发服务器之前,需要搞定两点新东西:

  1. 通过errno变量验证错误原因
  2. 为了完成非阻塞(Non-blocking)i/o,更改套接字特性

1)
Linux套接字相关函数一般通过返回-1通知发生了错误,但是这样无法知道产生错误的原因。为了提供额外信息,Linux声明了如下全局变量: int errno;

为了访问该变量,需要头文件error.h,此头文件中有变量的extern声明。每种函数发生错误时,保存到errno中的值都是不不同的,我们这里只了解下面类型的错误:
“read函数发现输入缓冲中没有数据可读时,返回-1,同时在errno中保存EAGAIN常量。”

2)套接字改为非阻塞方式的方法。
使用fcntl函数
Linux提供更改或读取文件属性的如下方法:

#include <fcntl.h>

int fcntl(int filedes, int cmd, ...):
-> 成功时返回cmd参数相关值,失败时返回-1

filedes:属性更改目标的文件描述符
cmd:表示函数调用的目地

解释一下:fcntl具有可变参数的形式
如果向cmd中传入F_GETFL,可以获得filedes的属性(int型)。
如果向cmd中传入F_SETFL,可以更改filedes的属性(int型)。
因此,如果希望将文件(套接字)更改为非阻塞模式:

inf flag = fcntl(fd, F_GETFL, 0);	//获取属性
fcntl(fd, F_SETFL, flag|O_NONBLOCK);// 设置属性

为啥介绍这俩东西呢?因为与边缘触发的服务器端有关系呗~

  1. 在边缘触发方式中,接收数据时仅注册一次该事件。因此一旦发生输入相关事件,应该从输入缓冲中读取全部数据。因此需要验证输入缓冲是否为空:当read函数返回-1,变量errno中值为EAGAIN,说明没有数据可读
  2. 边缘触发方式下,以阻塞方式工作的read&write函数有可能引起服务器端的长时间停顿,因此边缘触发方式中一定要采用非阻塞read&write函数。

17.2.4 边缘触发的服务器端实现

echo_EPETserv.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>

#define BUF_SIZE 4
#define EPOLL_SIZE 50
void setnonblockingmode(int fd);	// 设置非阻塞模式
void error_handling(char *buf);

int main(int argc, char *argv[])
{
	// 声明变量
	int serv_sock, clnt_sock;				// 套接字相关
	struct sockaddr_in serv_adr, clnt_adr;
	socklen_t adr_sz;
	int str_len, i;
	char buf[BUF_SIZE];

	struct epoll_event *ep_events;			// epoll相关 具体说明见上面代码
	struct epoll_event event;
	int epfd, event_cnt;

	if(argc!=2) {
		printf("Usage : %s <port>\n", argv[0]);
		exit(1);
	}
	// 套接字地址初始化、分配地址、开始listen
	serv_sock=socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family=AF_INET;
	serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
	serv_adr.sin_port=htons(atoi(argv[1]));
	
	if(bind(serv_sock, (struct sockaddr*) &serv_adr, sizeof(serv_adr))==-1)
		error_handling("bind() error");
	if(listen(serv_sock, 5)==-1)
		error_handling("listen() error");
	// epoll创建监视器文件 epfd、 为epoll_wait函数中的存储分配缓冲
	epfd=epoll_create(EPOLL_SIZE);
	ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
	// 设置套接字非阻塞模式(更改其read&write方式)
	setnonblockingmode(serv_sock);
	// 填写event变量、添加服务器套接字进入epfd监视器中,监视方式为:有数据进入时(需要读取数据时)
	event.events=EPOLLIN;
	event.data.fd=serv_sock;	
	epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
	// 主循环:监视——》判断——》按情况处理
	while(1)
	{
		event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
		if(event_cnt==-1)	// 监视器错误时直接退出
		{
			puts("epoll_wait() error");
			break;
		}
		// 监视器正常时:for循环遍历存储着发生变化的缓冲空间
		puts("return epoll_wait");
		for(i=0; i<event_cnt; i++)
		{
			if(ep_events[i].data.fd==serv_sock);// 服务器套接字发生事件:listen工作,有请求(数据)流入。需要accept
			{
				adr_sz=sizeof(clnt_adr);
				clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
				setnonblockingmode(clnt_sock);
				event.events=EPOLLIN|EPOLLET;	// 设置监控方式为:有数据流入 或 边缘触发方式获得通知
				event.data.fd=clnt_sock;		// 添加客户端套接字进入监视器
				epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
				printf("connected client: %d \n", clnt_sock);
			}
			else
			{									// 客户端套接字发生事件:有数据接入且为边缘触发方式通知
					while(1)	// 由于采用了EPLLLET 只会通知一次,因此如果不加while在读取4个字节后,会在
					{			// epoll_wait阻塞
						str_len=read(ep_events[i].data.fd, buf, BUF_SIZE);
						if(str_len==0)    // close request!
						{
							epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
							close(ep_events[i].data.fd);
							printf("closed client: %d \n", ep_events[i].data.fd);
							break;
						}
						else if(str_len<0)	// 如果str_len<0 且 errno == EAGAIN:代表输入缓冲无数据
						{
							if(errno==EAGAIN)
								break;
						}
						else
						{
							write(ep_events[i].data.fd, buf, str_len);    // echo!
						}
				}
			}
		}
	}
	close(serv_sock);
	close(epfd);
	return 0;
}

void setnonblockingmode(int fd)
{
	int flag=fcntl(fd, F_GETFL, 0);			// 获取fd设置
	fcntl(fd, F_SETFL, flag|O_NONBLOCK);	// 更改fd设置
}
void error_handling(char *buf)
{
	fputs(buf, stderr);
	fputc('\n', stderr);
	exit(1);
}

测试结果:
在这里插入图片描述
在这里插入图片描述在这里插入图片描述在这里插入图片描述

17.2.5 边缘触发与条件触发孰优孰劣

边缘触发的优势:可以分离接收数据和处理数据的时间点!
从上面的代码中我们可能很难看出孰优孰劣,我们看下面这个例子:
在这里插入图片描述
我们希望从abc客户端分别接受数据,在服务器端进行整合处理,之后发送到其他主机。
为了完成,如果按照下面的流程运行,服务器端实现比较简单:

  1. 客户端按照abc顺序连接服务器,并依序向服务器发送数据
  2. 需要接收数据的客户端应在abc之前连接到服务器并等待

但是现实是:
3. c和b正在发送数据,可能a还没连上
4. abc发送数据顺序混乱
5. 服务器接收到数据,但是目标客户端没有连接到服务器

因此我们需要在输入缓冲接收到数据(注册相应事件)后,服务器也能决定读取或者处理这些数据的时间点,提升服务器灵活性。

然而条件触发也可以区分数据的接收和处理啊!!
但是!在输入缓冲收到数据的情况下,如果不读取数据(延迟处理),每次调用epoll_wait函数时都会产生相应事件!事件越来越多,服务器渐渐无法承受。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值