select、poll、epoll、多线程在多任务并发设计中的应用

文章目录

  • 前言
  • 一、回顾一下select
    • 1.1 高级IO模型
    • 1.2 select实现的缺点
  • 二、 多路复用IO-----poll和epoll
  • 三、poll和epoll的使用
    • 2.1 使用poll实现tcp多任务并发
    • 2.2 使用epoll实现tcp多任务并发
    • 2.2 使用多线程实现tcp多任务并发
  • 总结
  • 彩蛋--蛋--蛋


前言

本期主要的分享的是使用poll和epoll以及多线程在多任务并发中的应用,前面我们已经对使用select实现多任务并发已经有了初步认识,那么这期我们接着来看一下poll和epoll吧!


一、回顾一下select

1.1 高级IO模型

(1)阻塞IO
	效率高,等待数据过程中不占用CPU资源

(2)非阻塞IO
    能够解决多个文件描述符来数据的情况
	效率低,等待数据过程中CPU不断轮询所有文件描述符是否有数据
  
(3)异步IO
   当监听文件描述符有数据时,发送信号,收到信号接收数据
   只能应用在文件描述符比较少的情况下

(4)多路复用IO
   select
   
   poll
   
   epoll

1.2 select实现的缺点

缺点:
	(1)select监听文件描述符个数上限为1024
	(2)select监听的文件描述符集合在应用层,事件发时,内核数据需要传递给应用层,造成资源开销
	(3)select需要手动检测产生事件的文件描述符
	(4)select只能工作在水平触发模式(低速模式)

二、 多路复用IO-----poll和epoll

1.poll
	int poll(struct pollfd *fds, nfds_t nfds, int timeout);
	功能:
		监听事件表中是否有事件产生
	参数:
		fds:事件表结构体空间首地址
		nfds:事件表中事件的个数
		timeout:超时时间
			-1 永远阻塞等待
	返回值:
		成功返回时间产生事件的个数
		失败返回-1 
		超时没有事件发生返回0 
		
	struct pollfd {
	   int   fd;         /* file descriptor */				监听文件描述符
	   short events;     /* requested events */				监听事件
	   short revents;    /* returned events */				实际产生的事件 
    };

	缺点:
		1.poll监听应用层数据,内核监听事件需要通知用户层,引发资源开销
		2.poll需要手动检测产生事件的文件描述符
		3.poll工作在水平触发模式(低速模式)
		水平触发模式是一种低速触发模式,如果有这个文件描述符的消息,那么CPU就会一直通知这个文件描述符,直到接收到这个消息;
		边沿触发模式是一种高速触发模式,CPU只会通知一下,如果没有接收到CPU会去执行别的任务

2.epoll
(1)epoll_create
   int epoll_create(int size);
   功能:
		在内核中创建一张监听事件表
   参数:
		size:事件表中事件的个数
   返回值:
		成功返回新文件描述符
		失败返回-1 
		
(2)epoll_ctl
   int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
   功能:
		向事件表中添加(删除、修改)事件 
   参数:
		epfd:事件表文件描述符
		op:EPOLL_CTL_ADD	添加
		   EPOLL_CTL_MOD	修改
		   EPOLL_CTL_DEL	删除 
		fd:操作的文件描述符
		events:
			与文件描述符向关联的数据
			typedef union epoll_data {
			   void        *ptr;
			   int          fd;
			   uint32_t     u32;
			   uint64_t     u64;
			} epoll_data_t;

			struct epoll_event {
			   uint32_t     events;      /* Epoll events */			事件 
			   epoll_data_t data;        /* User data variable */	数据
			};
   返回值:
		成功返回0 
		失败返回-1 
   
(3)epoll_wait 
   int epoll_wait(int epfd, struct epoll_event *events,
                  int maxevents, int timeout);
   功能:
		监听事件表,并获得产生事件的信息存入到数组中
   参数:
		epfd:文件描述符
		events:存放事件的空间首地址
		maxevents:最多存放事件的个数
		timeout:超时时间 
   返回值:
		成功返回时间产生事件文件描述符个数
		失败返回-1
		如果时间到达仍没有事件发生返回0 

	优点:
		1.epoll不会受限于文件描述符个数
		2.epoll监听内核事件表,内核之间数据通信提升效率
		3.epoll不需要手动查找产生事件
		4.epoll可以工作在水平触发模式(低速)和边沿触发模式(高速)

三、poll和epoll的使用

2.1 使用poll实现tcp多任务并发

下面例子主要是使用poll实现的一个tcp网络服务器的多任务并发,代码如下:

#include "head.h"
#define MAX_FD_LEN		1024

//向监听的文件描述符集合中添加新的文件描述符
int AddFd(int confd, struct pollfd *pfd, int maxlen)
{
	int i = 0;

	for (i = 0;i < maxlen; ++i)
	{
		if (pfd[i].fd == -1)
		{
			break;				//找-1的位置,是空位置
		}
	}

	if (i == maxlen)
	{
		return -1;
	}
	
	pfd[i].fd = confd;
	pfd[i].events = POLLIN;

	return 0;
}

int main(int argc, const char *argv[])
{
	int sockfd = 0;
	struct sockaddr_in servaddr;
	int ret = 0;
	struct pollfd fds[MAX_FD_LEN];
	int i = 0;
	int nready = 0;
	int confd = 0;
	char tmpbuff[4096] = {0};

	for (i = 0;i < 1024;++i)	//将每一个文件描述符都置为-1(因为文件描述符都大于0,置为-1是为了标记这些都未被使用)
	{
		fds[i].fd = -1;
	}

	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == sockfd)
	{
		perror("fail to socket");
		return -1;
	}

	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(50000);
	servaddr.sin_addr.s_addr = inet_addr("192.168.43.242");
	ret = bind(sockfd, (struct sockaddr *)(&servaddr), sizeof(servaddr));
	if (-1 == ret)
	{
		perror("fail to bind");
		return -1;
	}


	ret = listen(sockfd, 1024);		//同时监听1024个用户请求
	if (-1 == ret)
	{
		perror("fail to listen");
		return -1;
	}

	fds[0].fd = sockfd;				//将sockfd添加到监听事件表中sockfd文件描述符
	fds[0].events = POLLIN;			//将其设置为POLLIN事件(表示文件描述符来数据)
	
	while (1)
	{
		nready = poll(fds, MAX_FD_LEN, -1);	//使用poll监听文件描述符,如果来事件,那么nready为产生事件的个数
		if (-1 == nready)
		{
			perror("fail to nready");
			return -1;
		}

		for (i = 0;i < MAX_FD_LEN; ++i)			//那么这里就突显出了poll的缺点,需要我们自己手动循环去检测发生事件的文件描述符
		{
			if (-1 == fds[i].fd)	//如果是-1,那么没有文件描述符,则继续检测下一个
			{
				continue;
			}

			if (fds[i].revents & POLLIN && fds[i].fd == sockfd)		//如果这个事件发生了并且是sockfd的POLLIN事件,那么进行三次握手链接
			{
				confd = accept(sockfd, NULL, NULL);
				if (-1 == confd)
				{
					perror("fail to accept");
					close(sockfd);				//关闭sockfd
					fds[0].fd = -1;				//将其位置置为-1
					continue;
				}
				AddFd(confd, fds, MAX_FD_LEN);	//像监听事件表中增加confd
			}

			else if (fds[i].revents & POLLIN && fds[i].fd != sockfd)	//如果这个事件发生了并且不是sockfd的而是别的文件描述符的POLLIN事件,那么进行功能处理
			{
				memset(tmpbuff, 0, sizeof(tmpbuff));
				ssize_t nsize = 0;
				nsize = recv(fds[i].fd, tmpbuff, sizeof(tmpbuff), 0);
				if (-1 == nsize)
				{
					perror("fail to recv");
					close(fds[i].fd);
					fds[i].fd = -1;
					continue;
				}
				else if (0 == nsize)
				{
					close(fds[i].fd);
					fds[i].fd = -1;
					continue;
				}
				printf("RECV:%s\n", tmpbuff);
				sprintf(tmpbuff, "%s------echo", tmpbuff);

				nsize = send(fds[i].fd, tmpbuff, strlen(tmpbuff), 0);
				if (-1 == nsize)
				{
					perror("fail to send");
					close(fds[i].fd);
					fds[i].fd = -1;
					continue;
				}
			}
		}
	}
	return 0;
}

以上呢就是所有的关于使用poll的TCP服务器端代码具体实现过程,在关键点处都进行了详细的标注,希望小伙伴们在不懂得地方停下来认真思考;
接下来也得写一个客服端来测试一下这个服务器,由于服务器端的功能比较单一,在这里简单介绍一下,不懂得小伙伴可以参见前几期网络编程的模块分享;客户端的功能:请求连接服务器—向服务器发送hello world ---------次数, 这个就是客户端的功能;

#include "head.h"

int main(int argc, const char *argv[])
{
	
	int sockfd = 0;
	struct sockaddr_in servaddr;
	int ret = 0;
	char tmpbuff[1024] = {0};
	int cnt = 0;
	ssize_t nsize = 0;

	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == sockfd)
	{
		perror("fail to sockfd");
		return -1;
	}

	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(50000);
	servaddr.sin_addr.s_addr = inet_addr("192.168.43.242");

	ret = connect(sockfd, (struct sockaddr *)(&servaddr), sizeof(servaddr));
	if (-1 == ret)
	{
		perror("fail to connect");
		return -1;
	}

	while (1)
	{
		memset(tmpbuff, 0, sizeof(tmpbuff));
		sprintf(tmpbuff, "hello world ---------%d", cnt++);
		nsize = send(sockfd, tmpbuff, strlen(tmpbuff), 0);
		if (-1 == nsize)
		{
			break;
		}
		
		memset(tmpbuff, 0, sizeof(tmpbuff));
		nsize = recv(sockfd, tmpbuff, sizeof(tmpbuff), 0);
		if (nsize <= 0)
		{
			break;
		}
		printf("%s\n", tmpbuff);

	}

	return 0;
}

2.2 使用epoll实现tcp多任务并发

下面这个例子呢旨在于使用epoll实现tcp多任务并发,这个模型也是从select—poll—epoll这样不断克服缺点进化而来的,所以epoll在实际应用中也是最为广泛的,下面来看一下使用epoll是如何实现tcp多任务并发的:

#include "head.h"
#define MAX_FD_LEN		1024	//最大监听文件描述符的个数

int AddFd(int epfd, int fd)
{
	struct epoll_event env;		//事件结构体
	int ret = 0;

	env.events = EPOLLIN;		//设置为EPOLLIN事件
	env.data.fd = fd;			//设置文件描述符
	ret = epoll_ctl(epfd, EPOLL_CTL_ADD,fd, &env);	//把fd增加到epfd的监听表中
	if (-1 == ret)
	{
		perror("fail to epoll_ctl");
		return -1;				//失败返回-1
	}
	return 0;					//成功返回0
}

int DelFd(int epfd, int fd)
{
	int ret = 0;

	ret = epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);	//把fd送epfd的这种表中删除
	if (-1 == ret)
	{
		perror("fail to epoll_ctl");
		return -1;
	}
	return 0;
}


int main(int argc, const char *argv[])
{
	int sockfd = 0;
	struct sockaddr_in servaddr;
	int ret = 0;
	int i = 0;
	int nready = 0;
	int confd = 0;
	char tmpbuff[4096] = {0};
	struct epoll_event retenv[MAX_FD_LEN];
	ssize_t nsize = 0;

	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == sockfd)
	{
		perror("fail to socket");
		return -1;
	}

	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(50000);
	servaddr.sin_addr.s_addr = inet_addr("192.168.43.242");
	ret = bind(sockfd, (struct sockaddr *)(&servaddr), sizeof(servaddr));
	if (-1 == ret)
	{
		perror("fail to bind");
		return -1;
	}


	ret = listen(sockfd, 1024);
	if (-1 == ret)
	{
		perror("fail to listen");
		return -1;
	}

	int epfd = 0;

	epfd = epoll_create(MAX_FD_LEN);		//在内核中创建一张监听事件表,个数为MAX_FD_LEN,返回值为这张事件表的文件描述符
	if (-1 == epfd)
	{
		perror("fail to epoll_create");
		return -1;
	}

	AddFd(epfd, sockfd);		//向事件表中添加sockfd文件描述符


	while (1)
	{
		nready = epoll_wait(epfd, retenv, MAX_FD_LEN, -1);	//监听事件表,并将收到的数据存放到数组中(数据是文件描述符标记的,意思就是先都接过来,再看是属于哪一个文件描述符的,再进行数据的进一步领取),返回值为产生事件的文件描述符的个数(也就是产生了nready个事件),接下来就要去接收数据的表中去处理nready次事件(对应下面的for循环)
		if (-1 == nready)
		{
			perror("fail to epoll_wait");
			return -1;
		}

		for (i = 0;i < nready;++i)			//做产生事件的文件描述符个数次循环
		{
			if (retenv[i].data.fd == sockfd)		//这个事件是sockfd事件
			{
				confd = accept(sockfd, NULL, NULL);
				if (-1 == confd)
				{
					perror("fail to accept");
					DelFd(epfd, sockfd);
					close(sockfd);
					continue;
				}
				AddFd(epfd, confd);
			}
			else if (retenv[i].data.fd != sockfd)	//不是sockfd的事件则是confd事件
			{
				memset(tmpbuff, 0, sizeof(tmpbuff));
				nsize = recv(retenv[i].data.fd, tmpbuff, sizeof(tmpbuff), 0);
				if (-1 == nsize)
				{
					DelFd(epfd, retenv[i].data.fd);
					close(retenv[i].data.fd);
					continue;
				}
				else if (0 == nsize)		//断开连接
				{
					DelFd(epfd, retenv[i].data.fd);
					close(retenv[i].data.fd);
					continue;
				}

				printf("RECV:%s\n", tmpbuff);
				sprintf(tmpbuff, "%s ------ echo", tmpbuff);
				nsize = send(retenv[i].data.fd, tmpbuff, strlen(tmpbuff), 0);
				if (-1 == nsize)
				{
					perror("fail to send");
					DelFd(epfd, retenv[i].data.fd);
					close(retenv[i].data.fd);
					continue;
				}
			}
		}
	}

	close(epfd);

	return 0;
}

以上就是使用epoll实现的tcp多任务并发的模型,这个模型也是非常实用的一个模型;那么下面再来看一下多线程实现多任务并发机制;

2.2 使用多线程实现tcp多任务并发

实现的原理其实就是每当有客户端发送链接请求时,那么服务器都会为其开辟一个单独的线程供服务器和客户端进行数据交互,以此实现多任务并发,每个线程属性分离,互不干涉;

#include "head.h"
#define MAX_FD_LEN		1024

void *threadfun(void *arg)
{
	int *pconfd = arg;
	char tmpbuff[4096] = {0};
	ssize_t nsize = 0;
	
	while (1)
	{
		memset(tmpbuff, 0, sizeof(tmpbuff));
		nsize = recv(*pconfd, tmpbuff, sizeof(tmpbuff), 0);
		if (-1 == nsize)
		{
			perror("fail to recv");
			close(*pconfd);
			return NULL;
		}
		else if (0 == nsize)
		{
			close(*pconfd);
			return NULL;
		}
		
		printf("RECV:%s\n", tmpbuff);

		sprintf(tmpbuff, "%s ------ echo", tmpbuff);
		nsize = send(*pconfd, tmpbuff, strlen(tmpbuff), 0);
		if (-1 == nsize)
		{
			perror("fail to send");
			close(*pconfd);
			return NULL;
		}
	}

}
int main(int argc, const char *argv[])
{
	int sockfd = 0;
	struct sockaddr_in servaddr;
	int ret = 0;
	int i = 0;
	int confd[1024] = {0};
	pthread_attr_t attr;
	pthread_t tid[1024];

	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == sockfd)
	{
		perror("fail to socket");
		return -1;
	}

	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(50000);
	servaddr.sin_addr.s_addr = inet_addr("192.168.43.242");
	ret = bind(sockfd, (struct sockaddr *)(&servaddr), sizeof(servaddr));
	if (-1 == ret)
	{
		perror("fail to bind");
		return -1;
	}


	ret = listen(sockfd, 1024);
	if (-1 == ret)
	{
		perror("fail to listen");
		return -1;
	}

	pthread_attr_init(&attr);
	pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);	//将线程设置为分离属性

	while (1)
	{
		confd[i] = accept(sockfd, NULL, NULL);	//循环进行三次握手链接请求
		if (-1 == confd[i])
		{
			perror("fail to accept");
			close(sockfd);
			return -1;
		}

		pthread_create(&tid[i], &attr, threadfun, &confd[i]);//以分离属性创建线程(线程不会阻塞)
	}

	pthread_attr_destroy(&attr);


	return 0;
}

总结

这三个的运行结果大家可以参见前几期分享的《使用select实现TCP并发服务器模型》一文,运行结果是完全一致的,那么到这里呢,tcp并发服务器模型在目前已经给大家介绍了四个了,所以大家可以用这个模型着手去做其他服务器多任务并发的设计,以此实现更加符合人们需求的产品!

彩蛋–蛋–蛋

以为就这么结束了吗,送大家一个彩蛋吧,多进程实现多任务并发设计,哈哈哈,来,继续学,学完再走:
这个程序的原理比较简单,每当有客户端请求时,那么服务器的主进程会进行三次握手链接,进而在子进程中进行相应的业务处理;创建子进程都是在主进程中实现的;所以多线程和多进程的缺点时会造成资源占用问题;

#include "head.h"
#define MAX_FD_LEN		1024

int main(int argc, const char *argv[])
{
	int sockfd = 0;
	struct sockaddr_in servaddr;
	int ret = 0;
	int i = 0;
	int confd = 0;
	pid_t pid;
	char tmpbuff[4096] = {0};
	ssize_t nsize = 0;

	sockfd = socket(AF_INET, SOCK_STREAM, 0);
	if (-1 == sockfd)
	{
		perror("fail to socket");
		return -1;
	}

	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(50000);
	servaddr.sin_addr.s_addr = inet_addr("192.168.43.242");
	ret = bind(sockfd, (struct sockaddr *)(&servaddr), sizeof(servaddr));
	if (-1 == ret)
	{
		perror("fail to bind");
		return -1;
	}


	ret = listen(sockfd, 1024);
	if (-1 == ret)
	{
		perror("fail to listen");
		return -1;
	}


	while (1)
	{
		confd = accept(sockfd, NULL, NULL);
		if (-1 == confd)
		{
			perror("fail to accept");
			close(sockfd);
			return -1;
		}

		pid = fork();
		if (-1 == pid)
		{
			perror("fail to fork");
			return -1;
		}
		if (0 == pid)
		{
			while (1)
			{
				memset(tmpbuff, 0, sizeof(tmpbuff));
				nsize = recv(confd, tmpbuff, sizeof(tmpbuff), 0);
				if (-1 == nsize)
				{
					perror("fail to recv");
					close(confd);
					return -1;
				}
				else if (0 == nsize)
				{
					close(confd);
					return -1;
				}

				printf("RECV:%s\n", tmpbuff);

				sprintf(tmpbuff, "%s ----- echo", tmpbuff);
				send(confd, tmpbuff, strlen(tmpbuff), 0);
			}
			exit(0);
		}
	}
	return 0;
}

最后,各位小伙伴们如果喜欢我的分享可以点赞收藏哦,你们的认可是我创作的动力,一起加油!

你可能感兴趣的:(日常小玩,数据库,sql,linux,tcp/ip,网络)