C++实现简单的聊天室

C++ epoll和多线程实现简单多人聊天室

代码地址ChatRoom: 网络程序设计,聊天实验室代码 (gitee.com)

多路复用(Multiplexing)是一种通过一种机制实现同时监控多个文件描述符(sockets或文件等)的技术。在Linux系统中,epoll 是一种多路复用的机制,用于提高 I/O 操作的效率。epoll 相比于其他多路复用机制(如 selectpoll)在处理大量连接时有更好的性能表现。

epoll的主要函数

  1. epoll_create(创建 epoll 实例):

    int epoll_create(int size);
    
    • 用于创建一个 epoll 实例,并返回一个文件描述符。
    • 参数 size 是在内核中预分配用于存储事件的数量,但它不是硬性要求,实际上可以忽略。
  2. epoll_ctl(控制 epoll 实例上的事件):

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    
    • 用于在 epoll 实例中添加、修改或删除文件描述符上的事件。
    • epfdepoll 实例的文件描述符。
    • op 表示操作类型,可以是 EPOLL_CTL_ADD(添加事件)、EPOLL_CTL_MOD(修改事件)或 EPOLL_CTL_DEL(删除事件)。
    • fd 是目标文件描述符。
    • event 是一个结构体,用于指定事件的类型和其他信息。
  3. epoll_wait(等待文件描述符上的事件):

    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
    
    • 用于等待文件描述符上的事件发生。
    • epfdepoll 实例的文件描述符。
    • events 是用于存储事件信息的数组。
    • maxevents 表示最多返回的事件数量。
    • timeout 指定等待的超时时间,单位是毫秒。传递 -1 表示一直等待,传递 0 表示立即返回。
  4. struct epoll_event(事件结构体):

    struct epoll_event {
        uint32_t events;  // 表示事件的类型,如 EPOLLIN、EPOLLOUT 等
        epoll_data_t data;  // 用户数据,可以是文件描述符或指针
    };
    
    • events 字段指定感兴趣的事件类型,如可读、可写等。
    • data 字段可以存储用户定义的数据,通常是文件描述符或指针。

通过以上函数,可以在一个 epoll 实例上注册多个文件描述符,然后使用 epoll_wait 等待这些文件描述符上的事件发生,从而实现高效的 I/O 多路复用。 epoll 的设计使得它能够高效地处理大量的连接,因为它不会随着文件描述符数量的增加而线性增加开销。

epoll的主要工作模式

epoll 提供两种工作模式:边沿触发(Edge Triggered)和水平触发(Level Triggered)。这两种模式定义了在何种情况下 epoll_wait 函数会通知应用程序文件描述符上的事件。

  1. 边沿触发(Edge Triggered):
    • 在边沿触发模式下,epoll_wait 仅在文件描述符状态变化时通知应用程序,而不是在文件描述符就绪的整个时间段内都通知。
    • 一旦文件描述符上有新的数据可读或可写,epoll_wait 就会返回,并且应用程序需要处理这些事件直到没有更多的事件为止。
    • 边沿触发要求应用程序一次性处理完文件描述符上的事件,否则可能会错过某些事件。
  2. 水平触发(Level Triggered):
    • 在水平触发模式下,epoll_wait 会在文件描述符就绪的整个时间段内通知应用程序。
    • 一旦文件描述符上有新的数据可读或可写,epoll_wait 就会返回,应用程序可以处理这些事件,然后继续等待下一次通知。
    • 水平触发允许应用程序在多次 epoll_wait 调用之间保留对文件描述符的就绪状态的感知。

在使用 epoll 时,可以通过 struct epoll_event 结构体中的 events 字段设置触发模式。相关的标志包括:

  • EPOLLIN:表示文件描述符上有数据可读。
  • EPOLLOUT:表示文件描述符上有空间可写。
  • EPOLLRDHUP:表示对端关闭连接,或者半关闭连接。
  • EPOLLHUP:表示发生挂起事件。
  • EPOLLET:表示使用边沿触发模式。

例如,使用边沿触发的 epoll_ctl 调用可以如下设置:

event.events = EPOLLIN | EPOLLET;

而使用水平触发的 epoll_ctl 调用可以如下设置:

event.events = EPOLLIN;

在实际应用中,选择使用边沿触发还是水平触发通常取决于应用程序的需求和设计。边沿触发模式可以减少事件通知的次数,但要求应用程序及时处理所有事件。水平触发模式相对更容易使用,因为它允许应用程序在多次 epoll_wait 调用之间保留对文件描述符的就绪状态的感知。

epoll版本Server

服务器端的代码主要思路是接受一个客户端的信息后,会把信息转发给其他的所有客户端,从而实现多人聊天

//Server
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <vector>
#include <algorithm>

#define BUFFER_SIZE 1024
#define CLIENT_SIZE 1024


std::vector<int> clients;


int main()
{
	printf("Server:\n");
	//2. 创建socket
	int serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (serverSocket == -1)
	{
		perror("socket error");
		exit(1);
	}

	printf("socket success\n");

	//3. 创建地址协议族
	struct sockaddr_in addr;
	addr.sin_family = AF_INET; //ipv4
	addr.sin_addr.s_addr = inet_addr("192.168.179.128");
	addr.sin_port = htons(10086); //�����ֽ���--->�����ֽ��� 

	//4. 绑定
	int r = bind(serverSocket, (sockaddr*)&addr, sizeof(addr));
	if (r == -1)
	{
		perror("bind");
		exit(1);
	}
	printf("bind success\n");

	//5. 监听
	r = listen(serverSocket, 10);
	if (r == -1)
	{
		perror("listen");
		exit(1);
	}
	printf("listen success\n");

	//创建epoll
	int epld = epoll_create1(0);
	if(epld < 0)
	{
		perror("epoll");
		exit(1);
	}
	printf("epoll success\n");

	struct epoll_event ev;
	ev.events = EPOLLIN;
	ev.data.fd = serverSocket;

	int ret = epoll_ctl(epld, EPOLL_CTL_ADD, serverSocket, &ev);
	if(ret < 0)
	{
		perror("epoll_ctl");
		exit(1);
	}
	printf("epoll_ctl success\n");


	while (1)
	{
		struct epoll_event evs[CLIENT_SIZE];
		int n = epoll_wait(epld, evs, CLIENT_SIZE, -1);
		if(n < 0)
		{
			perror("epoll_wait");
			exit(1);
		}

		for(int i=0; i<n; i++)
		{
			int fd = evs[i].data.fd;
			//如果是监听的fd收到消息,那么表示有客户端进行连接
			if(fd == serverSocket)
			{
				struct sockaddr_in cAddr = { 0 };
				socklen_t len = sizeof(cAddr);
				int client_sockfd = accept(serverSocket, (sockaddr*)&cAddr, &len);
				if(client_sockfd < 0)
				{
					perror("accept");
					exit(1);
				}

				ev.events = EPOLLIN;
				ev.data.fd = client_sockfd;
				ret = epoll_ctl(epld, EPOLL_CTL_ADD, client_sockfd, &ev);
				if(ret < 0)
				{
					perror("epoll_ctl");
					exit(1);
				}

				printf("Client %lu connected...\n", clients.size());

				clients.push_back(client_sockfd);
			}
			else//如果是客户端有消息
			{
				char recv_buff[BUFFER_SIZE];
				memset(recv_buff, 0, BUFFER_SIZE);
				int r = recv(fd, recv_buff, sizeof(recv_buff), 0);
				int idx = std::find(clients.begin(), clients.end(), fd) - clients.begin();

				if (r > 0)
				{
					printf("Clinet %d -----> Server : %s\n",idx,  recv_buff);
				}

				//广播给所有client
				for (int i = 0; i < clients.size(); i++)
				{
					if (i != idx)
					{
						send(clients[i], recv_buff, sizeof(recv_buff), 0);
					}
				}

				// 输入quit退出
				if (strncmp(recv_buff, "quit", 4) == 0 || r <= 0)
				{
					clients.erase(std::find(clients.begin(), clients.end(), fd));
					close(fd);
					epoll_ctl(epld, EPOLL_CTL_DEL, fd, 0);
					printf("Clinet %d 退出聊天\n", idx);
				}
			}
		}
	}


	close(serverSocket);
	return 0;
}

多线程版本Server

//Server.cpp
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <thread>


#define BUFFER_SIZE 1024
#define CLIENT_SIZE 1024

int clientSocket[CLIENT_SIZE];
int count = 0;

void recv_func(int idx);

int main()
{
	printf("Server:\n");
	//2. 创建socket
	int serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (serverSocket == -1)
	{
		perror("socket");
		exit(1);
	}

	//3. 创建地址协议族
	struct sockaddr_in addr;
	addr.sin_family = AF_INET; //ipv4
	addr.sin_addr.s_addr = inet_addr("192.168.179.128");
	addr.sin_port = htons(10086); //�����ֽ���--->�����ֽ��� 

	//4. 绑定
	int r = bind(serverSocket, (sockaddr*)&addr, sizeof(addr));
	if (r == -1)
	{
		perror("bind");
		exit(1);
	}

	//5. 监听
	r = listen(serverSocket, 10);
	if (r == -1)
	{
		perror("listen");
		exit(1);
	}

	//6. 等待客户端连接
	struct sockaddr_in cAddr = { 0 };
	socklen_t len = sizeof(cAddr);

	while (1)
	{
		clientSocket[count] = accept(serverSocket, (sockaddr*)&cAddr, &len);
		if (clientSocket[count] == -1)
		{
			perror("accept");
		}
		printf("client %d ---------> adrress: %s! port: %u16\n", count, inet_ntoa(cAddr.sin_addr), cAddr.sin_port);

		//7. (创建线程)通信
		std::thread RecvMsg(&recv_func, count);
		RecvMsg.detach();

		count++;
	}


	close(serverSocket);
	return 0;
}

void recv_func(int idx)
{
	char recv_buff[BUFFER_SIZE];
	while (1)
	{
		memset(recv_buff, 0, BUFFER_SIZE);
		int r = recv(clientSocket[idx], recv_buff, sizeof(recv_buff), 0);
		if (r > 0)
		{
			printf("Clinet %d -----> Server : %s\n",idx,  recv_buff);
		}

		//广播给所有client
		for (int i = 0; i < count; i++)
		{
			if (i != idx)
			{
				send(clientSocket[i], recv_buff, sizeof(recv_buff), 0);
			}
		}

		// 输入quit退出
		if (strncmp(recv_buff, "quit", 4) == 0 || r <= 0)
		{
			printf("Clinet %d 退出聊天\n", idx);
			break;
		}
	}
}

客户端代码(多线程实现)

客户端代码是通用的,对于服务器端是通过多线程实现还是epoll实现是没有任何影响的。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <thread>

#define BUFFER_SIZE 1024


void send_func();
void recv_func();

int clientSocket;

int main()
{

	//2. ����socket
	clientSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (clientSocket == -1)
	{
		perror("socket");
		exit(1);
	}

	//3. ��ȡ������Э���ַ��
	struct sockaddr_in addr;
	addr.sin_family = AF_INET; //Э��汾
	addr.sin_addr.s_addr = inet_addr("192.168.179.128");
	addr.sin_port = htons(10086); //�����ֽ���--->�����ֽ��� 

	//4. ���ӷ�����
	int r = connect(clientSocket, (sockaddr*)&addr, sizeof(addr));
	if (r == -1)
	{
		perror("bind");
		exit(1);
	}

	//5. ͨ��
	std::thread RecvMsg(&recv_func);
	RecvMsg.detach();
	send_func();
	
	return 0;
}

void send_func()
{
	char send_buff[BUFFER_SIZE];

	while (1)
	{
		if (fgets(send_buff, BUFFER_SIZE, stdin) == NULL)
		{
			puts("no message\n");
			continue;
		}
		else
		{
			send(clientSocket, send_buff, sizeof(send_buff), 0);
			printf("I -----> Others : %s\n", send_buff);
		}

		if (strncmp(send_buff, "quit", 4) == 0)
		{
			break;
		}

	}

}

void recv_func()
{
	char recv_buff[BUFFER_SIZE];
	while (1)
	{
		int r = recv(clientSocket, recv_buff, sizeof(recv_buff), 0);
		if (r > 0)
		{
			printf("Others -----> I : %s\n", recv_buff);
		}
	}
}

最终效果

最终效果如图所示,连接了三个客户端(0,1,2),都可以接收到彼此的信息。

img

在Windows上实现

还实现了windows上的多线程版本,代码看仓库ChatRoom: 网络程序设计,聊天实验室代码 (gitee.com)

后续可以整合实现Windows和Linux跨平台的程序

效果如图

img

posted @ 2023-12-31 23:00  DogWealth~  阅读(154)  评论(0编辑  收藏  举报