当前位置: 首页 > news >正文

C++ 基于 Epoll 的多客户端聊天室项目

项目概述

这个项目实现了一个简单的 多客户端聊天室,基于 C++ 编程语言,使用了 epoll 机制来管理多个客户端连接。项目分为客户端和服务器端两部分,客户端通过 socket 连接到服务器,服务器可以处理多个客户端的消息并进行广播。


服务器端代码详解

服务器端主要步骤

  1. 创建监听套接字:

    • 使用 socket() 创建监听套接字。
    • 绑定 IP 地址和端口号,通过 bind() 绑定。
    • 使用 listen() 开始监听连接请求。
  2. 创建 epoll 句柄:

    • 使用 epoll_create1() 创建 epoll 句柄。
    • 将监听套接字添加到 epoll 中,监听新的客户端连接。
  3. 等待事件:

    • 通过 epoll_wait() 等待事件发生,处理新的客户端连接或客户端发送的消息。
  4. 处理客户端连接:

    • 使用 accept() 接受新的客户端连接,并将新的客户端套接字添加到 epoll 中进行监听。
  5. 消息处理与转发:

    • 当客户端发送消息时,服务器读取消息并将其广播给其他所有已连接的客户端。
#include<iostream>
#include<arpa/inet.h>
#include<unistd.h>
#include<string>
#include<sys/epoll.h>
#include<stdio.h>
#include<map>#define max_connect 128struct client_info{int fd_c;std::string name;std::string ip_addr;int host_port;
};void broadcast_client_count(const std::map<int, client_info>& mp_client) {std::string client_count_msg = "当前连接的客户端数量: " + std::to_string(mp_client.size()) + "\n";std::cout << client_count_msg << std::endl;
}int main() {int epld = epoll_create1(0);if(epld < 0) {perror("epoll 创建失败");return -1;}// 创建服务器的套接字文件int fd_sock = socket(AF_INET, SOCK_STREAM, 0);if(fd_sock == -1) {perror("创建套接字失败");return -1;}// 绑定通信接口struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(9999);addr.sin_addr.s_addr = INADDR_ANY;int ret = bind(fd_sock, (struct sockaddr*)&addr, sizeof(addr));if(ret == -1) {perror("绑定失败");return -1;}int fd_listen = listen(fd_sock, max_connect);if(fd_listen == -1) {perror("监听失败");return -1;}// 将监听的socket放入epollstruct epoll_event ev;ev.events = EPOLLIN;ev.data.fd = fd_sock;ret = epoll_ctl(epld, EPOLL_CTL_ADD, fd_sock, &ev);if(ret < 0) {perror("epoll_ctl 错误");return -1;}std::map<int, client_info> mp_client;// 循环监听while(1) {struct epoll_event evs[max_connect];int n = epoll_wait(epld, evs, max_connect, -1);if(n < 0) {perror("epoll_wait 错误");break;}for (int i = 0; i < n; i++) {int fd = evs[i].data.fd;// 新客户端连接if(fd == fd_sock) {struct sockaddr_in caddr;int addrlen = sizeof(caddr);int fd_acp = accept(fd_sock, (struct sockaddr*)&caddr, (socklen_t*)&addrlen);if(fd_acp < 0) {perror("accept 错误");continue;}struct epoll_event ev_client;ev_client.events = EPOLLIN;ev_client.data.fd = fd_acp;epoll_ctl(epld, EPOLL_CTL_ADD, fd_acp, &ev_client);char ip_str[INET_ADDRSTRLEN];inet_ntop(AF_INET, &(caddr.sin_addr), ip_str, INET_ADDRSTRLEN);int port = ntohs(caddr.sin_port);std::cout << "连接成功: " << ip_str << " 端口号: " << port << std::endl;client_info single_cl;single_cl.fd_c = fd_acp;single_cl.name = "";single_cl.ip_addr = ip_str;single_cl.host_port = port;mp_client[fd_acp] = single_cl;broadcast_client_count(mp_client);} else {char buff[1024];int n = read(fd, buff, 1024);if(n <= 0) {close(fd);std::cout << "断开连接: " << mp_client[fd].ip_addr << " 端口号: " << mp_client[fd].host_port << std::endl;epoll_ctl(epld, EPOLL_CTL_DEL, fd, 0);mp_client.erase(fd);broadcast_client_count(mp_client);} else {std::string msg(buff, n);if (mp_client[fd].name == "") {mp_client[fd].name = msg.substr(0, msg.find("\n"));std::cout << "客户端 " << mp_client[fd].name << " 已连接." << std::endl;} else {std::string name = mp_client[fd].name;for (auto pair : mp_client) {if (pair.first != fd) {std::string full_msg = '[' + name + "]: " + msg;write(pair.first, full_msg.c_str(), full_msg.size());}}}}}}}close(epld);close(fd_sock);return 0;
}

客户端主要步骤

  1. 创建套接字:

    • 使用 socket() 创建客户端套接字。
  2. 连接服务器:

    • 使用 connect() 与服务器建立连接。
  3. 多线程:

    • 启动两个线程,一个处理消息的读取(接收服务器的消息),另一个处理消息的写入(发送消息到服务器)。
  4. 消息发送与接收:

    • 使用 send() 发送消息。
    • 使用 recv() 接收服务器广播的消息。
#include<iostream>
#include<cstring>
#include<arpa/inet.h>
#include<unistd.h>
#include<stdlib.h>
#include<thread>void read_from_server(int sockfd) {char buffer[1024];while (true) {ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);if (n <= 0) {std::cerr << "Server closed the connection or error occurred." << std::endl;break;}buffer[n] = '\0';std::cout << buffer;}
}void write_to_server(int sockfd, const std::string& username) {if (send(sockfd, username.c_str(), username.size(), 0) < 0) {std::cerr << "Error sending username to server." << std::endl;return;}std::string message;while (std::getline(std::cin, message)) {message += "\n";if (send(sockfd, message.c_str(), message.size(), 0) < 0) {std::cerr << "Error sending message to server." << std::endl;break;}}
}// 客户端 main 函数
int main() {int fd_lis = socket(AF_INET, SOCK_STREAM, 0);if(fd_lis == -1) {perror("socket_listen error!");return -1;}struct sockaddr_in saddr;saddr.sin_family = AF_INET;saddr.sin_port = htons(9999);inet_pton(AF_INET, "192.168.159.129", &saddr.sin_addr.s_addr);int ret = connect(fd_lis, (struct sockaddr*)&saddr, sizeof(saddr));if(ret == -1) {perror("connect error");return -1;} else {std::cout << "welcome to chat space! please enter first word as your chat name" << std::endl;std::string username;std::getline(std::cin, username);// 启动读写线程std::thread reader(read_from_server, fd_lis);std::thread writer(write_to_server, fd_lis, username);// 等待线程结束reader.join();writer.join();close(fd_lis);}
}

在服务器使用 epoll 进行网络编程时,epoll 能够同时监听多个事件,比如客户端的连接请求和已连接客户端的新消息。服务器通过监听套接字和客户端套接字的不同事件来区分客户端的连接和消息传递。

区分客户端连接与消息的关键

  1. 监听套接字 (fd_sock):

    • 当有新的客户端尝试连接服务器时,服务器的监听套接字会触发 EPOLLIN 事件,表示有新的连接请求。
    • 服务器通过 accept() 来接受这个连接,并获取新的客户端套接字。
  2. 客户端套接字 (fd_acp):

    • 当已经连接的客户端发送消息时,客户端的套接字也会触发 EPOLLIN 事件。
    • 服务器通过 read()recv() 从该套接字中读取客户端发送的消息。

通过 epoll 的事件机制,可以轻松区分客户端的连接请求和新消息的到来。以下是具体如何区分这两者的示例代码。

int fd_sock = socket(AF_INET, SOCK_STREAM, 0);  // 创建监听套接字
bind(fd_sock, (struct sockaddr*)&addr, sizeof(addr));
listen(fd_sock, max_connect);// 将监听的套接字添加到 epoll 中监听
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = fd_sock;
epoll_ctl(epld, EPOLL_CTL_ADD, fd_sock, &ev);while (1) {struct epoll_event evs[max_connect];int n = epoll_wait(epld, evs, max_connect, -1);  // 等待事件for (int i = 0; i < n; i++) {int fd = evs[i].data.fd;// 如果是监听套接字上的事件,表示有新连接if (fd == fd_sock) {struct sockaddr_in caddr;int addrlen = sizeof(caddr);int fd_acp = accept(fd_sock, (struct sockaddr*)&caddr, (socklen_t*)&addrlen);// 将新的客户端套接字加入到 epoll 监听中struct epoll_event ev_client;ev_client.events = EPOLLIN;ev_client.data.fd = fd_acp;epoll_ctl(epld, EPOLL_CTL_ADD, fd_acp, &ev_client);// 打印新连接的客户端信息char ip_str[INET_ADDRSTRLEN];inet_ntop(AF_INET, &(caddr.sin_addr), ip_str, INET_ADDRSTRLEN);int port = ntohs(caddr.sin_port);std::cout << "新客户端连接: " << ip_str << "  端口号: " << port << std::endl;} else {// 如果是已连接客户端的事件,表示客户端发送了新消息char buff[1024];int n = read(fd, buff, 1024);if (n > 0) {// 处理客户端发送的消息std::string msg(buff, n);std::cout << "收到消息: " << msg << std::endl;// 将消息转发给其他客户端for (auto pair : mp_client) {if (pair.first != fd) {std::string full_msg = "[客户端]: " + msg;write(pair.first, full_msg.c_str(), full_msg.size());}}} else if (n == 0) {// 如果读到的数据为 0,表示客户端断开连接close(fd);std::cout << "客户端断开连接: " << mp_client[fd].ip_addr << std::endl;epoll_ctl(epld, EPOLL_CTL_DEL, fd, 0);mp_client.erase(fd);}}}
}

具体流程说明

  1. 监听套接字上的事件 (fd == fd_sock):

    • 当有客户端连接时,fd_sock 触发 EPOLLIN 事件。
    • 服务器调用 accept() 接受新的连接,并为新客户端创建一个新的套接字 (fd_acp)。
    • 新的客户端套接字也需要添加到 epoll 监听中,以便在后续客户端发送消息时可以检测到。
  2. 客户端套接字上的事件 (fd != fd_sock):

    • 当某个客户端发送消息时,其对应的客户端套接字会触发 EPOLLIN 事件。
    • 服务器通过 read() 从该套接字读取数据,并将数据转发给其他所有已连接的客户端。
  3. 客户端断开连接:

    • 如果 read() 返回值为 0,表示客户端已经断开连接,服务器会关闭该套接字并从 epoll 监听列表中移除它。

总结

  • 客户端连接事件:通过 fd == fd_sock 来区分,表示有新的客户端连接。
  • 客户端消息事件:通过 fd != fd_sock 来区分,表示已有客户端发送消息。

通过 epoll 的事件驱动机制,服务器可以同时处理多个客户端的连接和消息收发,并且在大规模并发场景下表现出色。


http://www.mrgr.cn/news/36767.html

相关文章:

  • GUET-CTF2019]soul sipse
  • k8s 1.28.2 集群部署 ingress 1.11.1 包含 admission-webhook
  • video视频标签播放视频时点击或拖拽进度条事件
  • 跨网文件交换是什么?FileLink跨网文件交换解决方案
  • ide使用技巧与插件推荐
  • netty编程之对3种IO模式的支持以及对应的关键源码分析
  • yolov5/v7/v8随机种子固定方法
  • 电脑usb接口封禁如何实现?5种禁用USB接口的方法分享!(第一种你GET了吗?)
  • 【已解决】Ubuntu 24.04 修改 ssh 连接端口无效
  • BSIM4 and MOSFET Modeling For IC Simulation
  • 屏幕翻译下载哪个?建议试试这5个
  • 【Golang】Go语言中type关键字到底是什么?
  • 遥感图像语义分割数据集制作(使用ArcGIS Pro)
  • 如何更改发票校验行项目布局
  • Java数据结构--List介绍
  • python功能测试
  • 不同类型的专利有哪些特点和区别?
  • 数据结构练习题————(二叉树)——考前必备合集!
  • 单片机项目合集列表与专栏说明——Excel合集列表目录查阅(持续更新)
  • 猜想的反例:DFS中结点顺序与后代关系的分析