【linux网络编程】epoll反应堆模型

1、原始的epoll模型

socket、bind、listen创建socket套接字--->epoll_create创建监听红黑树

--->返回监听文件红黑树文件描述符epfd--->epoll_ctl()向树上添加一个监听fd

--->while(1)--->epoll_wait监听--->对应监听fd有事件产生--->返回监听满足数组

--->判断返回数组元素--->lfd满足--->Accept--->cfd 满足--->read()--->小->大--->write回去。

2、epoll反应堆模型

epoll ET模式 + 非阻塞、轮询 + void *ptr。

不但要监听 cfd 的读事件、还要监听cfd的写事件。【自动回调】

socket、bind、listen创建socket套接字--->epoll_create创建监听红黑树

--->返回监听文件红黑树文件描述符epfd--->epoll_ctl()向树上添加一个监听fd

--->while(1)--->epoll_wait监听--->对应监听fd有事件产生--->返回监听满足数组

--->判断返回数组元素--->lfd满足--->Accept--->cfd 满足--->read()--->小->大

--->cfd从监听红黑树上摘下--->EPOLLOUT--->回调函数--->epoll_ctl函数

--->EPOLL_CTL_ADD重新放到红黑树上监听写事件--->等待epoll_wait返回

--->说明cfd可写--->write回去--->cfd从监听红黑树上摘下--->epoll_ctl

--->EPOLL_CTL_ADD重新放到红黑树上监听读事件--->epoll_wait监听。

反应堆的理解:加入IO转接之后,有了事件,server才去处理,这里反应堆也是这样,由于网络环境复杂,服务器处理数据之后,可能并不能直接写回去,比如遇到网络繁忙或者对方缓冲区已经满了这种情况,就不能直接写回给客户端。反应堆就是在处理数据之后,监听写事件,能写会客户端了,才去做写回操作。写回之后,再改为监听读事件。如此循环。

/*************************************************************************
 > File Name: epoll_reactor.c
 > Author: Winter
 > Created Time: 2022年07月08日 星期五 14时48分21秒
 ************************************************************************/

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

#define MAX_EVENTS 1024                  // 监听上限
#define BUF_LEN 4096                     // 缓冲区大小
#define SERVER_PORT 9527                 // 端口号

void recv_data(int fd, int events, void* arg);
void send_data(int fd, int events, void* arg);

// 就绪文件描述符相关信息
struct MyEvent {
	int fd;                                   				// 要监听的文件描述符
	int events;                               				// 对应的监听事件
	void* arg;                                				// 泛型参数
	void (*call_back)(int fd, int events, void* arg);       // 回调函数
	int status;      										// 是否监听,1表示在红黑树上,0表示不在
	char buf[BUF_LEN];                                      // 缓冲区
	int len;                                                // 缓冲区大小
	long last_active;                                       // 记录每次加入到红黑树g_efd的时间
};

// 两个全局变量
int g_efd;                                                  // 全局变量,保存红黑树的根节点,即epoll_create的返回值
struct MyEvent g_events[MAX_EVENTS + 1];                    // 自定义结构体类型数组 +1------>listen fd


// 将结构体MyEvent的成员变量初始化
void event_set(struct MyEvent* ev, int fd, void (*call_back)(int, int, void*), void* arg) {
	ev->fd = fd;
	ev->arg = arg;
	ev->events = 0;
	ev->call_back = call_back;
	ev->status = 0;
	memset(ev->buf, 0, sizeof(ev->buf));
	ev->len = 0;
	ev->last_active = time(NULL);
}

// 向epoll监听红黑树上添加一个文件描述符
void event_add(int efd, int events, struct MyEvent* ev) {
	struct epoll_event epv = {0, {0}};
	int op;
	epv.data.ptr = ev;
	epv.events = ev->events = events;       // events是EPOLLIN或者EPOLLOUT

	// 不在红黑树上的了
	if (ev->status == 0) {
		op = EPOLL_CTL_ADD;                    // 将其添加到红黑树上
		ev->status = 1;
	}

	// 实际添加/修改
	if (epoll_ctl(efd, op, ev->fd, &epv) < 0) {
		printf("event add falied [fd = %d], event[%d]\n", ev->fd, events);
	} else {
		printf("event add ok [fd = %d], op = %d, event[%0x]\n", ev->fd, op, events);
	}
	return;
}

// 从epoll监听红黑树上删除一个文件描述符
void event_del(int efd, struct MyEvent* ev) {
	struct epoll_event epv = {0, {0}};

	// 不在红黑树上
	if (ev->status == 0) {
		return;
	}
	epv.data.ptr = NULL;
	// 修改状态
	ev->status = 0;
	// 将ev->fd从红黑树上摘下
	epoll_ctl(efd, EPOLL_CTL_DEL, ev->fd, &epv);
	return;
}

// 有文件描述符就绪,epoll返回,调用该函数与客户端建立连接
void accept_conn(int lfd, int events, void* arg) {
	// 客户端地址结构
	struct sockaddr_in client_addr;
	socklen_t client_addr_len = sizeof(client_addr);
	int cfd, i;
	char client_ip[BUF_LEN];

	if ((cfd = accept(lfd, (struct sockaddr*)(&client_addr), &client_addr_len)) == -1) {
		if (errno != EAGAIN && errno != EINTR) {
			// 不做处理
		}
		printf("%s:accept:%s\n", __func__, strerror(errno));
		return;
	}

	do {
		// 找到全局数组中最小空闲元素
		for (i = 0; i < MAX_EVENTS; i++) {
			if (g_events[i].status == 0) {
				break;
			}
		}
		// 代码健壮性处理
		if (i == MAX_EVENTS) {
			printf("%s:max connect limit [%d]\n", __func__, MAX_EVENTS);
			break;
		}
		int flag = 0;
		// 将cfd设置成非阻塞
		if ((flag == fcntl(cfd, F_SETFL, O_NONBLOCK)) < 0) {
			printf("%s:fcntl nonblocking falied:%s\n", __func__, strerror(errno));
			break;
		}

		// 给cfd设置设置回调函数recv_data
		event_set(&g_events[i], cfd, recv_data, &g_events[i]);
		// 将读事件添加到红黑树上
		event_add(g_efd, EPOLLIN, &g_events[i]);
	}while (0);

	// 打印客户端信息
	printf("new connect[%s:%d][time:%ld], pos[%d]\n",
		inet_ntop(AF_INET, &client_addr.sin_addr.s_addr, client_ip, sizeof(client_ip)),
		ntohs(client_addr.sin_port), g_events[i].last_active, i);

	return;
}

// 接收数据
void recv_data(int fd, int events, void* arg) {
	struct MyEvent* ev = (struct MyEvent*)arg;

	// 读文件描述符,数据存储到MyEvent的buf中
	int len = recv(fd, ev->buf, sizeof(ev->buf), 0);
	// 将该节点从红黑树上摘下
	event_del(g_efd, ev);

	if (len > 0) {
		ev->len = len;
		ev->buf[len] = '\0';
		printf("client %d:%s\n", fd, ev->buf);

		// 设置该fd的回调函数是send_data
		event_set(ev, fd, send_data, ev);
		// 将写事件添加到红黑树上
		event_add(g_efd, EPOLLOUT, ev);
	} else if (len == 0) {
		close(ev->fd);
		printf("[fd = %d], pos[%ld], closed\n", fd, ev - g_events);
	} else {
		close(ev->fd);
		printf("recv[fd = %d], error[%d]: %s\n", fd, errno, strerror(errno));
	}
	return;
}


// 发送数据
void send_data(int fd, int events, void* arg) {
	struct MyEvent* ev = (struct MyEvent*)arg;
	// 直接将数据回写给客户端,未做处理
	int len = send(fd, ev->buf, ev->len, 0);
 
	// 从红黑树g_efd中删除
	event_del(g_efd, ev);
 
	if (len > 0) {
		printf("send [fd = %d], [%d]%s\n", fd, len, ev->buf);
		// 将该fd的回调函数改为reccv_data
		event_set(ev, fd, recv_data, ev);
		// 重新添加到红黑树上,设置为监听读事件
		event_add(g_efd, EPOLLIN, ev);
	} else {
		close(ev->fd);
		printf("send[fd = %d], error = %s\n", fd, strerror(errno));
	}
	return;
}


// 初始化监听socket
void init_listen_socket(int efd, short port) {
	// 服务器地址结构
	struct sockaddr_in server_addr;
	bzero(&server_addr, sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_port = htons(SERVER_PORT);
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);

	// 1创建socket
	int lfd = socket(AF_INET, SOCK_STREAM, 0);
	if (lfd == -1) {
		perror("socket error");
		exit(1);
	}
	// 将lfd设置成非阻塞
	int flag = fcntl(lfd, F_GETFL);
	flag = flag | O_NONBLOCK;
	fcntl(lfd, F_SETFL, flag);

	// 2绑定服务器地址结构
	int res = bind(lfd, (struct sockaddr*)(&server_addr), sizeof(server_addr));
	if (res == -1) {
		perror("bind error");
		exit(1);
	}

	// 3设置监听上限
	res = listen(lfd, 128);
	if (res == -1) {
		perror("bind error");
		exit(1);
	}

	// 为lfd设置回调函数accept_conn
	// 这里lfd是监听文件描述符
	event_set(&g_events[MAX_EVENTS], lfd, accept_conn, &g_events[MAX_EVENTS]);
	// 将EPOLLIN事件添加到红黑树上
	event_add(efd, EPOLLIN, &g_events[MAX_EVENTS]);
}


int main(int argc, char* argv[])
{
	unsigned short port = SERVER_PORT;
	// 使用用户指定的参数
	if (argc == 2) {
		port = atoi(argv[1]);
	}

	// 创建红黑树
	g_efd = epoll_create(MAX_EVENTS + 1);
	if (g_efd <= 0) {
		printf("create_epoll in %s error:%s\n", __func__, strerror(errno));
	}

	// 初始化监听socket
	init_listen_socket(g_efd, port);

	// 保存已经满足就绪事件的文件描述符数组
	struct epoll_event events[MAX_EVENTS + 1];
	printf("server running : port[%d]\n", port);

	int check_pos = 0, i;
	while (1) {
		// 超时验证,每次测试100个连接,不测试listenfd,当客户端60s没有与服务器通信,则关闭客户端连接
		long now = time(NULL);
		for (i = 0; i < 100; i++, check_pos++) {
			if (check_pos == MAX_EVENTS) {
				check_pos = 0;
			}

			// 不在红黑树上
			if (g_events[check_pos].status == 0) {
				continue;
			}
			// 客户端不活跃时间
			long duration = now - g_events[check_pos].last_active;
			if (duration >= 60) {
				// 关闭连接
				close(g_events[check_pos].fd);
				printf("[fd = %d], timeout\n", g_events[check_pos].fd);
				// 将该客户端从红黑树上删除
				event_del(g_efd, &g_events[check_pos]);
			}
		}

		// 监听红黑树g_efd将满足事件的文件描述符添加到events数组中,1s没有事件满足,就返回0
		int nfd = epoll_wait(g_efd, events, MAX_EVENTS + 1, 1000);
		if (nfd < 0) {
			printf("epoll_wait error,exit\n");
			break;
		}

		for (int k = 0; k < nfd; k++) {
			// 使用自定义结构体MyEvent类型指针,接收联合体data中的ptr成员
			struct MyEvent* ev = (struct MyEvent*)events[k].data.ptr;

			// 读事件
			if ((events[k].events & EPOLLIN) && (ev->events & EPOLLIN)) {
				ev->call_back(ev->fd, events[k].events, ev->arg);
			}

			// 写事件
			if ((events[k].events & EPOLLOUT) && (ev->events & EPOLLOUT)) {
				ev->call_back(ev->fd, events[k].events, ev->arg);
			}
		}
	}
	return 0;
}

笔者最近把epoll反应堆的代码看了几遍,有了新的感悟,整理一下思路。

(1)有一个自定义的结构体,里面有【监听文件描述符fd、对应的监听事件events、泛型参数arg、回调函数call_back、监听状态status、缓冲区buf、缓冲区大小len、记录每次加入到红黑树的时间】;两个全局变量【红黑树的根节点、自定义结构体数组】

这个结构体非常重要,回调函数的泛型参数是比较重要的信息。

(2)从主函数开始梳理代码思路:首先是处理端口port,再创建监听红黑树,到这里都好理解;接下来在主函数中调用的init_listen_socket这个函数,传参是红黑树的根节点g_efd和端口port,接下来分析init_listen_socket这个函数;

(3)这个函数直到调用event_set之前的代码都好理解,就是常规的创建监听文件描述符、将监听文件描述符设置成非阻塞的方式、绑定服务器地址结构、设置监听上限。到event_set函数就开始难理解了,先看event_set的原型:

// 将结构体MyEvent的成员变量初始化
void event_set(struct MyEvent* ev, int fd, void (*call_back)(int, int, void*), void* arg) {
	ev->fd = fd;
	ev->arg = arg;
	ev->events = 0;
	ev->call_back = call_back;
	ev->status = 0;
	memset(ev->buf, 0, sizeof(ev->buf));
	ev->len = 0;
	ev->last_active = time(NULL);
}

可见,这个函数是为自定义的结构体赋值的。再看init_listen_socket函数中event_set是怎么用的:

event_set(&g_events[MAX_EVENTS], lfd, accept_conn, &g_events[MAX_EVENTS]);

首先,类型上都没有问题。将结构体数组的最后一个数据的地址传入,将监听文件描述符lfd传入、回调函数是accept_conn,最后一个泛型参数也是自定义结构体的最后一个数据的地址(这很有意思)。下一句是代码是:

event_add(efd, EPOLLIN, &g_events[MAX_EVENTS]);

先啃event_add函数源码

// 向epoll监听红黑树上添加一个文件描述符
void event_add(int efd, int events, struct MyEvent* ev) {
	struct epoll_event epv = {0, {0}};
	int op;
	epv.data.ptr = ev;
	epv.events = ev->events = events;       // events是EPOLLIN或者EPOLLOUT

	// 不在红黑树上的了
	if (ev->status == 0) {
		op = EPOLL_CTL_ADD;                    // 将其添加到红黑树上
		ev->status = 1;
	}

	// 实际添加/修改
	if (epoll_ctl(efd, op, ev->fd, &epv) < 0) {
		printf("event add falied [fd = %d], event[%d]\n", ev->fd, events);
	} else {
		printf("event add ok [fd = %d], op = %d, event[%0x]\n", ev->fd, op, events);
	}
	return;
}

可以看出event_add函数是将events事件添加到监听红黑树efd上,

(4)分析accept_conn函数。这个函数到do语句执行之前也都好理解,即服务器阻塞监听客户端。看看do{}while(0)语句中的代码。首先是遍历自定义数组,找到全局最小空闲元素,数组越界处理(用户多了,终止程序,这是代码健壮性处理)、将通信文件描述符cfd设置成非阻塞方式,接下来又是event_set函数:

event_set(&g_events[i], cfd, recv_data, &g_events[i]);

将找到的最小空闲元素传入,处理事件是通信文件描述符cfd,回调函数是recv_data(看函数名是发送数据),泛型参数也是找到的最小空闲元素。把(3)、(4)画个图会看的更清楚

也就说:先在自定义数组下标最大的位置将监听文件描述符lfd及其回调函数accept_conn填入、而监听文件描述符lfd的回调函数accept_conn,先处理通信事件,得到通信文件描述符cfd,然后遍历自定义数据,找到数组最小的空闲元素,将通信文件描述符cfd及其回调函数recv_data填入。

那么,通信文件描述符cfd的功能是什么呢?

当时是从客户端读取数据了,即接收数据。

再将EPOLLIN事件添加到监听红黑树efd上

event_add(g_efd, EPOLLIN, &g_events[i]);

后面的代码是输出客户端的信息。

(5)分析recv_data函数代码。接收数据,再将文件描述符fd的回调函数改成send_data并将其挂到红黑树上,将事件设置成写事件

// 设置该fd的回调函数是send_data
event_set(ev, fd, send_data, ev);
// 将写事件添加到红黑树上
event_add(g_efd, EPOLLOUT, ev);

 即fd处理完接收数据后,要开始发送数据了。

(6)分析send_data函数代码。发送数据,发送成功后,再将文件描述符fd的回调函数改成recv_data并将其挂到红黑树上,将事件设置成读事件

// 将该fd的回调函数改为reccv_data
event_set(ev, fd, recv_data, ev);
// 重新添加到红黑树上,设置为监听读事件
event_add(g_efd, EPOLLIN, ev);

 即fd处理完发送数据后,要开始读取数据了。

如此往复循环。。。。。。

socket、bind、listen创建socket套接字--->epoll_create创建监听红黑树

--->返回监听文件红黑树文件描述符epfd--->epoll_ctl()向树上添加一个监听fd

--->while(1)--->epoll_wait监听--->对应监听fd有事件产生--->返回监听满足数组

--->判断返回数组元素--->lfd满足--->Accept--->cfd 满足--->read()--->小->大

--->cfd从监听红黑树上摘下--->EPOLLOUT--->回调函数--->epoll_ctl函数

--->EPOLL_CTL_ADD重新放到红黑树上监听写事件--->等待epoll_wait返回

--->说明cfd可写--->write回去--->cfd从监听红黑树上摘下--->epoll_ctl

--->EPOLL_CTL_ADD重新放到红黑树上监听读事件--->epoll_wait监听。

最终运行的时候,似乎只能有一次数据的传输??? 

  • 0
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值