reactor的原理与实现


客户端跟服务器都有一个连接,客户端A与客户端B进行即时通信,A发数据给服务器,服务器通过listenfd,先判断fd有没有数据可读,如果有数据就读出来通过协议判断是发给谁的,然后发给相应的客户端,这就是即时通信。 随着连接越来越多,就会出现一些问题,例如许多io过来,其实只有少数需要处理,还有随着连接数的增多,sendbuff里边会装满了,就会导致不可写,所以send之前,得去判断是否可写。

在这里插入图片描述

大量的io而言,每个是否可读可写,所以需要让fd与相应的事件对应一个回调函数
reactor就是对io进行一个管理,对不同的io走不同的回调函数,与所谓的多进程多线程没关系,但是在做法上边,为了提高性能与逻辑,后边会慢慢地引入多线程的方法
在这里插入图片描述

一、reactor

reactor是一个框架模型,将对io事件(accept、read、write…)的监测(io多路复用技术实现监测)转换成对业务逻辑(网络编程关注的问题:连接的建立、断开、数据的到达、发送)的处理。reactor是一种对fd及其绑定的事件的集中处理的方法,本身和多线程无关,但是在实际应用中,可以根据实际需要在合适的地方引入多线程,也可以将监听和客户端连接的socket分别交给两个reactor反应器。
基于io,fd,socket在口语中是一个意思
组成:
非阻塞io(socket)+io多路复用技术(select、epio)
io函数是否阻塞取决于对应的socket是否设置为非阻塞的。(默认阻塞)
特征:
基于事件循环(有一个while(true)),以事件驱动或事件回调的方式来实现业务逻辑。
readable–>readcb
writeable–>wtrtecb

二、reactor的实现

当设置回调函数分别设置时,可以在同一次时间循环中处理所有事件。
一个反应器,管理所有需要监听的fd(item)
数据组织结构可以用效率更高的,epoll内部用的就是红黑树。
接口函数:
注册事件函数,该阶段事件为用户关心的网络编程的事件。
注销事件函数
启动事件循环,事件循环内部将io事件转换成用户关心的事件。
listenfd的作用,跟send和recv的fd的处理方法是不一样的,作用是tcp刚开始监听这个端口的时候,比如说访问百度的服务器,通过网页对外提供一个端口,这个listenfd是对外提供的,处理listenfd的只有accept这一个函数。listenfd有事件可读的时候,通过accept去处理,recv和send是处理不了的,因为这两个是要带数据出来的,accept再分配一个新的fd,所以只有listenfd是用accept。
这么多io,我们用什么数据结构去存储呢,accept返回一个fd会对应一个item存在,换句话说,我们用什么数据结构去存这些item比较好,有红黑树和链表,我们这里用链表来做

#include <errno.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#include <sys/poll.h>
#include <sys/epoll.h>
#include <pthread.h>
 
#define MAXLNE  4096

#define POLL_SIZE	1024

#define BUFFER_LENGTH 1024
#define MAX_EPOLL_EVENT 1024
typedef int NCALLBACK(int fd, int event, void *arg);//函数指针
//在这个里边需要考虑是否有fd即可读即可写,来判断写几个回调函数,函数其实几个或者一个都可以写
struct nitem{//数据结构:对应每个fd的item:
	int fd;
	int events;
	//int (*callback)(int fd, int event, void *arg);//
	NCALLBACK *readcb;//EPOLLIN//int (*readcb)(int fd, int event, void *arg);//
	NCALLBACK *writecb//int (*writecb)(int fd, int event, void *arg);//当前的回调函数,EPOLLOUT
	NCALLBACK *acceptcb//int (*acceptcb)(int fd, int event, void *arg);//处理listenfdEPOLLIN
	unsigned char sbuffer[BUFFER_LENGTH];//如果是字符的话我们一般设置成无符号char型
	int slength;//send的长度
	unsigned char rbuffer[BUFFER_LENGTH];
	int rlength;//recv的长度
};
//如果下边items[fd]的下标超过1024怎么办呢,我们可以定义一个这样的结构体,每一块存储1024个,如果fd超过1024,n=fd/1024,n等于几就执行几次next,就能找到对应的地方,链表
struct itemblock {
   struct itemblock *next;
	struct nitem *items;//items的数组
}struct reator {//用来存储epoll的fd,以及所有的item,epoll管理所有fd
	int epfd;
	struct itemblock *first;
	struct itemblock **last;//这是一个通用的写法,二级指针的原因是,*last指向的是链表最后那个节点,last指向的是next,last指向的是一个block,block的首地址的next,也就是*last指向的第一个的next
};
int init_reactor(struct reactor *r);
struct reactor *R;
struct reactor *instance = NULL;
struct reactor *getInastance(void) {//单例模式
	if(instance == NULL) {
		instance = malloc(sizeof(struct reactor));
		if (instance == NULL) return NULL;
		memset(instance, 0, sizeof(struct reactor));
		if (0 > init_reactor(insatance)) {
			return NULL;
		}
	}
	return instance;
}
int nreactor_del_event(int fd, NCALLBACK cb, int event, void *arg)
{
	struct reactor *r = getInstance();
	
	struct epoll_event ev = {0};
	ev.data.ptr = arg;

	epoll_ctl(r->epfd, EPOLL_CTL_DEL, fd, &ev);
	r->head->items[fd].events = 0;

	return 0;
}
int read_callback(int fd, int event, void *arg)//
{
	struct reactor *R = getInstance();
	unsigned char *buffer = R->head->items[fd].rbuffer;
	int ret = recv(fd, buffer, BUFFER_LENGTH, 0);
	if (ret == 0) { // 先delet再close
		nreactor_del_event(fd, NULL, 0, NULL);
		close(fd);
	} else if (ret > 0) {
		unsigned char *sbuffer = R->head->items[fd].sbuffer;
		memcpy(sbuffer, buffer, ret);
		R->head->items[fd].slength = ret;

		printf("readcb: %s\n", sbuffer);
		nreactor_set_event(fd, write_callback, WRITE_CB, NULL);
	}
}
int write_callback(int fd, int event, void *arg)//
{
	struct reactor *R = getInstance();
	unsigned char *sbuffer = R->head->items[fd].sbuffer;
	int length = R->head->items[fd].slength;
	int ret = send(fd, sbuffer, length, 0);

	if (ret < length) {//没有发送完,没有处理完
		nreactor_set_event(fd, write_callback, WRITE_CB, NULL);
	} else {
		nreactor_set_event(fd, read_callback, READ_CB, NULL);
	}
	return 0;
}
int accept_callback(int fd, int event, void *arg)//
{
	int connfd;
	struct sockaddr_in client;
	socklen_t len = sizeof(client);
	if ((connfd = accept(listenfd, (struct sockaddr *)&client, &len)) == -1) {
		printf("accept socket error: %s(errno: %d)\n", strerror(errno), errno);
		return 0;
	}
	nreactor_set_event(connfd, read_callback, READCB, NULL);
}
int init_server(int port)
{
	 int listenfd;
	 struct sockaddr_in servaddr;
    char buff[MAXLNE];
 
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(port);
 
    if (bind(listenfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) == -1) {
        printf("bind socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    if (listen(listenfd, 10) == -1) {
        printf("listen socket error: %s(errno: %d)\n", strerror(errno), errno);
        return 0;
    }
    return listenfd;
}
int init_reactor(struct reactor *r) {
	if (r==NULL) {
		return -1;
	} 
	int epfd = epoll_create(1);//创建epoll
	r->epfd = epfd;
	r->head = (struct itemblock *)malloc(MAX_EPOLL_EVENT*sizeof(struct itemblock));
	//接下来就要构建fd和这个item的关系,通过fd能够找到item,有人说nitem那个结构体不是有fd这个属性吗,请注意这个fd是nitem里边的,也就是我们处理的时候,epoll在处理里边带的fd返回event的fd的时候,也就是下边代码中int clientfd =  events[i].data.fd;这一行的时候,这个fd怎么找到对应的item在什么地方,我们用一个粗暴的方式,直接采用下标存储,直接用数组
	if(r->head == NULL) {
		close(epfd);
		return -2;
	}
	r->head->items = malloc(MAX_EPOLL_EVENT*sizeof(struct nitem));
	if (r->head->items == NULL) {
	   free(r->head);
	   close(epfd);
		return -2;
	}
	memset(r->head, 0, sizeof(struct itemblock));
	r->head->next = NULL;
	return 0;
}
#define READ_CB 0
#define WRITE_CB 1
#define ACCEPT_CB 2
//accept
int nreactor_set_event(int fd, NCALLBACK cb, int event, void *arg)//设置事件
{
	struct reactor *r = getInstance();
	struct epoll_event ev = {0};
	ev.data.ptr = arg;
	if (event == READ_CB) {
		r->head->items[fd].fd = fd;
		r->head->items[fd].readcb = cb;
		r->head->items[fd].arg = arg;
		ev.events = EPOLLIN;
	}else if (event == WRITE_CB) {
	r->head->items[fd].fd = fd;
		r->head->items[fd].writecb = cb;
		r->head->items[fd].arg = arg;
		ev.events = EPOLLout;
	} else if (event == ACCEPT_CB) {
		r->head->items[fd].fd = fd;
		r->head->items[fd].acceptcb = cb;
		r->head->items[fd].arg = arg;
		ev.events = EPOLLIN;
	}
	epoll_ctrl(r->epfd, fd, EPOLL_CTL_ADD, fd, &ev);
	return 0;
}
int reactor_loop(int listenfd)
{
	// 接下来我们要做的事情就是把listenfd给set进去,设置一个事件
	struct reactor *R = getInstance();
	struct epoll_event events[POLL_SIZE] = {0};
   while (1) {
		int nready = epoll_wait(R->epfd, events, POLL_SIZE, 5);
		if (nready == -1) {
			continue;
		}
		int i = 0;
		for (i = 0;i < nready;i ++) {
			int clientfd =  events[i].data.fd;
			//关于这里的if与else的添加,取决于之前struct nitem里边的回调函数定义,如果只有一个,那就是if else if,不存在同一个事件在多个函数处理。这里有三个回调,读和写有可能同时发生,但是listen不可能和读写同时发生的。现在三个一起处理是没有问题的,如果只有一个事件,就是else if
			if (clientfd == listenfd) {
				R->head->items[listenfd].acceptcb(listenfd, 0, NULL);
			} 
			if (events[i].events & EPOLLIN) {
				R->head->items[listenfd].readcb(clientfd, 0, NULL);
			}
			if(events[i].events & EPOLLOUT) {
				R->head->items[listenfd].writecb(clientfd, 0, NULL);
			}
		}
	}
	close(listenfd);
	return 0;
}
int main(int argc, char **argv) 
{
   int connfd, n;
   int listenfd = init_server(9999);//设置端口
   nreactor_set_event(listenfd, accept_callback, ACCEPTCB, NULL);
   
	reactor_loop(listenfd);
	return 0;
}

三、水平触发LT和边缘ET触发:

边缘触发是循环地去读数据,边缘触发效率高,毕竟每次系统调用返回复制数据(比如epoll_wait,复制rdlist)都是有一定开销的,一般使用边缘触发。边缘触发时内核只会在数据到达发送一次信号,因此用户空间(框架)需要循环调用read直到内核缓冲区数据全部取出,不然就取不到了。因此,边缘触发时一定要将socket设置为非阻塞的,因为最后一次读取的时候内核缓冲区可能是没有数据的,不能让它阻塞在这里。
水平触发只读一次,只要监听的文件描述符fd有数据,就会触发epoll_wait的返回值,这是默认的epoll_wait的方式,关于水平和边缘触发,后面会继续讨论

int read_callback(int fd, int event, void *arg) {
	struct reactor *R = getInstance();
	unsigned char *buffer = R->head->items[fd].rbuffer;
	
#if 0 //ET
	int idx = 0, ret = 0;
	while (idx < BUFFER_LENGTH) {
		ret = recv(fd, buffer+idx, BUFFER_LENGTH-idx, 0);
		if (ret == -1) { 
			break;
		} else if (ret > 0) {
			idx += ret;
		} else {// == 0
			break;
		}
	}
	if (idx == BUFFER_LENGTH && ret != -1) {
		nreactor_set_event(fd, read_callback, READ_CB, NULL);
	} else if (ret == 0) {
		nreactor_set_event
		//close(fd);
	} else {
		nreactor_set_event(fd, write_callback, WRITE_CB, NULL);
	}
#else //LT

	int ret = recv(fd, buffer, BUFFER_LENGTH, 0);
	if (ret == 0) { // fi
		nreactor_del_event(fd, NULL, 0, NULL);
		close(fd);
	} else if (ret > 0) {
		unsigned char *sbuffer = R->head->items[fd].sbuffer;
		memcpy(sbuffer, buffer, ret);
		R->head->items[fd].slength = ret;

		printf("readcb: %s\n", sbuffer);
		nreactor_set_event(fd, write_callback, WRITE_CB, NULL);
	}
		
#endif

总结

reactor是一种对fd及其绑定的事件的集中处理的方法,本身和多线程无关,但是在实际应用中,为了提高性能与逻辑,可以根据实际需要在合适的地方引入多线程。
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Reactor模式是一种事件驱动的设计模式,它的核心思想是将事件处理程序与I/O操作分离开来,通过事件分发器来处理所有的I/O事件,从而提高系统的可扩展性和可维护性。 Reactor模式的实现原理可以分为以下几个步骤: 1. 事件分发器:Reactor模式中的事件分发器负责监听所有的I/O事件,包括读写事件、连接事件和关闭事件等。一旦有事件发生,事件分发器会将事件分发给相应的事件处理程序进行处理。 2. 事件处理程序:事件处理程序是Reactor模式中的核心组件,它负责处理所有的I/O事件。每个事件处理程序都需要实现一个统一的接口,包括处理读写事件、连接事件和关闭事件等。 3. 事件处理器:事件处理器是一种通用的I/O操作处理器,它负责将I/O操作转化为事件,并将事件注册到事件分发器中。在Reactor模式中,所有的I/O操作都需要通过事件处理器进行注册和管理。 4. 多路复用器:多路复用器是一种高效的I/O事件处理机制,它可以同时处理多个I/O事件,从而提高系统的性能和可伸缩性。在Reactor模式中,多路复用器通常被用来监听所有的I/O事件,并将事件分发给相应的事件处理程序进行处理。 总的来说,Reactor模式的实现原理是基于事件驱动的设计思想,通过事件分发器、事件处理程序、事件处理器和多路复用器等组件来实现高效的I/O事件处理机制,从而提高系统的可扩展性和可维护性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值