Linux学习-基于TCP实现群聊

一、项目概述

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

二、核心功能模块

(一)客户端模块

  1. 连接初始化
    • 通过 init_tcp_send 函数创建 TCP 套接字,填充服务端地址(IP 和端口),为连接服务端做准备。
    • 代码示例:
c 复制代码
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 ,发送后关闭套接字并退出程序。
    • 代码示例:
c 复制代码
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 区分系统消息(用户加入、退出)和聊天消息,分别处理显示。
    • 代码示例:
c 复制代码
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 消息告知服务端用户加入;
      • 创建发送和接收线程,等待线程结束后关闭套接字。
    • 代码示例:
c 复制代码
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 和端口),并开始监听客户端连接。
    • 代码示例:
c 复制代码
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 实例中删除文件描述符,不再监听其事件。
    • 代码示例:
c 复制代码
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 中删除指定客户端套接字描述符,通常在客户端断开或退出时调用。
    • 代码示例:
c 复制代码
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 )处理(打印、广播、清理连接 )。
    • 代码示例:
c 复制代码
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 和连接数组删除相关描述符。
相关推荐
UNbuff2 小时前
Linux gzip 命令使用说明
linux
KFCcrazy43 小时前
嵌入式学习日记(36)TCP并发服务器构建——epoll
服务器·学习·tcp/ip
渡己(Sorin)3 小时前
Ubuntu有限网口无法使用解决方法
linux·运维·ubuntu
不懂机器人3 小时前
linux网络编程-----TCP服务端并发模型(epoll)
linux·网络·tcp/ip·算法
qq_297075673 小时前
网络安全测试(一)Kali Linux
linux·运维·服务器
diablobaal5 小时前
云计算学习100天-第28天
运维·服务器·学习
Adorable老犀牛5 小时前
一键编译安装zabbix(centos)
linux·centos·zabbix
艾莉丝努力练剑6 小时前
【C语言16天强化训练】从基础入门到进阶:Day 11
c语言·学习·算法
2401_831501736 小时前
Linux之Docker虚拟化技术(一)
java·linux·docker