Epoll+线程池+连接池

文章目录

  • 1.我的初衷
  • 2.Epoll介绍
  • 3.线程池的使用
  • 4.连接池的使用
  • 5.总结

1.我的初衷

我是一名大三的学生,哦 不下学期就大四了,小生是计算机科学与技术专业的(双非院校),老实说,从不后悔学这个专业,他让我明白了很多,见识到了很多东西,在这几年的学习生涯当中,在csdn很多前辈的博客上学习到了非常多的知识,每次学到新的东西,那种感觉就好像初恋一般(ps:虽然我没有),今天突然心血来潮,想在博客上发布一些关于自己的东西(其实很多都是从很多前辈那里学来的),算是对csdn 的一个感谢与支持吧!

那么这篇博客主要是针对我的一个项目:IM服务器的框架,(我也不知道算不算是框架),我真的是对社交这个领域挺感兴趣的,自己在这个项目上也花费了不少的时间,当然获益匪浅,不过很多东西自己可能还是体会的不是那么深刻,可能会存在一些错误的认识,还请各位前辈多多指教,评论区指出我的错误,非常感谢。O(∩_∩)O哈哈~,废话不多说了,我还是直接开讲吧!

2.Epoll介绍

想必在座的各位已经看过了无数关于介绍Epoll 的帖子,原作也好,转载也罢,大致都写的是一个东西,与select和poll 相比,他的性能高出千倍甚至万倍,省去了大量的用户空间和内核空间的拷贝,因为采用的是mmap(共享内存)这已经是ipc最快的方式了,另外采用了红黑树这一数据结构,在插入删除操作效率非常高,还有最重要的一点,它采用的不是轮询机制,他甚至直接将就绪的文件描述符返给你,真的是太贴心了,这么贴心的模型,要是不掌握一下,真的是对不起开发人员呀。对于他的一些函数,我就不一一赘述了,man手册里面有,这里我只针对我的项目去对一些功能函数做一下解析和介绍。

// 保存触发事件的某个文件描述符相关的数据

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
};

写过epoll 的朋友肯定对这两个结构体很熟悉,准确的说第一个是一个联合体,咱么先不展开深入研究,咱们先聊点别的,这算是打的一伏笔大多数人写的epoll 结构是这样的:

	while(1)
	{
			ready = epoll_wait(epfd,eparr,EPOLLSIZE,-1);
			if(ready == -1)
					perror("EPoll Call Failed..\n");
			else{
					while(ready){
							ready--;
							if(eparr[ready].data.fd == serverfd)	
							{
									int clientsize = sizeof(clientaddr);
									//建立新的链接
									clientfd = accept(serverfd,(struct sockaddr*)&clientaddr,&clientsize);
									eptmp.events = EPOLLIN;
									eptmp.data.fd = clientfd;
									epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&eptmp);
							}
							else
							{
									if((len = read(eparr[ready].data.fd,buf,sizeof(buf)))>0)
									{
										          //业务层处理
									}
									else if(len == 0)
									{
										  epoll_ctl(epfd,EPOLL_CTL_DEL,eparr[ready].data.fd,NULL);
											close(eparr[ready].data.fd);
									}
							}

					}
			}
	}

这样的代码结构真的非常好了,我第一次看到这种结构也是喜出望外,但是终究还是要回归到你的业务需求上面来,我的IM项目,我希望他可以使用两种传输协议(udp和tcp)比如说文字消息我就用udp传输,也就是说我希望epoll这棵树上不仅能tcp 描述符还能监听udp文件描述符,可能有的朋友说这还不简单,直接把udp文件描述符放到树上不就得了,监听方式和tcp 一样不就得了,就是少了个连接的过程(accpect),可是当时在写的时候,我就在想:假如短时间内有成千上百个用户发送文字消息,那我服务器该怎么读,读多大的缓冲区,就算内核缓冲区能装的下,我业务层该怎么做?着非常让我头疼,能不能像tcp 一样,每一个用户都有一个文件描述符,然后转发的业务,我直接去操作这个文件描述符不就得了,那么按照我一贯的脑回路:网上各种搜罗demo,终于呀我在github上找到一份,但是还不是我要的,他只是实现了udp 的并发,但是也帮了我的大忙,具体思路是这样的,像tcp 一样初始化一个udp文件描述符,作为监听socket,当此文件描述符触发事件时候,写一个伪连接,具体代码是这样的:

int udp_socket_connect(int epollfd,struct sockaddr_in  *servaddr)
{
	
	struct sockaddr_in my_addr, their_addr;
	int fd=socket(PF_INET, SOCK_DGRAM, 0);
	
	  /*设置socket属性,端口可以重用*/
   int opt=SO_REUSEADDR;
   setsockopt(fd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
   setnonblocking(fd);
   bzero(&my_addr, sizeof(my_addr));
   my_addr.sin_family = PF_INET;
   my_addr.sin_port = htons(myport);
   my_addr.sin_addr.s_addr = INADDR_ANY;
   if (bind(fd, (struct sockaddr *) &my_addr, sizeof(struct sockaddr)) == -1) 
   {
     perror("bind");
     exit(1);
   } 
   else
   {
     printf("IP and port bind success \n");
   }
	if(fd==-1)
		return  -1;
	connect(fd,(struct sockaddr*)servaddr,sizeof(struct sockaddr_in));
	add_event(epollfd,fd,EPOLLIN);
	return fd;
}

没有什么特别复杂的地方唯一需要解释的就是函数末尾用到的这个 connect,熟悉网络编程的朋友肯定知道,这个函数是tcp客户端用来连接服务器的,但是用在udp身上到底有什么用呢,我查了百度: 新创建的描述符和客户端进行绑定可以用来指明目的地址/端口;这将导致服务器只接受特定一个主机的请求。这真的是太好了,我就能像tcp一样去操作了,我接下来的问题就是如何区分tcp和udp监听描述符,这真的很棘手,这就要提到我那伏笔了:再粘一遍代码O(∩_∩)O哈哈~

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
};

凭借我这两年的编程经验,我第一眼就看到了这个void *ptr,我想让他来保存协议类型,但是我万万没想到的是他竟然是个联合体,只怪我眼瞎,我得意洋洋的改完之后去调试,就是不行,就是不行,我的天,当我看到union 这个字段时,我慢慢回忆起了C语言老师的哼哼教诲,原来是这么回事,共用体的所有成员的空间地址是一样的,也就是说只有一个成员起作用,原谅我的固执,在这停留了一天,那这怎么办,这里为啥是共用体,省那点空间有啥用,真的是,那没办法,直觉告诉我就是要利用到这个void *ptr,自己定义一个结构体:

typedef struct net_event
{
   int sockfd; //
   int protcol; //udp tcp 选用方式
}net_event;
这回没跑了吧,我们再来贴上我的关键代码段,这回来说就比较健全了:
		while(1)
		{      //改成for循环
			ready = epoll_wait(epfd,evearr,MAX_Epoll_Size,-1);
			for(int i=0;isockfd==serverfd)
					 {
						printf("tcp connect..\n");
						accept_client(epfd,serverfd);
					 }
					 else if(m_ev->sockfd==udpfd)
					 {
						 printf("udp connect...\n");
						 accept_udpclient(epfd,udpfd);
					 }
					 else
					 {
						if(m_ev->protcol==udp_protocol)
						{
							printf("push udp task...\n");
						}
						else
						{
							printf("push tcp task...\n");
						}
					 }				
				}
			}
		}
对了还有这个add_event()函数
static void add_event(int epollfd,int fd,int state)
{
	//在这里设置ptr
	net_event *m_data=(net_event*)malloc(sizeof(net_event));
	m_data->sockfd=fd;
	m_data->protcol=state;
    struct epoll_event ev;
    ev.events = EPOLLIN|EPOLLET;
	ev.data.ptr = (void*)m_data;
    epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
} 

我觉的我写的还是比较清晰的,但是肯定是存在漏洞就比如说为这个m_data申请的这个空间吧,我该怎么回收这个空间呢,不回收?内存泄漏o(╥﹏╥)o,要不就定义全局的,我内心上是不能接受的,真的,有的时候心理上觉得这种方法矛盾,真的就不会去尝试,那到底怎么办呀,又卡住了,不过我还是找到了解决办法:
这位老哥的办法,还是比较好的,给他点赞o( ̄▽ ̄)d good
https://blog.csdn.net/qq_33195791/article/details/81838444
他的思想就是利用epoll_data_t中的u64,前32为做为消息类型,后32位做文件描述符,主要就是这样:
ep_event.data.u64 = (uint64_t)(((uint64_t)(CallbackType) << 32) | (uint64_t)(int)sockfd);
真是神操作呀

3.线程池的使用

epoll 的问题基本上解决了,但是难道这就是我想要的并发吗?我看了很多的资料,发现,无论是select poll 还是神乎其神的epoll 他们的最大作用就是来监听事件的,我在一篇帖子上看见一个非常生动的描述:
监控来自10根不同地方的水管(I/O端口)是否有水流到达(即是否可读),那么需要10个人(即10个线程或10处代码)来做这件事。如果利用某种技术(比如摄像头)把这10根水管的状态情况统一传达到某一点,那么就只需要1个人在那个点进行监控就行了,而类似与select或epoll这样的多路I/O复用机制就好比是摄像头的功能,它们能够把多个I/O端口的状况反馈到同一处。
真正的实现并发 还是要靠多线程或者是多进程,哎~又要进行冗长的长篇大论了,别嫌弃我墨迹,我的语言表达能力很一般,我的初衷是希望当有事件触发的时候,就交给线程去做,就比如说,来了一个文字转发任务,我给他加入到任务队列之中,然后线程去队列中拿任务就好了,听上去真的特别美好,实际做起来,就不像那么简单了,这个东西,我肯定是要参考一下前辈们的代码,找来找去,大致都是一个思想,都是基于生产者消费者模型。
1.初始化任务队列
2.首先创建若干线程
3.生产者的任务:将任务放到任务队列之中,唤醒工作者。
4.生产者的任务:从队列之中取任务。
由于设计初衷是希望这个线程池可以动态的扩容和缩减,(可能我目前的业务需求是短业务,但是如果说涉及到长业务,例如文件上传之类的操作)这个就非常的nice了,我的设计是这样的在初始化线程池的时候,创建一个管理者,每隔一段时间去扫描忙的线程数和空闲的线程数,如果空闲的线程占比较大,我就杀死或者挂起一部分线程,反之我就在创建出一批线程出来。
在这里我就不占用太多篇幅了,我在这就直接贴上我的一个线程池是怎么应用的。
代码部分我还在后续更新(单独开一贴)

while(server running)
{
    epoll等待事件;
    if(新连接到达且是有效连接)
    {
        accept此连接;
        将此连接设置为non-blocking;
       为此连接设置event(EPOLLIN | EPOLLET ...);
        将此连接加入epoll监听队列;
        从线程池取一个空闲工作者线程并处理此连接;
    }
    else if(读请求)
    {
        从线程池取一个空闲工作者线程并处理读请求;
    }
    else
        其他事件;     
}

4.连接池的使用

哎呀终于写到这一步了,不容易呀,首先哈,这个linux配置mysql我就不写了哈 (放过我吧o(╥﹏╥)o)
配置成功之后,就可以直接调用API了,但是makefile里面要加上库mysql_config --cflags --libs(就是这个啦)
走到这一步真的挺不容易的,因为我的大部分阐述都是基于成果的,事实上我的这个IM 历经过两个版本之前的那个版本真的是low到爆炸,但是怎么说呢,也是自己的成果,还不舍得删,单线程跑的一个select 模型,业务层代码和select函数全都堆叠在一起来,一个函数2000多行,每次调一个小bug 都要半天,行了我不在这抱怨了,言归正传,因为我的这个项目要频繁的与数据库进行交互,刚开始那个select模型跑这些数据库查询业务没什么问题,但是换成epoll+多线程,问题就来了,我在调用查询的时候,老是提示我丢失连接,我真是醉了,查了很多资料,说是什么需要改mysql配置,但是改了也没什么卵用,我当时根本就没意识到多线程连接mysql的问题,网上还让我重新编译根目录下的那个文件,我没敢试,因为我牢记着一句话:大胆猜想,小心尝试,(之前我了解过一个聊天机器人,由于我的脑残操作,终端打不开了o(╥﹏╥)o),思来想去还剩一种方法,每次查询业务都都连接一次,但是资料上说每个连接也是需要一段时间的,具体我记不太清了,反正非常消耗性能,最后我我在一篇帖子上了解到:
两个线程不能同时在同一个连接上发送查询到MySQL。特别是你必须保证在一个mysql_query()和mysql_store_result()之间没有其他线程正在使用同一个连接(考虑连接池)
MySQL可以有多个连接接入,保证每一个单独的线程应该拿到的是单独的连接。建议使用线程池的同时,也使用数据库连接池,具体的实现流程这位前辈写的非常明白,我在这贴一张我的调用方法

  while(1)
		{      //改成for循环
			ready = epoll_wait(epfd,evearr,MAX_Epoll_Size,-1);
			for(int i=0;isockfd==serverfd)
					 {
						printf("tcp connect..\n");
						accept_client(epfd,serverfd);
					 }
					 else if(m_ev->sockfd==udpfd)
					 {
						 printf("udp connect...\n");
						 accept_udpclient(epfd,udpfd);
					 }
					 else
					 {
						if(m_ev->protcol==udp_protocol)
						{
							printf("push udp task...\n");
							pthread_arg m_arg;
	                        m_arg.clientfd=m_ev->sockfd;
                            m_arg.node=get_db_connect(sp); //取一个数据库连接  用完之后释放
							Producer(p,udpservice_job,(void*)&m_arg);
						}
						else
						{
							printf("push tcp task...\n");
       						pthread_arg m_arg;
                            m_arg.clientfd=m_ev->sockfd;
                            m_arg.node=get_db_connect(sp);
							Producer(p,service_job,(void*)&m_arg);	
						}
					 }				
				}
			}
		}

https://blog.csdn.net/qq_36359022/article/details/78771282#comments

5.总结

这篇算是我的处女作吧,也不能说是技术分享,顶多是心得,不知道为什么,写的时候,激情飞扬,觉的充实,写完的时候有一种怅然若失的感觉,不管怎么说,路还很长,内外兼修,但行好事,莫问前程,念念不忘,必有回响。。。

你可能感兴趣的:(网络io,linux,epoll,线程池)