Linux网络编程6——poll和epoll

学习视频链接

04-poll函数实现服务器_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1iJ411S7UA?p=68&spm_id_from=333.1007.top_right_bar_window_history.content.click

目录

一、poll 函数

1.1 poll函数原型

1.2 流程

1.3 实现

1.4 优点和缺点

二、epell 函数

2.1 简介

2.2 文件描述符上限

2.3 API 

2.4 代码

三、epoll 事件模型

3.1 分类

3.2 查看他们之间的区别

3.3 网络中的两种模式


select 出来的比较早,其缺点是监听散乱的文件描述符效率会低一点。

所以使用 poll,但是 poll 效率没改进多少,所以又改进为 epoll

一、poll 函数

1.1 poll函数原型

Linux网络编程6——poll和epoll_第1张图片

1.2 流程

Linux网络编程6——poll和epoll_第2张图片

循环里面执行的就是 poll 监听 lfd 和所有的 cfd,如果是 lfd 就执行 accept,如果是 cfd 就执行 read/write

1.3 实现

1、代码讲解

进入死循环前,数组是一个这样的状态

Linux网络编程6——poll和epoll_第3张图片

if(client[0].revents & POLLIN) 是用来处理 listenfd

Linux网络编程6——poll和epoll_第4张图片

如果有新的连接,就会去在数组里面找空闲的位置 for(i=1;i

Linux网络编程6——poll和epoll_第5张图片

设置完成后就跳出循环,或者遍历到 1024 后跳出循环。如果遍历到 1024 会报错,没有便利到 1024 就会在刚刚修改 fd 的位置,再设置 events 等于 POLLIN 就完成了

for 循环是用来处理 cfd

Linux网络编程6——poll和epoll_第6张图片

上来先做一个异常的处理,保证代码健壮性 

if(Client[].revents & POLLIN) 为真,表示读事件满足了,就判断读的返回值

read 的返回值有以下这些 

Linux网络编程6——poll和epoll_第7张图片

如果返回值等于 -1,其实应该依次处理这些内容,但是函数中只对 ECONNRESET 这种情况进行了处理,其他的直接打印出错

如果返回值等于 0,就停用对应的文件描述符

如果返回值大于 0,就说明读到了数据,就进行处理

2、代码(代码有BUG,后面有机会再改)

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define MAXLINE 80
#define SERV_PORT 8000
#define OPEN_MAX 1024

void perr_exit(const char* str) 
{
	perror(str);
	exit(1);
}

int main(void)
{
	int i, j, maxi, listenfd, connfd, sockfd, nready, opt;
	ssize_t n;  // 接受poll返回值,记录满足监听事件的fd个数
	
	char buf[MAXLINE], str[INET_ADDRSTRLEN];
	socklen_t clilen;

	struct pollfd client[OPEN_MAX];
	struct sockaddr_in cliaddr, servaddr;

	listenfd = socket(AF_INET, SOCK_STREAM, 0);

	opt = 1;
	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(SERV_PORT);
	bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
	listen(listenfd, 128);

	client[0].fd = listenfd;   // 要监听的第一个文件描述符存入client[0]
	client[0].events = POLLIN; // listenfd监听普通读事件

	for (i = 1; i < OPEN_MAX; i++) {
		client[i].fd = -1;     // 用-1初始化client[]里剩下的元素
	}

	maxi = 0;

	for ( ; ; ) {
		nready = poll(client, maxi + 1, -1); // 阻塞监听是否有客户端链接请求

		if (client[0].revents & POLLIN) {     // listenfd有读事件就绪
			clilen = sizeof(cliaddr);
			connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);  // 接受客户端请求accept不会阻塞
			printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));

			for (i = 1; i < OPEN_MAX; i++) {
				if (client[i].fd < 0) {
					client[i].fd = connfd;  // 找到client[]中空闲的位置,存放accept返回的connfd
					break;
				}
			}

			if (i = OPEN_MAX) {        // 达到了最大客户端数
				perr_exit("too many clients");
			}

			client[i].events = POLLIN; // 设置刚刚返回的connfd,监控读事件
			if (i > maxi) {
				maxi = i;              // 更新client[]中最大元素下标
			}
			if (--nready <= 0) {
				continue;              // 没有更多就绪事件时,继续回到poll阻塞
			}
		}

		for (i = 1; i <= maxi; i++) {   // 前面的if没满足,说明没有listenfd满足,检测client[]看是哪个connfd就绪
			if ((sockfd = client[i].fd) < 0) {
				continue;
			}

			if (client[i].revents & POLLIN) {
				if ((n = read(sockfd, buf, MAXLINE)) < 0) {
					if(errno == ECONNRESET) { // 收到RET标志
						printf("client[%d] aborted connection\n", i);
						close(sockfd);
						client[i].fd = -1;    // poll中不监控该文件描述符,直接置为-1即可,不用像select中那样移除
					}
					else {
						perr_exit("read error");
					}
				}
				else if (n == 0) {  // 说明客户端先关闭链接
					printf("client[%d] closed connection\n", i);
					close(sockfd);
					client[i].fd = -1;
				}
				else {
					for (j = 0; j < n; j++) {
						buf[j] = toupper(buf[j]);
					}
					write(sockfd, buf, n);
				}
				if (--nready <= 0) {
					break;
				}
			}
		}
	}
	
	return 0;
}

1.4 优点和缺点

Linux网络编程6——poll和epoll_第8张图片

二、epell 函数

2.1 简介

epoll 是 Linux 下多路复用 IO 接口 select/poll 的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统 CPU 利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核 IO 事件异步唤醒而加入 Ready 队列的描述符集合就行了

目前 epoll 是 linux 大规模并发网络程序中的热门首选模型。

epoll 除了提供 select/poll 那种 IO 事件的电平触发 (Level Triggered) 外,还提供了边沿触发 (Edge Triggered),这就使得用户空间程序有可能缓存 lO 状态,减少 epoll_wait/epoll_pwait 的调用,提高应用程序效率。

2.2 文件描述符上限

1、可以使用 cat 命令查看一个进程可以打开的 socket 描述符上限。

cat /proc/sys/fs/file-max (当前计算机所能打开的最大文件个数,受硬件影响)

415bb479afca473f92aa95a57ccc7ea9.png

ulimit -a (当前用户下的进程,默认打开文件描述符个数)

Linux网络编程6——poll和epoll_第9张图片

2、如有需要,可以通过修改配置文件的方式修改该上限值。

sudo vi /etc/security/limits.conf

在文件尾部写入以下配置,soft 软限制,hard 硬限制。如下图所示。

* soft nofile 65536 

* hard nofile 100000

Linux网络编程6——poll和epoll_第10张图片

在这里修改不能超过 hard

 Linux网络编程6——poll和epoll_第11张图片

2.3 API 

前面只要一个函数就可以了,epoll 需要三个函数

第一个函数是 poll_create

Linux网络编程6——poll和epoll_第12张图片

第二个函数是 epoll_ctl 

Linux网络编程6——poll和epoll_第13张图片

第三个函数是 epoll_wait

Linux网络编程6——poll和epoll_第14张图片

数组中存储就是连接上的内容

Linux网络编程6——poll和epoll_第15张图片

2.4 代码

首先代码创建一个根结点,再创建 lfd 结点,用于监听,把 lfd 插入到根结点的子节点上。根据epoll_wait 返回的数字,循环遍历数组,处理读事件。判断读事件是否是请求联立连接,如果是请求连接的,就使用 epoll_ctl 插入结点,其他的就是数据读写事件。具体代码如下

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define MAXLINE 8192
#define SERV_PORT 8000
#define OPEN_MAX 5000

void perr_exit(const char* str) 
{
	perror(str);
	exit(1);
}

int main(void)
{
	int i, listenfd, connfd, sockfd, n;
	int num = 0;
	ssize_t nready, efd, res;
	char buf[MAXLINE], str[INET_ADDRSTRLEN];
	socklen_t clilen;

	struct sockaddr_in cliaddr, servaddr;
	struct epoll_event tep, ep[OPEN_MAX];

	listenfd = socket(AF_INET, SOCK_STREAM, 0);
	int opt = 1;
	setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));  // 端口复用
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(SERV_PORT);
	bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
	listen(listenfd, 20);

	efd = epoll_create(OPEN_MAX);  // 创建epoll模型,efd指向红黑书根节点
	if (efd == -1) {
		perr_exit("epoll_create error");
	}

	// 指定lfd的监听事件为 读
	tep.events = EPOLLIN;
	tep.data.fd = listenfd;

	res = epoll_ctl(efd, EPOLL_CTL_ADD, listenfd, &tep);  // 将lfd及对应的结构体设置到树上,efd可找到该树
	if(res == -1) {
		perr_exit("epoll_ctl error");
	}


	for ( ; ; ) {
		// epoll为server阻塞监听事件,ep为struct epoll_event类型数组,OPEN_MAX为数组容量,-1表永久阻塞
		nready = epoll_wait(efd, ep, OPEN_MAX, -1);
		if (nready == -1) {
			perr_exit("epoll_wait error");
		}

		for (i = 0; i < nready; i++) {
			if (!(ep[i].events & EPOLLIN)) {  // 如果并不是 读 事件,就继续循环
				continue;
			}

			if (ep[i].data.fd == listenfd) {  // 判断满足事件的fd是不是lfd
				clilen = sizeof(cliaddr);
				connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &clilen);  // 接受连接

				printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));
				printf("cdf %d--client %d\n", connfd, ++num);

				tep.events = EPOLLIN;
				tep.data.fd = connfd;
				res = epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &tep);  // 加入红黑树
				if (res == -1) {
					perr_exit("epoll_ctl error");
				}
			}
			else {  // 不是lfd
				sockfd = ep[i].data.fd;
				n = read(sockfd, buf, MAXLINE);

				if (n == 0) {  // 读到0,说明客户端关闭连接
					res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);  // 将该文件描述符从红黑树摘除
					if (res == -1) {
						perr_exit("epoll_stl error");
					}
					close(sockfd);  // 关闭与该客户端的连接
					printf("Client[%d] closed connection\n", sockfd);
				}
				else if (n < 0) {  // 出错
					perror("read n < 0 error");
					res = epoll_ctl(efd, EPOLL_CTL_DEL, sockfd, NULL);
					close(sockfd);
				}
				else {
					for (i = 0; i < n; i++) {     // 实际读到的字节数
						buf[i] = toupper(buf[i]); // 转大写,写会给客户端
					}
					write(STDOUT_FILENO, buf, n);
					write(sockfd, buf, n);
				}
			}
		}
	}
	close(listenfd);
	close(efd);
	
	return 0;
}

Linux网络编程6——poll和epoll_第16张图片

三、epoll 事件模型

3.1 分类

ET模式:边沿触发

缓冲区剩余未读尽的数据不会导致 epoll_wait 返回

event.events = EPOLLIN | EPOLLET;

LT模式:水平触发 —— 默认采用模式

缓冲区剩余未读尽的数据会导致 epoll_wait 返回

event.events = EPOLLIN

3.2 查看他们之间的区别

1、ET模式每 5 秒写一次,缓冲区中还有数据,但是不会去读数据,只有等到下次有10个字符数据来的时候才会继续读缓冲区后面 5 个字符的数据

Linux网络编程6——poll和epoll_第17张图片

#include 
#include 
#include 
#include 
#include 

#define MAXLINE 10

int main(void)
{
	int efd, i;
	int pfd[2];
	pid_t pid;
	char buf[MAXLINE], ch = 'a';

	pipe(pfd);
	pid = fork();

	if(pid == 0) {       // 子进程 写
		close(pfd[0]);
		while (1) {
			for (i = 0; i < MAXLINE / 2; i++) {
				buf[i] = ch;
			}
			buf[i-1] = '\n';
			ch++;
			for ( ; i < MAXLINE; i++) {
				buf[i] = ch;
			}
			buf[i-1] = '\n';
			ch++;
			write(pfd[1], buf, sizeof(buf));
			sleep(5);
		}
		close(pfd[1]);
	}
	else if (pid > 0) { // 父进程 读
		struct epoll_event event;
		struct epoll_event resevent[10];  // epoll_wait就绪返回event
		int res, len;

		close(pfd[1]);
		efd = epoll_create(10);

		event.events = EPOLLIN | EPOLLET; // ET边沿触发
		//event.events = EPOLLIN;          // LT水平触发(默认)
		event.data.fd = pfd[0];
		epoll_ctl(efd, EPOLL_CTL_ADD, pfd[0], &event);

		while (1) {
			res = epoll_wait(efd, resevent, 10, -1);
			printf("res %d\n", res);
			if (resevent[0].data.fd == pfd[0]) {
				len = read(pfd[0], buf, MAXLINE / 2);
				write(STDOUT_FILENO, buf, len);
			}
		}
		close(pfd[0]);		
	}
	
	return 0;
}

2、LT模式只要缓冲区中还有数据,就回去读数据,每次读 5 个字节的数据,一次性连续读两次。

Linux网络编程6——poll和epoll_第18张图片

//event.events = EPOLLIN | EPOLLET; // ET边沿触发
event.events = EPOLLIN;          // LT水平触发(默认)

3.3 网络中的两种模式

#include 
#include 
#include 
#include 

#define MAXLINE 10
#define SERV_PORT 9000

int main(void)
{
	struct sockaddr_in servaddr;
	char buf[MAXLINE];
	int sockfd, i;
	char ch = 'a';

	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 (1) {
		for (i = 0; i < MAXLINE / 2; i++) {
			buf[i] = ch;
		}
		buf[i-1] = '\n';
		ch++;
		for ( ; i < MAXLINE; i++) {
			buf[i] = ch;
		}
		buf[i-1] = '\n';
		ch++;
		write(sockfd, buf, sizeof(buf));
		sleep(5);
	}
	
	return 0;
}
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define MAXLINE 10
#define SERV_PORT 9000

int main(void)
{
	struct sockaddr_in servaddr, cliaddr;
	socklen_t cliaddr_len;
	int listenfd, connfd;
	char buf[MAXLINE];
	char str[INET_ADDRSTRLEN];
	int efd;

	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);
	bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
	listen(listenfd, 20);

	struct epoll_event event;
	struct epoll_event resevent[10];
	int res, len;

	efd = epoll_create(10);
	event.events = EPOLLIN | EPOLLET;  // ET边沿触发
	//event.events = EPOLLIN;          // 默认LT水平触发

	printf("Aceepting connections ...\n");

	cliaddr_len = sizeof(cliaddr);
	connfd = accept(listenfd, (struct sockaddr *)&cliaddr, &cliaddr_len);
	printf("received from %s at PORT %d\n", inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(str)), ntohs(cliaddr.sin_port));

	event.data.fd = connfd;
	epoll_ctl(efd, EPOLL_CTL_ADD, connfd, &event);

	while (1) {
		res = epoll_wait(efd, resevent, 10, -1);
		printf("res %d\n", res);
		if (resevent[0].data.fd == connfd) {
			len = read(connfd, buf, MAXLINE / 2);
			write(STDOUT_FILENO, buf, len);
		}
	}
	
	return 0;
}

如下图,在 ET 模式中,也是每次客户端发送过来信息,服务端才执行一次写操作

改成 LT 模式后,每次发送消息后,服务端先打印出 5 个字符,然后服务端再次触发 epoll_wait 返回,又会再次打印剩下的 5 个字符

// event.events = EPOLLIN | EPOLLET;  // ET边沿触发
event.events = EPOLLIN;          // 默认LT水平触发

3.4 结论:

epoll 的 ET 模式,高效模式,但是只支持非阻塞模式。

struct epoll_event event;

event.events = EPOLLIN | EPOLLET;

epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &event);

int flg = fcntl(cfd, F_GETFL);

flg |= O_ NONBLOCK;

fcntl(cfd, F_SETFL, flg);

优点:

高效。突破1024文件描述符。

缺点:

不能跨平台。Linux.

你可能感兴趣的:(linux,c语言)