epoll并发编程 and 多线程编程中的消息队列实现网络聊天室

1.epoll介绍

epoll是Linux操作系统中的一种I/O事件通知机制。它通过将文件描述符(如socket)注册到内核中的事件表中,然后监听事件的发生情况,一旦有事件发生,就通知用户程序。相比于传统的select和poll机制,epoll在处理大量并发连接时具有更高的性能和效率。

epoll的主要特点如下:

  1. 效率高:epoll使用了红黑树的数据结构来存储注册的文件描述符,可以快速地插入、删除和查找文件描述符。同时,epoll在等待事件时不会阻塞,而是将事件存储在内核空间中,用户程序可以通过epoll_wait等待事件的发生。

  2. 支持边缘触发和水平触发:epoll可以通过设置文件描述符的触发方式来决定何时通知用户程序。边缘触发模式只在状态变化时通知,而水平触发模式在状态保持时就会通知。

  3. 支持批量操作:epoll可以一次性返回多个文件描述符的事件,而不需要逐个遍历,减少了系统调用的开销。

  4. 支持大规模并发连接:epoll能够处理数以万计的并发连接,能够高效地处理大规模的网络应用。

总之,epoll是Linux系统中高性能网络编程的重要工具之一,它提供了高效的I/O事件通知机制,能够满足大规模并发连接的需求。

2.epoll实现

2.1 创建epoll

#include <sys/epoll.h>

int epoll_create(int size);
int epoll_create1(int flags);

epoll_create() 可以创建一个epoll实例。在linux 内核版本大于2.6.8 后,这个size 参数就被弃用了,但是传入的值必须大于0。

在 epoll_create () 的最初实现版本时, size参数的作用是创建epoll实例时候告诉内核需要使用多少个文件描述符。内核会使用 size 的大小去申请对应的内存(如果在使用的时候超过了给定的size, 内核会申请更多的空间)。现在,这个size参数不再使用了(内核会动态的申请需要的内存)。但要注意的是,这个size必须要大于0,为了兼容旧版的linux 内核的代码。

epoll_create() 会返回新的epoll对象的文件描述符。这个文件描述符用于后续的epoll操作。如果不需要使用这个描述符,请使用close关闭。

epoll_create1() 如果flags的值是0,epoll_create1()等同于epoll_create()除了过时的size被遗弃了。当然flasg可以使用 EPOLL_CLOEXEC,请查看 open() 中的O_CLOEXEC来查看 EPOLL_CLOEXEC有什么用。

返回值: 如果执行成功,返回一个非负数(实际为文件描述符), 如果执行失败,会返回-1,具体原因请查看error.

2.2 设置epoll事件

#include <sys/epoll.h>

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

这个系统调用能够控制给定的文件描述符epfd指向的epoll实例,op是添加事件的类型,fd是目标文件描述符。

有效的op值有以下几种:

  • EPOLL_CTL_ADD 在epfd中注册指定的fd文件描述符并能把eventfd关联起来。
  • EPOLL_CTL_MOD 改变*** fdevetn***之间的联系。
  • EPOLL_CTL_DEL 从指定的epfd中删除fd文件描述符。在这种模式中event是被忽略的,并且为可以等于NULL。

event这个参数是用于关联制定的fd文件描述符的。它的定义如下:

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

events这个参数是一个字节的掩码构成的。下面是可以用的事件:

  • EPOLLIN - 当关联的文件可以执行 read ()操作时。
  • EPOLLOUT - 当关联的文件可以执行 write ()操作时。
  • EPOLLRDHUP - (从 linux 2.6.17 开始)当socket关闭的时候,或者半关闭写段的(当使用边缘触发的时候,这个标识在写一些测试代码去检测关闭的时候特别好用)
  • EPOLLPRI - 当 read ()能够读取紧急数据的时候。
  • EPOLLERR - 当关联的文件发生错误的时候,epoll_wait() 总是会等待这个事件,并不是需要必须设置的标识。
  • EPOLLHUP - 当指定的文件描述符被挂起的时候。epoll_wait() 总是会等待这个事件,并不是需要必须设置的标识。当socket从某一个地方读取数据的时候(管道或者socket),这个事件只是标识出这个已经读取到最后了(EOF)。所有的有效数据已经被读取完毕了,之后任何的读取都会返回0(EOF)。
  • EPOLLET - 设置指定的文件描述符模式为边缘触发,默认的模式是水平触发。
  • EPOLLONESHOT - (从 linux 2.6.17 开始)设置指定文件描述符为单次模式。这意味着,在设置后只会有一次从epoll_wait() 中捕获到事件,之后你必须要重新调用 epoll_ctl() 重新设置。

返回值:如果成功,返回0。如果失败,会返回-1, errno将会被设置

有以下几种错误:

  • EBADF - epfd 或者 fd 是无效的文件描述符。
  • EEXIST - op是EPOLL_CTL_ADD,同时 fd 在之前,已经被注册到epoll中了。
  • EINVAL - epfd不是一个epoll描述符。或者fdepfd相同,或者op参数非法。
  • ENOENT - op是EPOLL_CTL_MOD或者EPOLL_CTL_DEL,但是fd还没有被注册到epoll上。
  • ENOMEM - 内存不足。
  • EPERM - 目标的fd不支持epoll。

2.3 等待epoll事件

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);
                      
int epoll_pwait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout,
                      const sigset_t *sigmask);

epoll_wait 这个系统调用是用来等待epfd中的事件。events指向调用者可以使用的事件的内存区域。maxevents告知内核有多少个events,必须要大于0.

timeout这个参数是用来制定epoll_wait 会阻塞多少毫秒,会一直阻塞到下面几种情况:

  1. 一个文件描述符触发了事件。
  2. 被一个信号处理函数打断,或者timeout超时。

timeout等于-1的时候这个函数会无限期的阻塞下去,当timeout等于0的时候,就算没有任何事件,也会立刻返回。

struct epoll_event 如下定义:

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_wait() 返回的时候,会包含用户在epoll_ctl中设置的events。

还有一个系统调用epoll_pwait ()。epoll_pwait()和epoll_wait ()的关系就像select()和 pselect()的关系。和pselect()一样,epoll_pwait()可以让应用程序安全的等待知道某一个文件描述符就绪或者捕捉到信号。

下面的 epoll_pwait () 调用:

ready = epoll_pwait(epfd, &events, maxevents, timeout, &sigmask);

在内部等同于:

pthread_sigmask(SIG_SETMASK, &sigmask, &origmask);
ready = epoll_wait(epfd, &events, maxevents, timeout);
pthread_sigmask(SIG_SETMASK, &origmask, NULL);

如果 sigmask为NULL, epoll_pwait()等同于epoll_wait()。

返回值:有多少个IO事件已经准备就绪。如果返回0说明没有IO事件就绪,而是timeout超时。遇到错误的时候,会返回-1,并设置 errno。

有以下几种错误:

  • EBADF - epfd是无效的文件描述符
  • EFAULT - 指针events指向的内存没有访问权限
  • EINTR - 这个调用被信号打断。
  • EINVAL - epfd不是一个epoll的文件描述符,或者maxevents小于等于0

3.代码解析

int main(void)
{
	/*初始化mysql*/
	con = mysql_init(con);
	MYSQL *mysql = mysql_real_connect(con, host, user, key, db, 0, NULL, 0);

	pthread_mutex_init(&mutex, NULL);
	if(mysql == NULL)
    {
        printf("connect err! 数据库连接失败! \n");
        return -1;
    }
	create(&head);

	int serv_fd, cli_fd;
	int epfd;                  //epoll句柄
	epfd = epoll_create(256);
	struct sockaddr_in serv_addr, cli_addr; //服务端客户端套接字地址
	struct epoll_event serv_ev;             //epoll事件结构体
	int cond;								//epoll_wait 的返回值	
	char readbuf[200] = {0};            //读取客户端发来的消息
	int flag;                            //判断recv返回值
	if( (serv_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
		my_err("socket", __LINE__);
	/*设置服务器结构*/
	memset(&serv_addr, 0, sizeof(struct sockaddr_in));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_port = htons(PORT);
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	socklen_t len = sizeof(struct sockaddr);
	if(bind(serv_fd, (struct sockaddr *)&serv_addr, len) < 0)
		my_err("bind", __LINE__);
	if(listen(serv_fd, 20) < 0)
		my_err("listen", __LINE__);
	
	/*给epoll中的事件赋值*/
	struct epoll_event max_ev[64];
	/*监听服务器*/
	serv_ev.events = EPOLLIN;
	serv_ev.data.fd = serv_fd;
	if(epoll_ctl(epfd, EPOLL_CTL_ADD, serv_fd, &serv_ev) < 0)
		my_err("epoll_ctl", __LINE__);
	int i, fd;
	int recv_length = 200, count = 0; // 设置接收长度200, 与发送长度一致,防止数据流混乱
	pool_init(10);
	while(1)
	{	
		if( (cond = epoll_wait(epfd, max_ev, 64, -1)) < 0)
			my_err("epoll_wait", __LINE__);
		for(i = 0; i < cond; i++)
		{
			fd = max_ev[i].data.fd;
			/*接收用户登录*/
			if(fd == serv_fd)
			{
				if( (cli_fd = accept(serv_fd, (struct sockaddr *)&serv_addr, &len)) < 0)
					my_err("accept", __LINE__);
				printf("连接成功\n");
				serv_ev.data.fd = cli_fd;
				serv_ev.events = EPOLLIN;
				if(epoll_ctl(epfd, EPOLL_CTL_ADD, cli_fd, &serv_ev) < 0)
					my_err("epoll_ctl", __LINE__);
			}
			/*处理用户发送的消息*/
			else
			{
				while( (flag = recv(fd, &readbuf[count], recv_length, 0)) )
				{
					count += flag;
					if(count == 200)
					 	break;
					else
						recv_length -= flag;
				}
				/*recv 返回0 套接字断开连接,将其从句柄中删除*/
				if(flag <= 0)
				{
					printf("%d用户断开连接\n", fd);
					rm_socket(fd);
					head = del_online(head, fd);
					epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &max_ev[i]);
					close(fd);            //关闭文件描述符
				}
				else
				{
					pool_add_worker((process), readbuf, fd);
				}
			}
			recv_length = 200;
			count = 0;
			memset(readbuf, 0, sizeof(readbuf));
		}
	}
	close(serv_fd);
}

1. 初始化MySQL连接,并检查连接是否成功。
2. 创建一个链表头节点。
3. 创建服务器和客户端套接字地址结构。
4. 创建一个Epoll句柄。
5. 创建服务器套接字,并绑定端口。
6. 监听服务器套接字。
7. 将服务器套接字添加到Epoll事件中。
8. 初始化一些变量,包括接收缓冲区大小和计数器。
9. 初始化线程池。
10. 进入主循环,等待Epoll事件的发生。
11. 处理Epoll事件,包括接收新的客户端连接和处理客户端发送的消息。
12. 如果接收到的消息长度达到200,表示接收完整,进行相应的处理。
13. 如果接收到的消息长度小于等于0,表示客户端断开连接,进行相应的清理工作。
14. 重置接收缓冲区和计数器。
15. 关闭服务器套接字。
这段代码实现了一个简单的多线程服务器,可以处理多个客户端的连接和消息。其中,Epoll技术使得服务器可以高效地处理多个并发连接,线程池则用于处理客户端发送的消息。

4.多线程编程中的消息队列


消息队列:是一个存储消息的容器,它允许一个进程将消息发送到队列,而另一个进程则可以从队列中接收消息;
消息:是进程之间传递的数据单元,可以是任意形式的结构体,包含发送者和接收者需要交换的信息。
队列标识符:消息队列由一个唯一的标识符来标识,进程可以使用这个标识符来打开特定的消息队列。

/*任务结构*/
typedef struct worker
{
	void (*process)(char *str, int socket);
	char str[200];
	int socket;
	struct worker *next;
}create_worker;
/*线程池结构*/
typedef struct
{
	pthread_mutex_t queue_mutex;
	pthread_cond_t queue_cond;
	/*链表结构,线程池中有所等待任务*/
	create_worker *queue_head;
	/*是否销毁线程池*/
	int shutdown;
	pthread_t *threadid;
	/*线程池中允许的活动线程数目*/
	int max_thread_num;
	/*当前等待队列的任务数目*/
	int cur_queue_size;
}create_pool;


static create_pool *pool = NULL;
/*创建线程池*/
void pool_init(int max_thread_num)
{
	pool = (create_pool *)malloc(sizeof(create_pool));
	pthread_mutex_init(&(pool->queue_mutex), NULL);
	pthread_cond_init(&(pool->queue_cond), NULL);
	pool->queue_head = NULL;
	pool->max_thread_num = max_thread_num;
	pool->cur_queue_size = 0;
	pool->shutdown = 0;
	pool->threadid = (pthread_t *)malloc(sizeof(pthread_t) * max_thread_num);
	for(int i = 0; i < max_thread_num; i++)
	{
		pthread_create(&pool->threadid[i], NULL, thread_routine, NULL);
	}
}

/*错误处理函数*/
void my_err(char *str, int line)
{
	fprintf(stderr, "line: %d\n", line);
	perror(str);
	exit(-1);
}

/*向线程池中加入任务*/
void pool_add_worker(void (*process) (char *str, int socket), char *str, int socket)
{
	/*构建一个新任务*/
	create_worker *newworker = (create_worker*)malloc(sizeof(create_worker));
	newworker->process = process;
	memcpy(newworker->str, str, 200);
	newworker->socket = socket;
	newworker->next = NULL;
	pthread_mutex_lock(&pool->queue_mutex);
	/*将任务加入到等待队列*/
	create_worker *member = pool->queue_head;
	if(member != NULL)
	{
		while(member->next != NULL)
			member = member->next;
		member->next = newworker;
	}
	else
		pool->queue_head = newworker;
	assert(pool->queue_head != NULL);
	pool->cur_queue_size++;
	pthread_mutex_unlock(&pool->queue_mutex);
	/*等待队列有任务,唤醒一个等待的线程*/
	pthread_cond_signal(&pool->queue_cond);
}
/*销毁线程池, 等待队列中的任务不会在执行,但是正在运行的线程一定会把任务运行完后在退出*/
int pool_destroy()
{
	if(pool->shutdown)
		return -1;		//防止两次调用	
	pool->shutdown = 1;
	/*唤醒所有的等待线程,线程池要销毁了*/
	pthread_cond_broadcast(&pool->queue_cond);

	/*阻塞等待线程退出,否则就成僵尸了*/
	for(int i = 0; i < pool->max_thread_num; i++)
		pthread_join(pool->threadid[i], NULL);      //等待所有线程执行完毕再销毁
	free(pool->threadid);
	/*销毁等待队列*/
	create_worker *head = NULL;
	while(pool->queue_head != NULL)
	{
		head = pool->queue_head;
		pool->queue_head = pool->queue_head->next;
		free(head);
	}

	/*销毁条件变量和互斥量*/
	pthread_mutex_destroy(&pool->queue_mutex);
	pthread_cond_destroy(&pool->queue_cond);
	free(pool);
	pool = NULL;
}
/*线程入口函数*/
void *thread_routine(void *arg)
{
	//printf("线程开始运行 %ld\n", pthread_self());
	while(1)
	{
		pthread_mutex_lock(&pool->queue_mutex);
		/*如果等待队列为0并且不销毁线程池,则处于阻塞状态*/
		while(pool->cur_queue_size == 0 && !pool->shutdown)
		{
			printf("thread: %ld is wait\n", pthread_self());
			pthread_cond_wait(&pool->queue_cond, &pool->queue_mutex);
		}
	//	printf("shutdown is: %d\n", pool->shutdown);
		/*线程池要销毁了*/
		if(pool->shutdown)
		{
			/*遇到break, continue, return 等跳转语句,要记得先解锁*/
			pthread_mutex_unlock(&pool->queue_mutex);
			printf("thread %ld will exit\n", pthread_self());
			pthread_exit(NULL);
		}
		printf("thread %ld is starting to work\n", pthread_self());
		
		assert(pool->cur_queue_size != 0);
		assert(pool->queue_head != NULL);
		/*等待队列长度减1,并去除链表中的头元素*/
		pool->cur_queue_size--;
		create_worker *work = pool->queue_head;
		pool->queue_head = work->next;
		pthread_mutex_unlock(&pool->queue_mutex);

		/*调用回调函数,执行任务*/
		(work->process) (work->str, work->socket);
		free(work);
		work = NULL;
	}
	//这一句应该不可以到达执行
	pthread_exit(NULL);
}

/*回调函数*/
void process(char *str, int socket)
{
	printf("str = %s\n", str);
	//printf("socket =  %d\n", socket);
	if(strncmp(str, "login:", 6) == 0)             //注册用户
		user_login(str, socket);

	else if(strncmp(str, "enter:", 6) == 0)        //登录
		user_enter(str, socket);

	else if(strncmp(str, "~history:", 9) == 0)     //查看聊天记录
		find_history(str, socket);


	else if(strncmp(str, "~", 1) == 0)		       //发送消息
		send_message(str, socket);

}

1. 首先,代码中定义了一个任务结构体 create_worker ,用于表示线程池中的任务。该结构体包含了一个函数指针 process ,用于指定任务的处理函数;一个字符串 str ,用于存储任务的数据;一个整数 socket ,表示任务相关的套接字;以及一个指向下一个任务的指针 next

2. 接着,代码定义了线程池结构体 create_pool ,用于管理线程池的状态和任务队列。该结构体包含了一个互斥锁 queue_mutex ,用于保护任务队列的访问;一个条件变量 queue_cond ,用于线程的阻塞和唤醒;一个指向任务队列头部的指针 queue_head ;一个表示线程池是否正在销毁的标志 shutdown ;一个整数 max_thread_num ,表示线程池中允许的最大线程数;一个整数 cur_queue_size ,表示当前等待队列中的任务数;以及一个指向线程ID数组的指针 threadid

3. 在 pool_init 函数中,线程池被初始化。这包括对互斥锁和条件变量进行初始化,创建指定数量的线程,并将线程ID保存在线程ID数组中。

4. my_err 函数是一个错误处理函数,用于打印错误信息并退出程序。

5. pool_add_worker 函数用于向线程池中添加任务。它首先创建一个新的任务节点,并将任务的处理函数、数据和套接字复制到节点中。然后,它通过互斥锁保护任务队列的访问,将新任务添加到队列末尾,并更新等待队列的任务数。最后,它通过条件变量通知等待的线程有新任务可用。

6. pool_destroy 函数用于销毁线程池。它首先将销毁标志设置为1,然后通过条件变量唤醒所有等待的线程。接下来,它等待所有线程执行完毕并释放资源。最后,它释放任务队列中的所有节点,并销毁互斥锁和条件变量。

7. thread_routine 函数是线程的入口函数。线程在一个无限循环中运行,首先通过互斥锁和条件变量等待任务的到来。当任务队列不为空且线程池未被销毁时,线程从任务队列中取出任务,并执行任务的处理函数。完成任务后,线程释放任务节点的内存,并继续等待下一个任务。

8. process 函数是一个回调函数,根据任务的内容执行相应的操作。在代码中,它根据任务的字符串内容执行不同的操作,例如用户登录、发送消息等。

这个线程池的实现可以方便地处理并发任务的执行。通过将任务添加到线程池中,线程池会自动分配线程来执行任务,并提供了线程安全的任务队列和同步机制,确保任务的顺序和并发执行的正确性。

5.运行效果

启动服务端

启动客户端

用户注册

用户聊天

源码地址

地址

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值