Linux学习-基于TCP实现群聊
一、项目概述
本项目实现了一个简单的基于 TCP 协议的群聊应用,包含客户端和服务端两部分。客户端支持用户输入用户名、发送聊天消息、退出群聊;服务端利用 epoll
实现 I/O 多路复用,支持多客户端连接,能处理用户加入、退出、聊天消息的广播。
二、核心功能模块
(一)客户端模块
- 连接初始化
- 通过
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;
}
- 消息发送线程(
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;
}
- 消息接收线程(
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;
}
- 主函数流程
- 步骤:
- 读取用户名并清除换行符;
- 初始化套接字、连接服务端;
- 发送
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);
}
(二)服务端模块
- 连接初始化(
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;
}
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;
}
- 客户端连接管理(
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;
}
- 主循环与事件处理
- 功能:
- 用
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;
}
三、关键技术点
- TCP 套接字编程:客户端和服务端通过
socket
connect
bind
listen
accept
等函数建立 TCP 连接,实现可靠的字节流传输。 - 多线程(客户端):客户端用 pthread 库创建两个线程,分别处理消息发送和接收,实现输入输出异步操作。
- I/O 多路复用(服务端):服务端利用
epoll
高效管理多个客户端连接,同时监听新连接和消息事件,提升并发处理能力。 - 结构体与消息协议:定义
Msg_t
结构体统一消息格式,包含类型、用户名、内容,两端通过该结构体解析和封装消息,实现协议约定。 - 内存与连接管理:服务端用数组管理客户端连接,配合
epoll
实现连接的添加、删除,避免资源泄漏和无效连接干扰。
四、常见问题与解决方案
- 消息乱码:因
fgets
读取换行符或strcpy
未正确处理字符串结束符,解决方案是用strcspn
清除换行符,strncpy
结合手动补\0
保证字符串安全。 - 服务端收不到聊天消息:可能服务端未正确解析
MSG_CHAT
类型消息或广播逻辑错误,需检查服务端消息类型判断和广播循环,确保Msg_t
结构体两端一致。 - 客户端连接后服务端无响应:排查网络(IP、端口是否正确,防火墙是否拦截 )、
epoll
事件注册(是否添加新连接到epoll
)、消息接收逻辑(recv
返回值处理 )。 - 资源泄漏:客户端线程未正确关闭套接字、服务端未及时清理断开的客户端连接,需在退出或断开时调用
close
、从epoll
和连接数组删除相关描述符。