• 基于Linux下的多人聊天室


    1.涉及知识点

    Linux、C语言、TCP通信、epoll、SQL

    2.整体架构流程

    服务器:
    1.搭建TCP连接客户端
    2.链接数据库
    3.使用epoll
    4.处理各种客户端消息的接收与发送
    客户端:
    1.搭建TCP连接服务器
    2.建立菜单处理各种消息的发送与接收

    3.核心功能展示

    1.登录界面
    用户端:
    用户登录界面
    服务器端(反馈):
    服务器端(反馈)
    进行注册:
    注册
    登录:
    登录
    私聊:
    私聊
    群聊
    群聊
    数据库:
    数据库

    4.详细代码

    • 服务器端
    #include 
    #include 
    #include  
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #define SERVER_IP "192.168.43.113"
    #define SERVER_PORT 1111
    
    int skd; // 服务器套接子
    int epfd;
    struct epoll_event ev; // 这个结构体仅仅创建一次,后续都可使用
    int clinum;            // 用来标记在线人数
    int num;               // 用来保存异动套接子的个数
    char buf[1024];        // 数据库传数据的
    enum MessageType
    {
        // menu1
        REGISTRATION,
        LOGIN,
        // menu2
        QUN,
        SI,
        DELETE_ID,
        LOG_out,
        Ke_out
        // 此处添加其他消息类型
    };
    
    typedef struct // 打包本地数据
    {
        int chat_mode;
        int UID;
        char nickname[50];
        char password[20];
        int target_id;
        char send_message[1024];
        int socket;
        int log_flag;
        enum MessageType type;
    } User;
    User user[1024];
    User user1;
    User user2;
    MYSQL *sql;
    void *client_register_func(void *arg);
    void *client_LOG_IN_func(void *arg);
    void look_time();
    int main(void)
    {
        memset(&user1, 0, sizeof(user1));
        skd = socket(AF_INET, SOCK_STREAM, 0);       // 套接字类型为流式套接字(SOCK_STREAM)
        struct sockaddr_in info;                     // 用于存储服务器的地址信息
        info.sin_addr.s_addr = inet_addr(SERVER_IP); // 设置服务器的IP地址
        info.sin_family = AF_INET;                   // 指定地址族为IPv4(AF_INET)
        info.sin_port = htons(SERVER_PORT);          // 设置服务器的端口号,htons是将主机字节顺序统一为大端(主机字节顺序大小段模式不确定)
    
        // 允许快速重用端口,并支持多个套接字绑定到同一个端口
        int opt = 1;
        setsockopt(skd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
        setsockopt(skd, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
        // 将套接字与指定的本地地址和端口号相关联
        int ret = bind(skd, (struct sockaddr *)&info, sizeof(info));
        if (ret < 0)
        {
            perror("bind");
            return -1;
        }
        printf("服务器IP绑定成功\n");
    
        ret = listen(skd, 1024);
        if (ret < 0)
        {
            perror("listen");
            return -1;
        }
        printf("服务器已启动,等待连接...\n");
        char buf[128];
        // 1.创建句柄 --- 只创建一次
        epfd = epoll_create(1);
        if (epfd <= 0)
        {
            perror("epoll_create");
            return -1;
        }
        // 2.在句柄中加入监听套接子
        ev.data.fd = skd;
        ev.events = EPOLLIN;
    
        ret = epoll_ctl(epfd, EPOLL_CTL_ADD, skd, &ev);
        if (ret < 0)
        {
            perror("epoll_ctl");
            return -1;
        }
        struct epoll_event retevn[1024];
        // 链接数据库
        // 1.初始化数据库
        sql = mysql_init(NULL);
        // 2.连接数据库
        sql = mysql_real_connect(sql, "localhost", "root", "1", "mysql", 0, NULL, 0);
        if (sql == NULL)
        {
            perror("mysql_real_connect");
            return -1;
        }
        printf("数据库连接成功\n");
        // 链接需要使用的数据库
        sprintf(buf, "%s", "use Chat_data");
        // 在终端创建数据表:users_info(UID int,昵称 char(50),密码 char(20))
        ret = mysql_query(sql, buf);
        if (ret != 0)
        {
            perror("mysql_query");
            return -1;
        }
        printf("更改数据库成功\n");
        memset(buf, 0, sizeof(buf));
    
        int i = 0;
        while (1)
        {
            printf("开始阻塞\n");
            //-1表示单次阻塞的时间足够长
            num = epoll_wait(epfd, retevn, 1024, -1);
            if (num <= 0)
            {
                perror("epoll_wait");
                continue;
            }
            printf("解除阻塞\n");
            for (i = 0; i < num; i++) // 异动套接子可能不止一个
            {
                if (retevn[i].data.fd == skd) // 说明服务器套接字有动静
                {
                    if (retevn[i].events == EPOLLIN)
                    {
                        // 说明有客户端上线
                        struct sockaddr client_info;
                        socklen_t len = sizeof(client_info);
                        // 将上线的文件描述符单独保存
                        int client_socket = accept(skd, &client_info, &len);
                        if (client_socket < 0)
                        {
                            perror("accept");
                            // 处理接受错误
                            continue; // 继续处理其他事件
                        }
                        // 将上线的套接字加入到epoll的句柄
                        ev.data.fd = client_socket;
                        ev.events = EPOLLIN;
                        int ret = epoll_ctl(epfd, EPOLL_CTL_ADD, client_socket, &ev);
                        if (ret < 0)
                        {
                            perror("epoll_ctl");
                            // 处理epoll控制错误
                            close(client_socket); // 关闭套接字
                            continue;             // 继续处理其他事件
                        }
                        printf("new client connected: %d\n", client_socket);
                        user[clinum].socket = client_socket; // 将新客户端信息保存到数组中
                        printf("clinum = %d\n", clinum);
                        printf("user[clinum].socket = %d\n", user[clinum].socket);
                        clinum++;
                    }
                }
                else // 说明客户端有动静
                {
                    for (int j = 0; j < clinum; j++)
                    {
                        if (user[j].socket == retevn[i].data.fd)
                        {
                            if (read(retevn[i].data.fd, &user1, sizeof(user1)) == 0)
                            {
                                printf("socket:%d  断开\n", user[j].UID);
                                close(user[j].socket);
                                // 移除断开连接的套接字
                                for (int k = j; k < clinum - 1; k++)
                                {
                                    user[k] = user[k + 1];
                                }
                                clinum--;
                                break;
                            }
                            switch (user1.type)
                            {
                            case REGISTRATION:
                            {
                                pthread_t pd1 = 0;
                                pthread_create(&pd1, NULL, client_register_func, (void *)retevn[i].data.fd);
                                break;
                            }
                            case LOGIN:
                            {
                                user1.socket = user[j].socket; // 异动套接子 --- 发送方套接子
                                pthread_t pd2 = 0;
                                pthread_create(&pd2, NULL, client_LOG_IN_func, (void *)retevn[i].data.fd);
                                break;
                            }
                            case QUN:
                            {
                                user1.socket = user[j].socket; // 异动套接子 --- 发送方套接子
                                printf("%d %d %s\n", user1.socket, user1.UID, user1.send_message);
                                printf("%s\n", user1.send_message);
                                printf("user1.socket%d\n", user1.socket);
                                for (int k = 0; k < clinum; k++)
                                {
                                    if (user[k].socket != user1.socket)
                                    {
                                        printf("user1.socket%d\n", user1.socket);
                                        printf("user1.nick%s\n", user1.nickname);
                                        printf("user[k].socket %d\n", user[k].socket);
                                        sprintf(buf, "%s", "select * from users_info");
                                        mysql_query(sql, buf);
                                        MYSQL_RES *retval = mysql_store_result(sql);
                                        // 8.1.获取行数 --- 判断数据表中是否存在相关数据
                                        // 判断这个人是否存在,找到对应人的对应值
                                        unsigned long rownum = mysql_num_rows(retval);
                                        unsigned long fieldnum = mysql_num_fields(retval);
                                        // 8.2获取每一行中的内容
                                        int a = 0, b = 0;
                                        MYSQL_ROW rowval;
                                        for (a = 0; a < rownum; a++) // 打印数据表中的所有内容
                                        {
                                            rowval = mysql_fetch_row(retval);
                                            if (atoi(rowval[3]) == user1.socket)
                                            {
                                                strcpy(user1.nickname, rowval[1]);
                                            }
                                        }
                                        write(user[k].socket, &user1, sizeof(user1));
                                    }
                                }
                                break;
                            }
                            case SI:
                            {
                                user2=user1;
                                printf("%d:%d\n",user1.socket,user1.UID);
                                sprintf(buf, "%s", "select * from users_info");
                                mysql_query(sql, buf);
                                MYSQL_RES *retval = mysql_store_result(sql);
                                // 8.1.获取行数 --- 判断数据表中是否存在相关数据
                                // 判断这个人是否存在,找到对应人的对应值
                                unsigned long rownum = mysql_num_rows(retval);
                                unsigned long fieldnum = mysql_num_fields(retval);
                                // 8.2获取每一行中的内容
                                int a = 0, b = 0;
                                MYSQL_ROW rowval;
                                for (a = 0; a < rownum; a++) // 打印数据表中的所有内容
                                {
                                    rowval = mysql_fetch_row(retval);
                                    if (atoi(rowval[3]) == user1.socket)
                                    {
                                       strcpy(user2.nickname,rowval[1]);
                                    }
                                    if (atoi(rowval[0]) == user2.UID)
                                    {
                                        user2.socket = atoi(rowval[3]);
                                    }
                                }
    
                                printf("接收方套接子为 is %d\n", user2.socket);
                                write(user2.socket, &user2, sizeof(user2));
    
                                break;
                            }
                            case LOG_out:
                            {
                                printf("UID:%d  %s 退出登陆\n", user1.UID, user1.nickname);
                                sprintf(buf, "UPDATE users_info SET socket = 0 WHERE socket = %d", user[j].socket);
                                mysql_query(sql, buf);
                                // UPDATE users_info SET socket = 0 WHERE UID = '50663392'
                                break;
                            }
                            case Ke_out:
                            {
                                printf("socket:%d  客户端退出\n", user[j].socket);
                                close(user[j].socket);
                                // 移除断开连接的套接字
                                for (int k = j; k < clinum - 1; k++)
                                {
                                    user[k] = user[k + 1];
                                }
                                clinum--;
                                break;
                            }
                            case DELETE_ID:
                            {
                                printf("UID:%d 账号已注销\n", user1.UID);
                                sprintf(buf, "delete from users_info where UID=%d", user1.UID);
                                mysql_query(sql, buf);
                            }
                            default:
                                break;
                            }
                        }
                    }
                }
            }
        }
        return 0;
    }
    void *client_register_func(void *arg)
    {
        srand(time(0));
        int fd = (int)arg;
        int i = 0;
        for (i = 0; i < clinum; i++)
        {
            if (user[i].socket == fd)
            {
                int number;
                do
                {
                    number = rand() % 90000000 + 10000000;
                } while (number == 0);
                // 检查随机数是否已经存在
                int exists = 0;
                for (int j = 0; j < clinum; j++)
                {
                    if (user[j].UID == number)
                    {
                        exists = 1;
                        break;
                    }
                }
                if (exists)
                {
                    // 如果随机数已存在,重新生成
                    i--;
                    continue;
                }
                strcpy(user[i].nickname, user1.nickname);
                strcpy(user[i].password, user1.password);
                user[i].UID = number;
                // 写到数据库中
                sprintf(buf, "insert into users_info value(%d,'%s','%s',%d)", user[i].UID, user[i].nickname, user[i].password, user[i].socket);
                mysql_query(sql, buf);
                write(fd, &user[i], sizeof(user[i]));
            }
        }
        return NULL;
    }
    
    void *client_LOG_IN_func(void *arg)
    {
        int fd = (int)arg;
        sprintf(buf, "%s", "select * from users_info");
        mysql_query(sql, buf);
        MYSQL_RES *retval = mysql_store_result(sql);
        // 8.1.获取行数 --- 判断数据表中是否存在相关数据
        // 判断这个人是否存在,找到对应人的对应值
        unsigned long rownum = mysql_num_rows(retval);
        unsigned long fieldnum = mysql_num_fields(retval);
        // 8.2获取每一行中的内容
        int a = 0, j = 0;
        MYSQL_ROW rowval;
        for (a = 0; a < rownum; a++) // 打印数据表中的所有内容
        {
            rowval = mysql_fetch_row(retval);
            if (atoi(rowval[0]) == user1.UID)
            {
                for (j = 0; j < fieldnum; j++)
                {
                    if (strcmp(rowval[j], user1.password) == 0)
                    {
                        user1.log_flag = 1;
                        if (user1.log_flag == 1)
                        {
                            printf("UID:%d 成功上线\n", user1.UID);
                            sprintf(buf, "UPDATE users_info SET socket = %d WHERE UID = %d", fd, user1.UID);
                            mysql_query(sql, buf);
                            write(user1.socket, &user1, sizeof(user1));
                        }
                    }
                }
            }
        }
        if (user1.log_flag == 0)
        {
            write(user1.socket, &user1, sizeof(user1));
            printf("账号或密码错误\n");
        }
        return NULL;
    }
    
    • 客户端
    #include 
    #include  
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #include 
    #define SERVER_IP "192.168.43.113"
    #define SERVER_PORT 1111
    
    int ke_socket; // 套接字
    int ret;
    char rbuf[1024];
    int si_id;
    int n; // 菜单选择
    
    enum MessageType
    {
        // menu1
        REGISTRATION,
        LOGIN,
    
        // menu2
        QUN,
        SI,
        DELETE_ID,
        LOG_out,
        Ke_out
        // 此处添加其他消息类型
    };
    
    typedef struct // 打包本地数据
    {
        int chat_mode;
        int UID;
        char nickname[50];
        char password[20];
        int target_id;
        char send_message[1024];
        int socket;
        int log_flag;
        enum MessageType type;
    } User;
    User user;
    void si_chat(int ke_socket);
    void look_time();
    void qun_chat(int ke_socket);
    void registers(int ke_socket);
    void menu2(int ke_socket);
    void LOG_in(int ke_socket);
    void *my_qun_threadfunc(void *arg);
    void *my_sichat_func(void *arg);
    
    void myfunc(int signum) // 重定义2号信号
    {
        if (signum == 2) // 收到 SIGINT 信号
        {
            printf("客户端已退出\n");
    
            close(ke_socket);
            exit(0);
        }
    }
    
    void *mypthreadfunc(void *arg) // 客户端发送
    {
        int client_socket = *(int *)arg;
        while (1)
        {
            printf("client send:\n");
            scanf("%s", user.send_message);
            write(client_socket, &user, sizeof(user));
        }
        return NULL;
    }
    
    int main(void)
    {
        ke_socket = socket(AF_INET, SOCK_STREAM, 0);
        struct sockaddr_in info;
        info.sin_addr.s_addr = inet_addr(SERVER_IP);
        info.sin_family = AF_INET;
        info.sin_port = htons(SERVER_PORT);
    
        int opt = 1;
        setsockopt(ke_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
        setsockopt(ke_socket, SOL_SOCKET, SO_REUSEPORT, &opt, sizeof(opt));
    
        ret = connect(ke_socket, (struct sockaddr *)&info, sizeof(info));
        if (ret < 0)
        {
            perror("connect");
            return -1;
        }
        printf("成功链接智慧树\n");
        signal(SIGINT, myfunc);
    
        while (1)
        {
            printf("*****欢迎使用虚空终端*****\n");
            printf("1.登陆 2.注册 3.退出\n");
            printf("请选择:\n");
            int a;
            scanf("%d", &a);
            switch (a)
            {
            case 1:
                LOG_in(ke_socket);
                break;
            case 2:
                registers(ke_socket);
                break;
            case 3:
                printf("退出\n");
                user.log_flag = 0;
                user.socket = ke_socket;
                user.type = Ke_out;
                write(ke_socket, &user, sizeof(user));
                return 0;
            default:
                printf("输入有误,请重新输入!\n");
                break;
            }
        }
        close(ke_socket);
        return 0;
    }
    
    void registers(int ke_socket)
    {
        while (1)
        {
            user.type = REGISTRATION;
            srand(time(0));
            printf("请输入想要注册的昵称:");
            scanf("%s", user.nickname);
            printf("请输入密码:");
            scanf("%s", user.password);
            printf("请确认密码:");
            char remm[20];
            scanf("%s", remm);
            if (strcmp(user.password, remm) == 0)
            {
                write(ke_socket, &user, sizeof(user)); // 写入用户信息
                read(ke_socket, &user, sizeof(user));  // 读取服务器返回的用户信息
    
                printf("注册成功,请牢记个人信息:\n");
                printf("昵称:%s\n", user.nickname);
                printf("UID:%d\n", user.UID);
                break;
            }
            else
            {
                printf("注册失败,两次密码不一致!\n");
                printf("请返回菜单,重新注册!\n");
            }
        }
    }
    
    void LOG_in(int ke_socket)
    {
        int count = 0;
        while (count < 3)
        {
            user.type = LOGIN;
            printf("请输入您的UID:\n");
            scanf("%d", &user.UID);
            printf("请输入密码:\n");
            getchar();
            scanf("%s", user.password);
            write(ke_socket, &user, sizeof(user));
            usleep(1000);
            read(ke_socket, &user, sizeof(user));
            if (user.log_flag == 1)
            {
                printf("登陆成功!\n");
                menu2(ke_socket);
                return;
            }
            else
            {
                count++;
                if (count < 3)
                {
                    printf("帐号或密码错误,请重新输入!\n");
                }
                else
                {
                    printf("输入错误次数达到上限,返回菜单\n");
                    return;
                }
            }
        }
    }
    
    void menu2(int ke_socket)
    {
        while (1)
        {
    
            printf("****登陆成功****\n");
            printf("1.私聊 2.群聊 3.注销 4.登出\n");
            printf("请选择:");
            int n = 0;
            scanf("%d", &n);
            switch (n)
            {
            case 1:
                si_chat(ke_socket);
                break;
            case 2:
                qun_chat(ke_socket);
                break;
            case 3:
                printf("注销\n");
                user.log_flag = 0;
                user.type = DELETE_ID;
                write(ke_socket, &user, sizeof(user));
                break;
            case 4:
                printf("登出\n");
                user.log_flag = 0;
                user.type = LOG_out;
                write(ke_socket, &user, sizeof(user));
                return;
            default:
                printf("输入有误!\n");
                continue;
            }
        }
    }
    
    void qun_chat(int ke_socket)
    {
        // 创建子线程接收数据
        pthread_t pd;
        pthread_create(&pd, NULL, my_qun_threadfunc, &ke_socket);
        user.type=QUN;
        while (1)
        {
            printf("发给群聊:\n");
            scanf("%s", user.send_message);
            if (strcmp(user.send_message, "quit") == 0)
            {
                printf("用户已退出群聊\n");
                pthread_cancel(pd);
                break;
            }
            write(ke_socket, &user, sizeof(user));
        }
        return;
    }
    
    void look_time()
    {
        time_t now = time(NULL);
        struct tm *tm_info = localtime(&now);
        char time_str[20];
        strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", tm_info);
        printf("[%s]\n", time_str);
    }
    
    void *my_qun_threadfunc(void *arg) // 发送给客户端
    {
    
        int client_socket = *(int *)arg;
        user.type = QUN;
        while (1)
        {
            read(client_socket, &user, sizeof(user));
            look_time();
            printf("%s向群聊发送:%s\n", user.nickname,user.send_message);
        }
        return NULL;
    }
    
    void si_chat(int ke_socket)
    {
    
        // 创建子线程接收私聊信息
        user.type = SI;
        pthread_t pd;
        ret = pthread_create(&pd, NULL, my_sichat_func, &ke_socket);
        printf("请输入想要私聊的UID:\n");
        scanf("%d", &user.UID);
        int account = user.UID;
        while (1)
        {
            printf("私聊发送:\n");
            scanf("%s", user.send_message);
            if (strcmp(user.send_message, "quit") == 0)
            {
                printf("已退出群聊\n");
                pthread_cancel(pd);
                break;
            }
            user.UID = account;
            write(ke_socket, &user, sizeof(user));
        }
        return;
    }
    
    void *my_sichat_func(void *arg) // 私聊接收
    {
        int client_socket = *(int *)arg;
        while (1)
        {
            read(ke_socket, &user, sizeof(user));
            look_time();
            printf("%s:%s\n", user.nickname, user.send_message);
        }
        return NULL;
    }
    

    5.复盘总结

    1.TCP搭建流程不够熟悉。
    2.对epoll的理解不够深入。
    3.功能不够丰富。

  • 相关阅读:
    CAN协议解析
    智慧燃气巡检管理系统
    任务28 成绩管理系统
    【云原生 | 17】容器的四种网络模式
    springboot+knife4j初体验
    [C国演义] 哈希的使用和开闭散列的模拟实现
    NXP iMX6ULL核心板框架图|软硬件|功能功耗|原理图的规格书资料
    关于c++中和java中 集合添加元素的区别
    【毕业设计】基于单片机的墨水屏阅读器(单词卡) - 物联网 嵌入式
    【手写数字识别】GPU训练版本
  • 原文地址:https://blog.csdn.net/Jokerblank/article/details/139473203