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

Linux学习-基于TCP实现群聊

一、项目概述

本项目实现了一个简单的基于 TCP 协议的群聊应用,包含客户端和服务端两部分。客户端支持用户输入用户名、发送聊天消息、退出群聊;服务端利用 epoll 实现 I/O 多路复用,支持多客户端连接,能处理用户加入、退出、聊天消息的广播。

二、核心功能模块

(一)客户端模块

  1. 连接初始化
    • 通过 init_tcp_send 函数创建 TCP 套接字,填充服务端地址(IP 和端口),为连接服务端做准备。
    • 代码示例:
int init_tcp_send() {int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) {perror("socket error");return -1;}seraddr.sin_family = AF_INET;seraddr.sin_port = htons(SER_PORT);seraddr.sin_addr.s_addr = inet_addr(SER_IP);return sockfd;
}
  1. 消息发送线程(send_msg
    • 功能:循环读取用户输入,封装成 Msg_t 结构体(区分聊天消息 MSG_CHAT 和退出消息 MSG_QUIT ),通过 TCP 套接字发送给服务端。
    • 关键逻辑:
      • strncpy 安全复制用户名到消息结构体,避免缓冲区溢出;
      • 检测到 .quit\n 输入时,切换消息类型为 MSG_QUIT ,发送后关闭套接字并退出程序。
    • 代码示例:
void *send_msg(void *arg) {Msg_t msg = {0};msg.type = MSG_CHAT;strncpy(msg.name, username, sizeof(msg.name) - 1); msg.name[sizeof(msg.name) - 1] = '\0';int sockfd = *((int *)arg);while (1) {memset(msg.buff, 0, sizeof(msg.buff));fgets(msg.buff, sizeof(msg.buff), stdin); if (strcmp(msg.buff, ".quit\n") == 0) { msg.type = MSG_QUIT;send(sockfd, &msg, sizeof(msg), 0);close(sockfd);exit(0);}ssize_t cnt = send(sockfd, &msg, sizeof(msg), 0); if (cnt < 0) {perror("send error\n");break;}}return NULL;
}
  1. 消息接收线程(recv_msg
    • 功能:循环接收服务端广播的消息,根据消息类型(MSG_JOIN MSG_QUIT MSG_CHAT )格式化输出。
    • 关键逻辑:
      • 收到 cnt == 0 时,判定服务端断开连接,关闭套接字并退出;
      • 依据 msg.type 区分系统消息(用户加入、退出)和聊天消息,分别处理显示。
    • 代码示例:
void *recv_msg(void *arg) {Msg_t msg = {0};int sockfd = *((int *)arg);while (1) {memset(&msg, 0, sizeof(msg));ssize_t cnt = recv(sockfd, &msg, sizeof(msg), 0); if (cnt < 0) {perror("recv error\n");break;} else if (cnt == 0) { printf("服务器已断开连接\n");close(sockfd);exit(0);}if (msg.type == MSG_JOIN) {printf("[系统消息] %s 加入群聊\n", msg.name);} else if (msg.type == MSG_QUIT) {printf("[系统消息] %s 退出群聊\n", msg.name);} else if (msg.type == MSG_CHAT) { printf("%s: %s", msg.name, msg.buff);}}return NULL;
}
  1. 主函数流程
    • 步骤:
      • 读取用户名并清除换行符;
      • 初始化套接字、连接服务端;
      • 发送 MSG_JOIN 消息告知服务端用户加入;
      • 创建发送和接收线程,等待线程结束后关闭套接字。
    • 代码示例:
int main(int argc, const char*argv[]) {printf("请输入你的用户名: ");fgets(username, sizeof(username), stdin);username[strcspn(username, "\n")] = '\0'; int sockfd = init_tcp_send(); if (sockfd < 0) {perror("socket error");return -1;}int cnt = connect(sockfd, (struct sockaddr*)&seraddr, sizeof(seraddr)); if (cnt < 0) {perror("connect error");return -1;}Msg_t join_msg = {0};join_msg.type = MSG_JOIN;strncpy(join_msg.name, username, sizeof(join_msg.name) - 1); join_msg.name[sizeof(join_msg.name) - 1] = '\0';int ret = send(sockfd, &join_msg, sizeof(join_msg), 0); if (ret < 0) {perror("send join error");return -1;}pthread_t tid[2];pthread_create(&tid[0], NULL, send_msg, &sockfd); pthread_create(&tid[1], NULL, recv_msg, &sockfd); pthread_join(tid[0], NULL); pthread_join(tid[1], NULL);close(sockfd); 
}

(二)服务端模块

  1. 连接初始化(init_tcp_ser
    • 功能:创建 TCP 套接字,绑定服务端地址(IP 和端口),并开始监听客户端连接。
    • 代码示例:
int init_tcp_ser() {int sockfd = socket(AF_INET, SOCK_STREAM, 0); if (sockfd < 0) {perror("socket error");return -1;}struct sockaddr_in seraddr;seraddr.sin_family = AF_INET;seraddr.sin_port = htons(SER_PORT);seraddr.sin_addr.s_addr = inet_addr(SER_IP);int ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr)); if (ret < 0) {perror("bind error");return -1;}ret = listen(sockfd, 100); if (ret < 0) {perror("listen error");return -1;}return sockfd;
}
  1. epoll 相关操作(epoll_add_fd epoll_del_fd
    • epoll_add_fd :将文件描述符(套接字)添加到 epoll 实例,监听指定事件(如 EPOLLIN 读事件 )。
    • epoll_del_fd :从 epoll 实例中删除文件描述符,不再监听其事件。
    • 代码示例:
int epoll_add_fd(int epfds, int fd, uint32_t events) {struct epoll_event ev;ev.events = events;ev.data.fd = fd;int ret = epoll_ctl(epfds, EPOLL_CTL_ADD, fd, &ev); if (ret < 0) {perror("epoll_ctl add error");return -1;}return 0;
}
int epoll_del_fd(int epfds, int fd) {int ret = epoll_ctl(epfds, EPOLL_CTL_DEL, fd, NULL); if (ret < 0) {perror("epoll_ctl del error");return -1;}return 0;
}
  1. 客户端连接管理(save_connfd del_connfd
    • save_connfd :将新连接的客户端套接字描述符保存到数组 connfds_g ,用于后续广播消息。
    • del_connfd :从 connfds_g 中删除指定客户端套接字描述符,通常在客户端断开或退出时调用。
    • 代码示例:
int save_connfd(int *connfds_g, int fd) {if (total_fd_g >= MAX_FD_CNT || total_fd_g < 0) { return -1;}connfds_g[total_fd_g] = fd; total_fd_g++; return 0;
}
int del_connfd(int *connfds_g, int fd) {int i;for (i = 0; i < total_fd_g; ++i) { if (connfds_g[i] == fd) {break;}}if (i >= total_fd_g) { printf("connfds_g Not found %d\n", fd);return -1;}for (; i < total_fd_g - 1; ++i) { connfds_g[i] = connfds_g[i + 1];}total_fd_g--; if (total_fd_g < 0) {return -1;}return 0;
}
  1. 主循环与事件处理
    • 功能:
      • epoll_wait 等待客户端事件(新连接、消息接收);
      • 处理新客户端连接,将其套接字加入 epoll 监听和连接管理数组;
      • 接收客户端消息,根据消息类型(MSG_JOIN MSG_QUIT MSG_CHAT )处理(打印、广播、清理连接 )。
    • 代码示例:
int main(int argc, const char *argv[]) {Msg_t mymsg;struct sockaddr_in cliaddr;socklen_t clilen = sizeof(cliaddr);int sockfd = init_tcp_ser(); if (sockfd < 0) {return -1;}int epfds = epoll_create(MAX_FD_CNT); if (epfds < 0) {perror("epoll_create error");return -1;}epoll_add_fd(epfds, sockfd, EPOLLIN); struct epoll_event evs[MAX_FD_CNT];while (1) {int cnt = epoll_wait(epfds, evs, MAX_FD_CNT, -1); if (cnt < 0) {perror("epoll_wait error");return -1;}for (int i = 0; i < cnt; ++i) {if (sockfd == evs[i].data.fd) { int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen); if (connfd < 0) {perror("accept error");return -1;}epoll_add_fd(epfds, connfd, EPOLLIN); save_connfd(connfds_g, connfd); } else { memset(&mymsg, 0, sizeof(mymsg));ssize_t size = recv(evs[i].data.fd, &mymsg, sizeof(Msg_t), 0); if (size < 0) { perror("recv error");epoll_del_fd(epfds, evs[i].data.fd); del_connfd(connfds_g, evs[i].data.fd); close(evs[i].data.fd); continue;} else if (size == 0) { printf("客户端断开连接\n");epoll_del_fd(epfds, evs[i].data.fd); del_connfd(connfds_g, evs[i].data.fd); close(evs[i].data.fd); continue;}if (mymsg.type == MSG_JOIN) { printf("[%s] 加入群聊!\n", mymsg.name);} else if (mymsg.type == MSG_QUIT) { printf("[%s] 退出群聊!\n", mymsg.name);epoll_del_fd(epfds, evs[i].data.fd); del_connfd(connfds_g, evs[i].data.fd); close(evs[i].data.fd); } else if (mymsg.type == MSG_CHAT) { printf("%s: %s", mymsg.name, mymsg.buff);}for (int j = 0; j < total_fd_g; ++j) { if (evs[i].data.fd != connfds_g[j]) { ssize_t send_size = send(connfds_g[j], &mymsg, sizeof(Msg_t), 0); if (send_size < 0) { perror("send error");close(connfds_g[j]); del_connfd(connfds_g, connfds_g[j]); continue;}}}}}}close(sockfd); return 0;
}

三、关键技术点

  1. TCP 套接字编程:客户端和服务端通过 socket connect bind listen accept 等函数建立 TCP 连接,实现可靠的字节流传输。
  2. 多线程(客户端):客户端用 pthread 库创建两个线程,分别处理消息发送和接收,实现输入输出异步操作。
  3. I/O 多路复用(服务端):服务端利用 epoll 高效管理多个客户端连接,同时监听新连接和消息事件,提升并发处理能力。
  4. 结构体与消息协议:定义 Msg_t 结构体统一消息格式,包含类型、用户名、内容,两端通过该结构体解析和封装消息,实现协议约定。
  5. 内存与连接管理:服务端用数组管理客户端连接,配合 epoll 实现连接的添加、删除,避免资源泄漏和无效连接干扰。

四、常见问题与解决方案

  1. 消息乱码:因 fgets 读取换行符或 strcpy 未正确处理字符串结束符,解决方案是用 strcspn 清除换行符,strncpy 结合手动补 \0 保证字符串安全。
  2. 服务端收不到聊天消息:可能服务端未正确解析 MSG_CHAT 类型消息或广播逻辑错误,需检查服务端消息类型判断和广播循环,确保 Msg_t 结构体两端一致。
  3. 客户端连接后服务端无响应:排查网络(IP、端口是否正确,防火墙是否拦截 )、epoll 事件注册(是否添加新连接到 epoll )、消息接收逻辑(recv 返回值处理 )。
  4. 资源泄漏:客户端线程未正确关闭套接字、服务端未及时清理断开的客户端连接,需在退出或断开时调用 close 、从 epoll 和连接数组删除相关描述符。
http://www.dtcms.com/a/354588.html

相关文章:

  • 医疗AI时代的生物医学Go编程:高性能计算与精准医疗的案例分析(三)
  • windows下查看别的服务器的端口是否通
  • [光学原理与应用-319]:激光器光路设计的主要输出文件的形式和内容
  • 解构与重构:“真人不露相,露相非真人” 的存在论新解 —— 论 “真在” 的行为表达本质
  • 一文读懂:用PyTorch从零搭建一个Transformer模型
  • (LeetCode 每日一题) 3446. 按对角线进行矩阵排序(矩阵、排序)
  • 读大语言模型08计算基础设施
  • GeoScene Maps 完整入门指南:从安装到实战
  • 《Explanation of Adaptive Platform Design》详细解读
  • 同一个栅格数据,为何在QGIS和ArcGIS Pro中打开后显示的数值范围不同?
  • redis单哨兵模式
  • 单元测试到底是什么?该怎么做?
  • 破译心智密码:神经科学如何为下一代自然语言处理绘制语义理解的蓝图
  • 【后端】微服务后端鉴权方案
  • 总结:在工作场景中的应用。(Excel)
  • UGUI源码剖析(13):交互的基石——Selectable状态机与Button事件
  • 【qml-7】qml与c++交互(自动补全提示)
  • mac m4执行nvm install 14.19.1报错,安装低版本node报错解决
  • 微服务保护和分布式事务-01.雪崩问题-原因分析
  • LeetCode-279. 完全平方数
  • 楼宇自控系统应需而生为现代建筑装上智能化翅膀
  • 【论文阅读】CLIP: 从自然语言监督中学习可迁移的视觉模型
  • 移动端网页调试实战,iOS WebKit Debug Proxy 的应用与替代方案
  • 《口令猜测研究进展》——论文阅读
  • springboot连接不上redis,但是redis客户端是能连接上的
  • ⸢ 贰 ⸥ ⤳ 安全架构:数字银行安全体系规划
  • iOS混淆工具实战,社交类 App 的隐私与安全防护混淆流程
  • 【C++详解】用哈希表封装实现myunordered_map和 myunordered_set
  • Redis 保证数据不丢失
  • 系统架构设计师备考第10天——网络技术-局域网以太网