网络IO管理


前言

网络IO模型讲解,包括原始的socket编程,多线程、select轮询、epoll以及reactor的网络编程。

一、原始socket编程

socket编程的流程具体可以看我的blog:https://editor.csdn.net/md/?articleId=131496108
原生的socket编程,其中的大部分接口都是阻塞的。
这里直接贴出代码:

nt main() {

// block
	int listenfd = socket(AF_INET, SOCK_STREAM, 0);  // 
	if (listenfd == -1) return -1;
// listenfd
	struct sockaddr_in servaddr;
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(9999);

	if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
		return -2;
	}
    printf("这里没有阻塞0;\n");
	listen(listenfd, 10);

#if 1
	// int 
	struct sockaddr_in client;
	socklen_t len = sizeof(client);
    printf("这里没有阻塞1;\n");
	int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);       // 默认是阻塞的
    printf("这里没有阻塞2;\n");
    int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
	if (ret == 0) {
		close(clientfd);
		break;
	}
	printf("buffer : %s, ret: %d\n", buffer, ret);
	ret = send(clientfd, buffer, ret, 0); // 
	```
若要将accept接口改为非阻塞的方式,在listen监听之前设置属性,修改代码为:
```javascript
#if 1 // nonblock
	int flag = fcntl(listenfd, F_GETFL, 0);
	flag |= O_NONBLOCK;
	fcntl(listenfd, F_SETFL, flag);
#endif

二、多线程socket

以上原生的socket代码出现的问题主要就是仅仅允许一个客户端连接,因此引入多线程的方法。
IO多线程基本的模型:
在这里插入图片描述

#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <unistd.h>
#include <pthread.h>
#define BUFFER_LENGTH	128

// thread --> fd
void *routine(void *arg) {

	int clientfd = *(int *)arg;

	while (1) {
		
		unsigned char buffer[BUFFER_LENGTH] = {0};
        // recv & send
		int ret = recv(clientfd, buffer, BUFFER_LENGTH, 0);
		if (ret == 0) {
			close(clientfd);
			break;
			
		}
		printf("buffer : %s, ret: %d\n", buffer, ret);

		ret = send(clientfd, buffer, ret, 0); // 

	}

}

// socket --> 
// bash --> execve("./server", "");
// 
// 0, 1, 2
// stdin, stdout, stderr
int main() {

// block
	int listenfd = socket(AF_INET, SOCK_STREAM, 0);  // 
	if (listenfd == -1) return -1;
// listenfd
	struct sockaddr_in servaddr;
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(9999);

	if (-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
		return -2;
	}

#if 0 // nonblock
	int flag = fcntl(listenfd, F_GETFL, 0);
	flag |= O_NONBLOCK;
	fcntl(listenfd, F_SETFL, flag);
#endif
    printf("这里没有阻塞0;\n");
	listen(listenfd, 10);

#if 1
	// int 
	struct sockaddr_in client;
	socklen_t len = sizeof(client);
    printf("这里没有阻塞1;\n");
	int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);       // 默认是阻塞的
    
	//printf("sendbuffer : %d\n", ret);
#else

	while (1) {
		struct sockaddr_in client;
		socklen_t len = sizeof(client);
		int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);
		
		pthread_t threadid;
		pthread_create(&threadid, NULL, routine, &clientfd);	// 需要执行的线程以及参数传递
		//fork();
	}
#endif

}

使用多线程的优势以及劣势:
优势:实现逻辑简单;
劣势:1.系统开销大;2.有数量限制

三、select轮询

  使用文件描述符集合来管理被监视的文件描述符.io是否有事件,其中的事件是 可读/可写。其中网络的fd 0 1 2是stdin、stdout以及stderr这三个最基本的fd句柄数字。本质:是一个IO管理组建
通过select函数依次轮询这些文件描述符的集合是否就绪,如果就绪则返回,否则阻塞。
相关api:
FD_ISSET:判断fd文件是否在数据集合中;
FD_SET:将fd文件添加到数据集合中;
FD_CLR:将fd文件从数据集合中删除;

while (1) {

		rset = rfds;    //
		wset = wfds;

		int nready = select(maxfd+1, &rset, &wset, NULL, NULL);     // 阻塞   rset 和 wset 集合中已经有文件描述符就绪,即至少有一个文件描述符可读或可写的时候返回
		if (FD_ISSET(listenfd, &rset)) {    // 检查读集合中是否设置 文件描述符listenfd

			printf("listenfd --> \n");

			struct sockaddr_in client;
			socklen_t len = sizeof(client);
			int clientfd = accept(listenfd, (struct sockaddr*)&client, &len);   // accept接受
			
			FD_SET(clientfd, &rfds);    // 设置可读事件进入 clientfd

			if (clientfd > maxfd) maxfd = clientfd;
		} 
		
		int i = 0;
		for (i = listenfd+1; i <= maxfd;i ++) {

			if (FD_ISSET(i, &rset)) {   //

				ret = recv(i, buffer, BUFFER_LENGTH, 0);
				if (ret == 0) {
					close(i);
					FD_CLR(i, &rfds);
					
				} else if (ret > 0) {
					printf("buffer : %s, ret: %d\n", buffer, ret);
					FD_SET(i, &wfds);   // 将文件描述符i添加进入可写事件中
				}
				
			} else if (FD_ISSET(i, &wset)) {          //
				
				ret = send(i, buffer, ret, 0);    // 发送buffer数据
				
				FD_CLR(i, &wfds);   // 清空wfds
				FD_SET(i, &rfds);
			}
		}

select 的优点

  1. 跨平台支持:select 在几乎所有操作系统上都有实现,具备较好的跨平台特性。
  2. 简单易用:使用简单,学习成本低,适用于简单的并发网络编程。
  3. 支持文件描述符集合:select 使用文件描述符集合来管理被监视的文件描述符

select 的缺点

  1. 性能问题:select 将监视的文件描述符集合从用户空间传递到内核空间,并且每次调用 select 都需要线性扫描整个文件描述符集合,效率较低。
  2. 限制连接数:select 的文件描述符集合大小有限,通常默认为 1024 或更小。
  3. 每次调用 select 都需要重新设置文件描述符集合,效率较低。

四、epoll事件通知

  高性能的事件驱动I/O(Input/Output)模型,用于处理大规模并发连接的网络编程。它通过提供一个事件驱动的接口,使得应用程序能够高效地等待多个文件描述符上的事件,并进行相应的处理。本质:高性能IO管理组件
模型图:
在这里插入图片描述

常用接口介绍:
eventpoll包括红黑树和双向链表,在内核态。
epoll_create:创建epoll实例,包括RB-tree(相当于红黑树的head节点)以及rbllist;
epoll_ctl: 向红黑树注册感兴趣的fd;
epoll_wait:轮询rbllist,存储的是就绪的fd,如果不为空则返回,将数据传送到应用层,否则阻塞。通过copy的方式

// fd --> epoll 
	int epfd = epoll_create(1);

	struct epoll_event ev, events[EVENTS_LENGTH];
	ev.events = EPOLLIN;
	ev.data.fd = listenfd; //

	epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); // 
	

	while (1) { // 7 * 24 

		int nready = epoll_wait(epfd, events, EVENTS_LENGTH, -1); // -1, ms 
		printf("------- %d\n", nready);
		
		int i = 0;
		for (i = 0;i < nready;i ++) {
			int clientfd= events[i].data.fd;
			
			if (listenfd == clientfd) { // accept

				//while(1) {
					struct sockaddr_in client;
					socklen_t len = sizeof(client);
					int connfd = accept(listenfd, (struct sockaddr*)&client, &len);
					if (connfd == -1) break;
					
					printf("accept: %d\n", connfd);
					ev.events = EPOLLIN;
					ev.data.fd = connfd;
					epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
				//}
			

			} else if (events[i].events & EPOLLIN) { //clientfd

				//char rbuffer[BUFFER_LENGTH] = {0};

				int n = recv(clientfd, rbuffer, BUFFER_LENGTH, 0);
				if (n > 0) {
					//rbuffer[n] = '\0';

					printf("recv: %s, n: %d\n", rbuffer, n);

					memcpy(wbuffer, rbuffer, BUFFER_LENGTH);

					ev.events = EPOLLOUT;
					ev.data.fd = clientfd;

					epoll_ctl(epfd, EPOLL_CTL_MOD, clientfd, &ev);
					
				} 
				
			} else if (events[i].events & EPOLLOUT) {

				
				int sent = send(clientfd, wbuffer, BUFFER_LENGTH, 0); //
				printf("sent: %d\n", sent);

				ev.events = EPOLLIN;
				ev.data.fd = clientfd;

				epoll_ctl(epfd, EPOLL_CTL_MOD, clientfd, &ev);
				
				
			}

		}

	}
	

五、Reactor反应堆网络模型

  本质:Reactor 逆置了事件处理流程,应用程序需要提供相应的接口并注册到 Reactor 上,如果相应的时间发生,Reactor 将主动调用应用程序注册的接口,这些接口又称为“回调函数”。
  中心思想:处理的 I/O 事件注册到一个中心 I/O 多路复用器上。一旦有 I/O 事件到来或是准备就绪(文件描述符或 socket 可读、写),多路复用器返回并将事先注册的相应 I/O 事件分发到对应的处理器中。
模型架构:
在这里插入图片描述

主函数:

int main(int argc, char *argv[]) {

	
	struct ntyreactor *reactor = (struct ntyreactor*)malloc(sizeof(struct ntyreactor));
	ntyreactor_init(reactor);

	unsigned short port = SERVER_PORT;
	if (argc == 2) {
		port = atoi(argv[1]);
	}

	int i = 0;
	int sockfds[PORT_COUNT] = {0};
	
	for (i = 0;i < PORT_COUNT;i ++) {
		sockfds[i] = init_sock(port+i);
		ntyreactor_addlistener(reactor, sockfds[i], accept_cb);     // 注册函数的流程
	}


	ntyreactor_run(reactor);

	ntyreactor_destory(reactor);
	
	for (i = 0;i < PORT_COUNT;i ++) {
		close(sockfds[i]);
	}
	free(reactor);
	

	return 0;
}

Reactor结构体以及初始化Reactor模型的步骤 :

struct ntyevent {
	int fd;         // 待操作的文件描述符
	int events;
	void *arg;
	int (*callback)(int fd, int events, void *arg);
	
	int status;
	char buffer[BUFFER_LENGTH];
	
	char wbuffer[BUFFER_LENGTH];
	
	int length;
	int wlength;
	//long last_active;

	// http reqeust
	int method;
	char resource[RESOURCE_LENGTH];

};

struct eventblock {
	
	struct eventblock *next;
	struct ntyevent *events;
};

struct ntyreactor {
	int epfd;       // epoll实例
	int blkcnt;		// 容量块

	struct eventblock *evblks;
};
// 初始化函数:
int ntyreactor_init(struct ntyreactor *reactor) {

	if (reactor == NULL) return -1;
	memset(reactor, 0, sizeof(struct ntyreactor));

	reactor->epfd = epoll_create(1);	// 创建epoll实例
	if (reactor->epfd <= 0) {
		printf("create epfd in %s err %s\n", __func__, strerror(errno));
		return -2;
	}

	struct ntyevent* evs = (struct ntyevent*)malloc((MAX_EPOLL_EVENTS) * sizeof(struct ntyevent));	//多个 eventitem,容量为MAX_EPOLL_EVENTS
	if (evs == NULL) {
		printf("create epfd in %s err %s\n", __func__, strerror(errno));
		close(reactor->epfd);
		return -3;
	}
	memset(evs, 0, (MAX_EPOLL_EVENTS) * sizeof(struct ntyevent));

	struct eventblock *block = malloc(sizeof(struct eventblock));
	if (block == NULL) {
		free(evs);
		close(reactor->epfd);
		return -3;
	}
	block->events = evs;
	block->next = NULL;

	reactor->evblks = block;
	reactor->blkcnt = 1;

	return 0;
}

注册函数,采用回调的方式:

int ntyreactor_addlistener(struct ntyreactor *reactor, int sockfd, NCALLBACK *acceptor) {

	if (reactor == NULL) return -1;
	if (reactor->evblks == NULL) return -1;

	struct ntyevent *event = ntyreactor_idx(reactor, sockfd);	// 根据sockfd的值找到对应分配的 event 数据
	if (event == NULL) return -1;
    // 以下是事件派发的逻辑   dispatch
	nty_event_set(event, sockfd, acceptor, reactor);	// event的 的设置   acceptor回调函数的注册
	nty_event_add(reactor->epfd, EPOLLIN, event);		// ADD EPOLLIN   调用epoll_ctl 首先

	return 0;
}

void nty_event_set(struct ntyevent *ev, int fd, NCALLBACK callback, void *arg) {

	ev->fd = fd;
	ev->callback = callback;
	ev->events = 0;
	ev->arg = arg;
	//ev->last_active = time(NULL);

	return ;
	
}

int nty_event_add(int epfd, int events, struct ntyevent *ev) {

	struct epoll_event ep_ev = {0, {0}};
	ep_ev.data.ptr = ev;
	ep_ev.events = ev->events = events;
    // 初始化的时候为0
	int op;
	if (ev->status == 1) {
		op = EPOLL_CTL_MOD;
	} else {
		op = EPOLL_CTL_ADD;
		ev->status = 1;     // 改为1
	}

	if (epoll_ctl(epfd, op, ev->fd, &ep_ev) < 0) {
		printf("event add failed [fd=%d], events[%d]\n", ev->fd, events);
		return -1;
	}

	return 0;
}

等待事件的发生:

int ntyreactor_run(struct ntyreactor *reactor) {
	if (reactor == NULL) return -1;
	if (reactor->epfd < 0) return -1;
	if (reactor->evblks == NULL) return -1;
	
	struct epoll_event events[MAX_EPOLL_EVENTS+1];
	
	int checkpos = 0, i;

	while (1) {

		int nready = epoll_wait(reactor->epfd, events, MAX_EPOLL_EVENTS, 1000);
		if (nready < 0) {
			printf("epoll_wait error, exit\n");
			continue;
		}

		for (i = 0;i < nready;i ++) {

			struct ntyevent *ev = (struct ntyevent*)events[i].data.ptr;

			if ((events[i].events & EPOLLIN) && (ev->events & EPOLLIN)) {   // 这两个判断是什么意思  第二个是事件item中的
				ev->callback(ev->fd, events[i].events, ev->arg);
			}
			if ((events[i].events & EPOLLOUT) && (ev->events & EPOLLOUT)) {
				ev->callback(ev->fd, events[i].events, ev->arg);
			}
			
		}

	}
}

accept、recv以及send均以回调的方式实现,这里就不贴出代码了,accept_cb的逻辑完成之后,再设置事件的回调函数为recv_cb,当recv_cb的逻辑完成后会更新event为send_cb依此往复。
epoll实现Reactor模型的流程总结:

  1. socket的流程;
  2. reactor数据成员的初始化;epoll_create 创建epfd
  3. 注册accept_cb回调函数;
  4. 注册socketefd到reactor中的epollfd实例;
  5. epoll_wait循环等待事件通知,+相关业务处理recv/send;
  6. 关闭文件描述符epfd、sockfd;清除reactor等内存;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值