首页 > 系统相关 >Ubuntu22.04 上使用 C 语言实现简易聊天室程序

Ubuntu22.04 上使用 C 语言实现简易聊天室程序

时间:2024-01-24 15:57:19浏览次数:45  
标签:聊天室 cJSON Ubuntu22.04 response 简易 user MSG id 客户端

Linux 程序设计课程作业,在此记录下我的实现过程和思路,如有错误或不足,欢迎指正!代码:https://github.com/Tangsmallrong/Linux_network_program/

1. 需求

设计并实现一个简单的聊天室程序,实现如下功能:

  • 用户界面:实现基于终端的字符界面,支持用户管理,包括用户名和密码的注册与登录。
  • 多用户交流:允许多个用户登录到服务器的聊天室并进行实时交流。用户输入的信息应能被聊天室内的所有其他用户看到。
  • 客户端功能:每个客户端对应一个用户,负责输入信息的采集、发送至服务器以及接收并显示来自服务器的信息。

2. 实现思路

2.1 客户端

  • 用户界面:提供登录、注册、发送消息等界面。
  • 消息处理:实现消息的发送和接收功能。

2.2 服务端

  • 用户信息管理:使用 JSON 文件存储用户信息,包括用户名和密码。
  • 连接处理:监听并接受客户端的连接请求。
  • 消息处理:接收客户端消息,进行处理并广播给所有在线用户。

2.3 代码目录结构

chat_room/
│   ├── client.c  // 客户端
│   ├── server.c  // 服务端
│   ├── lib/      // cJSON 库
|   	├── cJSON.c
│       └── cJSON.h

3. 消息传递机制

3.1 消息传递类型定义

  • 根据需求,可以定义客户端和服务器之间的消息类型,通过 JSON 格式在 TCP 连接上进行传递,可以使客户端与服务端的交互更为有序和高效。
// 枚举表示客户端和服务器之间的消息类型,如初始化消息、菜单消息、发送消息等。
typedef enum
{
    MSG_LOGIN,   // 登录
    MSG_REGIST,  // 注册
    MSG_QUIT,    // 退出
    MSG_LOGIN_FAILED,   // 登录失败
    MSG_REGIST_FAILED,  // 注册失败
    MSG_SEND,      // 聊天
    MSG_BROADCAST  // 广播
} MessageType;

3.2 消息格式和处理

  • 消息的格式化和解析使用了 cJSON 库,其提供了灵活且高效的数据封装与解析机制,支持复杂数据结构的传输。
#include "cJSON.h"

// 创建 cJSON 响应对象, 包含消息类型、消息内容和可选的数据字段
cJSON *createResponse(MessageType message_type, const char *message, cJSON *data) {
    cJSON *response = cJSON_CreateObject();

    // 添加消息类型到响应
    cJSON_AddNumberToObject(response, "message_type", message_type);

    // 添加消息内容到响应
    cJSON_AddStringToObject(response, "message", message);

    // 添加数据(如果提供的话)
    if (data) {
        cJSON_AddItemToObject(response, "data", data);
    }

    return response;
}

4. 客户端实现

4.1 状态机设计

之前上 web 课的时候了解过,从这次的需求来看还是比较适合使用的

在 C 语言程序中实现状态机,有助于管理复杂的客户端状态。状态机的主要组成包括:

  • 状态(States):客户端在不同阶段的状态,例如 登录状态聊天状态 等。
  • 事件(Events):导致状态转换的活动,比如 成功连接服务器收到消息 等。
  • 转换(Transitions):在不同状态间转换的逻辑,通常由事件触发。

状态机的优势在于可以清晰地定义程序在不同状态下的行为,并且易于扩展和维护。

4.1.1 状态定义

使用枚举类型定义客户端的不同状态:

// 枚举表示客户端的不同状态, 如菜单、发送消息等
typedef enum
{
    STATUS_MENU,     // 菜单
    STATUS_LOGIN,    // 登录
    STATUS_REGIST,   // 注册
    STATUS_QUIT,     // 退出
    STATUS_SEND_MSG  // 聊天
} ClientState;

4.1.2 状态转换

客户端程序的主循环根据当前状态调用相应的函数,实现状态之间的转换:

while (1) {
    switch (state) {
        case STATUS_MENU:
            state = fun_st1_menu(); // 处理主菜单逻辑
            break;
        case STATUS_LOGIN:
            state = fun_st2_login(); // 处理登录逻辑
            break;
    	// 其他状态处理...
    }
}

4.2 主菜单逻辑 fun_st1_menu()

  • 显示菜单选项:函数首先打印出聊天室服务系统的主菜单,包括选项 登录注册退出系统
  • 读取用户输入:函数使用getchar()函数从标准输入读取一个字符代表用户的选择。
  • 处理额外的输入:为了防止之后的输入受到之前输入的影响,使用一个循环来清除输入缓冲区中的剩余字符(包括回车符和换行符)。
  • 根据输入切换状态
    • 如果用户输入 1,函数返回 STATUS_LOGIN 状态,表示用户选择了登录。
    • 如果输入 2,则返回 STATUS_REGIST 状态,表示用户选择了注册。
    • 如果输入 3,则打印退出提示并返回 STATUS_QUIT 状态,表示用户选择了退出程序。
  • 处理无效输入:如果用户输入了除 123 以外的字符,函数会认为这是一个无效输入。此时,它会提示用户输入不合法,并重新展示菜单,要求用户重新输入。
  • 代码实现fun_st1_menu()
// 客户端菜单状态
ClientState fun_st1_menu() {
    int input, ch; // ch 用于清除输入缓冲区中的字符
    printf("===== 聊天室服务系统 ======\n");
    printf("1 登录\n");
    printf("2 注册\n");
    printf("3 退出系统\n");
    printf("==========================\n");
    printf("请输入您的选择: ");

    while (1) {
        // 使用 getchar 读取单个字符
        input = getchar();

        // 清除输入缓冲区中的回车符和换行符
        while ((ch = getchar()) != '\n' && ch != EOF);

        if (input == '1') {
            return STATUS_LOGIN;
        }
        else if (input == '2') {
            return STATUS_REGIST;
        }
        else if (input == '3') {
            printf("正在退出, 请稍等....\n");
            return STATUS_QUIT;
        }
        else {
            // 处理不合法的输入
            printf("输入不合法!!请重新输入: ");
        }
    }
}

4.3 登录注册逻辑

4.3.1 登录逻辑 fun_st2_login()

  • 用户输入获取:程序提示用户输入用户名和密码。输入内容应符合特定规则(如只包含字母、数字和下划线)。
// 检查输入是否有效:非空,且仅包含字母、数字和下划线
int isValidInput(const char *input) {
    if (input[0] == '\0')
        return 0; // 检查是否为空字符串
    for (int i = 0; input[i] != '\0'; i++) {
        if (!isalnum(input[i]) && input[i] != '_')
            return 0; // 只允许字母、数字和下划线
    }
    return 1;
}
  • JSON 消息构建:程序将用户名和密码封装进 JSON 对象,并标记消息类型为 MSG_LOGIN
// 客户端登录状态
ClientState fun_st2_login() {
    // 初始化 cJSON 对象
    cJSON *jsonObject = cJSON_CreateObject();

    char username[MAX_SIZE];
    char password[MAX_SIZE];
    printf("========= 登录 =========\n");
    getUsernameAndPassword(username, password); // 获取用户输入的用户名和密码

    // 将用户名和密码添加到 cJSON 对象
    cJSON_AddStringToObject(jsonObject, "username", username);
    cJSON_AddStringToObject(jsonObject, "password", password);
    // 将消息类型添加到 cJSON 对象
    cJSON_AddNumberToObject(jsonObject, "message_type", MSG_LOGIN);

    // 使用封装的函数发送 cJSON 对象
    sendJsonMessage(sockfd, jsonObject);
    cJSON_Delete(jsonObject); // 释放 cJSON 对象内存
	
    // ...
}
  • 消息发送:封装好的 JSON 消息通过 TCP 套接字发送到服务端。
// 客户端登录请求示例
{
    "username": "user123",
    "password": "password123",
    "message_type": 0  // MSG_LOGIN
}
  • 响应处理:客户端根据服务端返回的响应判断登录是否成功。成功时,程序会保存用户ID,作为后续操作的凭证。
  • 返回状态:根据登录结果,函数返回相应的客户端状态,如 STATUS_SEND_MSG(发送消息状态)或 STATUS_MENU(主菜单状态)
int response_type = status_item->valueint;
if (response_type == MSG_LOGIN) {
    cJSON *user_id_item = cJSON_GetObjectItem(response_json, "data");
    if (user_id_item) {
        cJSON *actual_user_id_item = cJSON_GetObjectItem(user_id_item, "user_id");
        user_id = actual_user_id_item->valueint; // 存储用户 ID 到全局变量
        strcpy(user_name, username);  // 更新全局用户名
        clearScreen();
        printf("登录成功!欢迎 %d 号用户, %s。\n", user_id, username);
        cJSON_Delete(response_json);
        return STATUS_SEND_MSG;
    }
    else {
        printf("响应中未包含用户ID。\n");
    }
}
else if (response_type == MSG_LOGIN_FAILED) {
    printf("登录失败! 请检查您的用户名和密码!\n");
}

cJSON_Delete(response_json); // 释放 cJSON 对象内存
return STATUS_MENU;

4.3.2 注册逻辑 fun_st2_regist()

  • 用户输入获取:同登录逻辑,用户需要输入用户名和密码。
// 获取用户输入的用户名和密码
void getUsernameAndPassword(char *username, char *password) {
    // 获取并验证用户名
    do {
        printf("请输入用户名(只能包含字母、数字和下划线):");
        fgets(username, MAX_SIZE, stdin);
        username[strcspn(username, "\n")] = 0; // 移除换行符
        if (!isValidInput(username)) {
            printf("输入不合法,请重新输入。\n");
        }
    } while (!isValidInput(username));

    // 获取并验证密码
    do {
        printf("请输入密码(只能包含字母、数字和下划线):");
        fgets(password, MAX_SIZE, stdin);
        password[strcspn(password, "\n")] = 0; // 移除换行符
        if (!isValidInput(password)) {
            printf("输入不合法,请重新输入。\n");
        }
    } while (!isValidInput(password));
}
  • JSON 消息构建:将输入的用户名和密码封装进 JSON 对象,标记消息类型为 MSG_REGIST
// 客户端注册请求示例
{
    "username": "aaa",
    "password": "123456",
    "message_type": 1  // MSG_REGIST 
}
  • 消息发送:通过 TCP 套接字将注册信息发送到服务端。

  • 响应处理:客户端接收并解析服务端的响应,根据响应内容给出相应提示。

// 客户端注册状态
ClientState fun_st2_regist() {
	// ...
    response_type = status_item->valueint;
    if (response_type == MSG_REGIST_FAILED)
    {
        printf("用户名已存在,请尝试使用其他用户名。\n");
        cJSON_Delete(response_json);
        return STATUS_MENU;
    }
    else if (response_type == MSG_REGIST)
    {
        printf("注册成功! 您的用户ID为: %d\n", cJSON_GetObjectItem(cJSON_GetObjectItem(response_json, "data"), "user_id")->valueint);
        cJSON_Delete(response_json);
        return STATUS_MENU; // 注册成功,返回到主菜单
    }

    cJSON_Delete(response_json);
    return STATUS_MENU;
}

4.4 聊天逻辑

4.4.1 发送消息 fun_st3_send_msg()

  • 消息输入提示:显示当前用户的用户名和 ID,并提示输入消息。
  • 启动接收线程:使用 pthread_create 创建一个新线程,运行 receive_and_display_messages 函数,用于在后台接收和显示来自其他用户的广播消息。
  • 消息处理循环
    • 循环等待用户在终端输入消息。
    • 如果用户输入 exit,则取消接收线程并等待其结束,然后清屏并返回到主菜单状态。
    • 如果输入的消息非空,构造一个 JSON 对象,包含用户 ID、消息内容和消息类型(MSG_SEND),并发送 JSON 消息到服务器。
    • 在每次发送消息后,打印提示符以等待下一条消息。
  • 返回状态:如果循环被中断,函数返回到主菜单状态。
// 客户端聊天状态
ClientState fun_st3_send_msg() {
    char message[MAX_SIZE];
    char input;

    printf("=== 用户:%s  编号:%d  已登录,输入 exit 表示退出聊天 ===\n", user_name, user_id);
    printf(">>> ");

    // 启动接收并显示消息的线程
    pthread_t receive_thread;
    if (pthread_create(&receive_thread, NULL, (void *)receive_and_display_messages, NULL) != 0) {
        perror("Error creating receive thread");
        return STATUS_SEND_MSG;
    }

    while (fgets(message, MAX_SIZE, stdin) != NULL) {
        message[strcspn(message, "\n")] = 0; // 移除换行符
        if (strcmp(message, "exit") == 0) {
            // 等待接收线程退出
            pthread_cancel(receive_thread);
            pthread_join(receive_thread, NULL);
            clearScreen();
            return STATUS_MENU; // 用户选择退出,返回到菜单
        }

        if (strlen(message) > 0) {
            cJSON *jsonObject = cJSON_CreateObject();
            cJSON_AddNumberToObject(jsonObject, "user_id", user_id);
            cJSON_AddStringToObject(jsonObject, "content", message);
            cJSON_AddNumberToObject(jsonObject, "message_type", MSG_SEND);
            sendJsonMessage(sockfd, jsonObject);
            cJSON_Delete(jsonObject);
        }

        pthread_mutex_lock(&mutex);
        printf(">>> ");
        pthread_mutex_unlock(&mutex);
    }

    return STATUS_MENU;
}

4.4.2 接收广播消息 receive_and_display_messages()

该函数在一个单独的线程中运行,用于接收和显示其他用户发送的广播消息:

  • 接收消息循环
    • 循环使用 read 函数从套接字 sockfd 读取服务器的响应。
    • 如果接收到的字节数为 0 或负数,循环终止。
  • 解析和显示消息
    • 使用 cJSON_Parse 解析收到的 JSON 消息。
    • 如果解析失败,打印错误信息。
    • 如果解析成功,检查消息类型。如果是 MSG_BROADCAST,则提取用户 ID 和消息内容,并显示到终端。
  • 消息处理结束:一旦接收消息循环结束,函数将退出。
// 在客户端接收并显示消息
void receive_and_display_messages() {
    char response[MAX_SIZE];

    while (1) {
        int bytes_received = read(sockfd, response, MAX_SIZE);
        if (bytes_received <= 0) {
            break;
        }
        response[bytes_received] = '\0';
        // printf("接收广播响应:%s\n", response);

        cJSON *message = cJSON_Parse(response);
        if (message == NULL) {
            // 处理无效 JSON 数据的情况
            printf("\n无效的 JSON 数据:%s\n", response);
        }
        else {
            // 继续处理有效的 JSON 数据
            int message_type = cJSON_GetObjectItem(message, "message_type")->valueint;

            // 加锁以确保线程安全
            pthread_mutex_lock(&mutex);

            if (message_type == MSG_BROADCAST) {
                const int userId = cJSON_GetObjectItem(message, "user_id")->valueint;
                const char *content = cJSON_GetObjectItem(message, "content")->valuestring;

                printf("\r[用户 %d]: %s\n", userId, content);

                // 打印下一行的提示符
                printf(">>> ");
                fflush(stdout);  // 确保输出被立即打印
            }

            pthread_mutex_unlock(&mutex);
            cJSON_Delete(message);
        }
    }
}

5. 服务端实现

5.1 服务线程 serviceThread()

该函数是每个客户端连接后在新线程中执行的函数,其处理来自客户端的请求,并根据请求的类型返回相应的响应。确保服务器可以同时处理多个客户端的请求,使服务器能够扩展到处理多个并发连接。

// 服务线程
void *serviceThread(void *arg) {
    // 处理套接字文件描述符
    // 初始化 JSON 对象
    // ...

    // 消息处理循环
    while (1) {
        // ...
        // 获取消息类型
        int message_type = cJSON_GetObjectItem(request, "message_type")->valueint;

        // 处理不同类型的消息
        switch (message_type) {
            case MSG_LOGIN: {
                response = handleLoginMessage(request, client_sockfd);
                break;
            }
            case MSG_REGIST: {
                response = handleRegistMessage(request, client_sockfd);
                break;
            }
            case MSG_SEND: {
                response = handleSendMessage(request);
                break;
            }
			// 其他类型消息处理...
        }

        if (response != NULL) {
            char *responseString = cJSON_Print(response);

            // 发送 JSON 响应到客户端
            write(client_sockfd, responseString, strlen(responseString));

            free(responseString);
            cJSON_Delete(response);
        }
        cJSON_Delete(request);
    }

    close(client_sockfd); // 关闭客户端套接字
    return NULL;
}

5.2 客户端管理

  • 定义链表节点,用于存储客户端信息
struct ClientNode
{
    int user_id;
    int sockfd;
    struct ClientNode *next;
};
// 定义全局链表头
struct ClientNode *clients = NULL;
  • 添加客户端到链表:当新客户端连接到服务器时,将该客户端的信息添加到链表中
// 将客户端添加到链表
void addClient(int user_id, int sockfd) {
    struct ClientNode *newNode = (struct ClientNode *)malloc(sizeof(struct ClientNode));
    newNode->user_id = user_id;
    newNode->sockfd = sockfd;
    newNode->next = NULL;

    pthread_mutex_lock(&clients_mutex); // 加锁以确保线程安全
    if (clients == NULL) {
        clients = newNode;
    } else {
        struct ClientNode *current = clients;
        while (current->next != NULL) {
            current = current->next;
        }
        current->next = newNode;
    }
    pthread_mutex_unlock(&clients_mutex); // 解锁
}
  • 从链表中移除客户端:当客户端断开连接或出现错误时,从链表中移除相应的客户端信息。
// 从链表中移除客户端
void removeClient(int sockfd) {
    pthread_mutex_lock(&clients_mutex); // 加锁
    struct ClientNode *current = clients;
    struct ClientNode *previous = NULL;

    while (current != NULL) {
        if (current->sockfd == sockfd) {
            if (previous == NULL) {
                clients = current->next;
            } else {
                previous->next = current->next;
            }
            free(current);
            break;
        }
        previous = current;
        current = current->next;
    }
    pthread_mutex_unlock(&clients_mutex); // 解锁
}

客户端管理模块的关键在于维护一个链表,该链表存储了所有当前连接的客户端信息。通过 addClientremoveClient 两个函数,服务器能够有效地管理客户端的连接和断开,确保了资源的正确分配和释放。互斥锁的使用保证了在多线程环境下的线程安全。

5.3 用户信息处理

5.3.1 读取用户信息 readUsers()

从文件中读取用户信息,并将其存储在一个 cJSON 对象中

  • 打开文件:首先尝试打开存储用户信息的文件(user.json)。如果文件不存在,则创建一个新的 cJSON 对象,并添加一个空的 users 数组。这是用于存储用户信息的 JSON 结构。
  • 读取文件内容:如果文件存在且非空,读取其内容到一个缓冲区中。
  • 解析 JSON 数据:使用 cJSON 解析读取到的文件内容。如果解析成功,遍历解析得到的 users 数组。
// 读取并解析JSON数据
fseek(file, 0, SEEK_SET);
char *buffer = (char *)malloc(len + 1);
fread(buffer, 1, len, file);
buffer[len] = '\0';

root = cJSON_Parse(buffer);
if (!root) {
    // 解析失败,初始化空的JSON对象
    root = cJSON_CreateObject();
    cJSON_AddArrayToObject(root, "users");
}
else {
    // 解析成功,处理用户数据
    cJSON *users = cJSON_GetObjectItem(root, "users");
    int n_users = cJSON_GetArraySize(users);
    for (int i = 0; i < n_users; i++) {
        cJSON *user = cJSON_GetArrayItem(users, i);
        cJSON *user_id = cJSON_GetObjectItem(user, "user_id");
        cJSON *username = cJSON_GetObjectItem(user, "username");
        cJSON *password = cJSON_GetObjectItem(user, "password");

        if (user_id && username && password) {
            printf("User ID: %d, Username: %s, Password: %s\n",
                   user_id->valueint, username->valuestring, password->valuestring);
        }
    }
}

5.3.2 保存用户信息到 JSON 文件 saveUsers()

将修改后的用户信息(存储在 cJSON 对象中)保存回文件

  • 打开文件:打开(或创建)用户信息文件(user.json)以供写入。
  • 加锁:为了线程安全,使用互斥锁在操作文件之前加锁。
  • 写入 JSON 数据:将 cJSON 对象转换为字符串,并写入文件。
  • 关闭文件并解锁:关闭文件,并释放互斥锁。

5.4 消息处理逻辑

5.4.1 处理登录请求 handleLoginMessage()

  • 从客户端接收的 JSON 请求中提取用户名和密码,遍历服务器端存储的用户信息,检查是否有匹配的用户。
cJSON *handleLoginMessage(cJSON *request, int client_fd) {
    // ... [省略初始化和声明]

    const char *username = cJSON_GetObjectItem(request, "username")->valuestring;
    const char *password = cJSON_GetObjectItem(request, "password")->valuestring;

    int login_failed = 1; // 标识登录是否失败
    cJSON *users = cJSON_GetObjectItem(root, "users");
    cJSON *user = NULL;

    cJSON_ArrayForEach(user, users) {
        cJSON *idObj = cJSON_GetObjectItem(user, "user_id");
        cJSON *userObj = cJSON_GetObjectItem(user, "username");
        cJSON *passwordObj = cJSON_GetObjectItem(user, "password");

        if (userObj && passwordObj &&
            strcmp(userObj->valuestring, username) == 0 &&
            strcmp(passwordObj->valuestring, password) == 0) {
            // 登录成功
            login_failed = 0;
            cJSON *response_data = cJSON_CreateObject();
            cJSON_AddNumberToObject(response_data, "user_id", idObj->valueint);
            response = createResponse(MSG_LOGIN, "登录成功", response_data);
            addClient(idObj->valueint, client_fd);
            break;
        }
    }

    if (login_failed) {
        response = createResponse(MSG_LOGIN_FAILED, "登录失败", NULL);
    }

    return response;
}
  • 如果认证成功,会返回一个包含用户 ID 的成功响应;如果失败,则返回一个登录失败的响应。
// 服务端登录响应示例
{
    "message_type": 0,  // MSG_LOGIN
    "data": {
        "user_id": 1001
    }
}

5.4.2 处理注册请求 handleRegistMessage()

  • 首先遍历已注册的用户信息,检查用户名是否已存在
cJSON *users = cJSON_GetObjectItem(root, "users");
cJSON *user = NULL;
int user_exists = 0;

cJSON_ArrayForEach(user, users) {
    cJSON *userObj = cJSON_GetObjectItem(user, "username");
    if (userObj && strcmp(userObj->valuestring, username) == 0)
    {
        user_exists = 1;
        break;
    }
}
  • 如果用户名已存在,则创建一个 JSON 响应,指示注册失败(MSG_REGIST_FAILED)
  • 如果不存在,就创建一个新用户,将其信息添加到用户列表中,并保存到文件中。最后,返回一个表明注册结果(成功或失败)的 JSON 响应。
cJSON *data = cJSON_CreateObject();
cJSON_AddNumberToObject(data, "user_id", new_user_id);
response = createResponse(MSG_REGIST, "注册成功", data);

5.4.3 处理发送消息请求 handleSendMessage()

  • 获取消息和用户信息:
    • 从传入的 request cJSON 对象中提取 user_idcontentuser_id 是消息发送者的用户 ID,而 content 是发送的消息内容。
    • 根据 user_id 查找对应的用户名。
  • 广播消息:
    • 调用 broadcastMessageToAllExceptSender 函数,将接收到的消息广播给除发送者之外的所有在线用户。
    • 广播内容包括发送者的用户 ID 和消息内容。
  • 创建响应:
    • 创建一个 cJSON 对象作为响应。这个响应包含消息类型(MSG_SEND),表示消息已被成功接收,并准备发送给其他用户。
// 处理 MSG_SEND 类型的消息
cJSON *handleSendMessage(cJSON *request) {
    cJSON *response = NULL; // 存储将要生成的响应

    int user_id = cJSON_GetObjectItem(request, "user_id")->valueint;
    const char *content = cJSON_GetObjectItem(request, "content")->valuestring;
    char *username = getUsernameFromUserID(user_id);

    printf("接收到来自用户 %s (ID: %d) 的消息: %s\n", username, user_id, content);

    // 调用广播消息函数,将消息发送给其他用户
    broadcastMessageToAllExceptSender(user_id, content);

    response = createResponse(MSG_SEND, "消息发送成功", NULL);

    // printf("返回响应:%s\n", cJSON_Print(response));

    return response;
}

5.4.4 处理退出请求 handleQuitMessage()

处理客户端发送的退出消息 (MSG_QUIT),确保客户端从服务器的在线客户端列表中移除

// 处理 MSG_QUIT 类型的消息
cJSON *handleQuitMessage(cJSON *request, int client_fd) {
    cJSON *response = createResponse(MSG_QUIT, "退出系统!", NULL);
    int user_id = cJSON_GetObjectItem(request, "user_id")->valueint;

    // 加锁
    pthread_mutex_lock(&online_users_mutex);

    // 关闭连接,删除链表中的在线用户
    removeClient(client_fd);

    // 解锁
    pthread_mutex_unlock(&online_users_mutex);

    return response;
}

5.5 广播消息逻辑

broadcastMessageToAllExceptSender() 函数遍历所有在线的客户端(通过链表 clients 维护),并将消息发送给除了消息发送者之外的每个客户端。涉及以下步骤:

  • 锁定客户端列表:
    • 使用互斥锁 clients_mutex 保护客户端列表,在遍历过程中防止并发修改。
  • 遍历客户端列表:
    • 对于链表中的每个节点(即每个在线客户端),检查其用户 ID 是否与消息发送者的 ID 相同。
    • 如果不是发送者,则创建一个新的 cJSON 对象,包含消息类型(MSG_BROADCAST)、发送者的用户 ID 和消息内容。
  • 发送消息:
    • 将 cJSON 对象转换为 JSON 字符串,并通过套接字发送给目标客户端。
  • 资源清理:
    • 删除 cJSON 对象并释放相关内存。
  • 解锁客户端列表:
    • 完成遍历和消息发送后,释放互斥锁。
void broadcastMessageToAllExceptSender(int sender_user_id, const char *content) {
    pthread_mutex_lock(&clients_mutex);
    struct ClientNode *current = clients;

    while (current != NULL) {
        if (current->user_id != sender_user_id) {
            cJSON *broadcast_data = cJSON_CreateObject();
            cJSON_AddNumberToObject(broadcast_data, "user_id", sender_user_id);
            cJSON_AddStringToObject(broadcast_data, "content", content);
            cJSON_AddNumberToObject(broadcast_data, "message_type", MSG_BROADCAST);

            char *broadcastString = cJSON_Print(broadcast_data);
            if (broadcastString != NULL) {
                write(current->sockfd, broadcastString, strlen(broadcastString));
                free(broadcastString);
            }

            cJSON_Delete(broadcast_data);
        }

        current = current->next;
    }

    pthread_mutex_unlock(&clients_mutex);
}

6. 实现效果

  • 客户端

  • 服务端

7. 总结和心得

  • 总结

    • 功能实现:简单的聊天室程序,实现了终端字符界面中用户注册、登录以及聊天的功能。

    • 状态机编程:客户端可以根据用户的选择切换不同的功能模块,用于控制程序的流程和行为。

    • 消息传递格式:客户端和服务器端的消息传递格式为 JSON 格式,使用了 cJSON 库进行处理。

    • 多线程:实现了一个接收消息的线程,以实时显示来自其他用户的消息。

    • 链表管理客户端:定义了一个链表来管理在线客户端,确保在添加和删除客户端时正确地处理链表的链接和内存分配。

    • 互斥问题:使用互斥锁来保护对在线用户列表的访问,确保在多线程环境下的数据一致性和线程安全。

  • 心得

    • JSON 内存管理:最初在处理 JSON 对象时出现了段错误。问题在于对 JSON 对象的错误内存管理。当一个 JSON 对象被添加到另一个 JSON 对象时,它的内存管理权转移给了父对象。因此,我不应该单独删除子对象,否则会导致重复释放内存。

    • 多线程资源共享:在多线程环境下,对全局变量的访问需要特别小心。使用互斥锁来保护全局变量,避免了数据竞争和不一致的问题。

标签:聊天室,cJSON,Ubuntu22.04,response,简易,user,MSG,id,客户端
From: https://www.cnblogs.com/thr-0103/p/17984853

相关文章

  • 编写简易斜45度地图编辑器
    最近在研究cocos2dx的地图,最开始使用的是Tiled,这个编辑器做比较小的地图还是比较强大的,不过做大地图的时候,有一些功能不太方便并且有缺陷(包括刷图繁琐以及坐标体系过于复杂,导致寻路比较看起来很不平滑)。于是就酝酿着自己写一个斜45度的地图编辑器。     现在的自己老是不能......
  • ubuntu22.04 mysql服务每天自动shutdown问题
    1.问题描述MYSQL每天自动关闭,查看/var/log/mysql/error.log.1.gz,内容如下:2019-06-12T06:33:13.582973+08:000[Note]Shuttingdownplugin‘CONNECTION_CONTROL_FAILED_LOGIN_ATTEMPTS’2019-06-12T06:33:13.583022+08:000[Note]Shuttingdownplugin‘CONNECTION_CON......
  • python3使用socket模块实现简易syslog服务器
    废话不多说直接上代码1importsocket2importtime345defsocket_bind(server_socket,host,port=514,max_retries=5):6retries=07whileretries<max_retries:8try:9server_socket.bind((host,port))10......
  • 正确理解springboot国际化简易运行流程
    看源码可以看出–》大致原理localeResolver国际化视图(默认的就是根据请求头带来的区域信息获取Locale进行国际化)返回的本地解析是根据响应头来决定的)接着按住ctrl点localeresolver可知localeresolver是一个接口于是有了这些我们只需通过继承LocaleResolver来自定义我们自己的Loca......
  • WSL2+Ubuntu22.04+Vscode 虚拟环境安装
    1.WSL2+Ubuntu22.04+Vscode虚拟环境安装详细攻略WIN11+WSL2+Ubuntu22.04+CUDA+MINICONDA3+Pytorch安装踩坑总结,手把手教学,看不会你打我1.1先决配置1.2安装命令官网https://learn.microsoft.com/zh-cn/windows/wsl/basic-commandswsl--install--no-distributio......
  • WSL2+Ubuntu22.04+Vscode 虚拟环境安装
    1.WSL2+Ubuntu22.04+Vscode虚拟环境安装详细攻略WIN11+WSL2+Ubuntu22.04+CUDA+MINICONDA3+Pytorch安装踩坑总结,手把手教学,看不会你打我1.1先决配置1.2安装命令官网https://learn.microsoft.com/zh-cn/windows/wsl/basic-commandswsl--install--no-distributio......
  • 简易Android名片制作
    classMainActivity:ComponentActivity(){overridefunonCreate(savedInstanceState:Bundle?){super.onCreate(savedInstanceState)setContent{MyCardTheme{//Asurfacecontainerusingthe'background......
  • 阿里云rds云数据恢复至自建数据库 (linux 服务器版本ubuntu22.04)
    一、准备1.安装mysql5.7注意:需要跟rds云数据库版本对应2.安装PerconaXtraBackup工具,将解压后的备份文件恢复到自建数据库的数据目录中3.下载需要还原的物理备份文件我的是.qp类型wget-c'https://****.bak.rds.aliyuncs.com/****_xb.qp?****'-Oins2......
  • ubuntu22.04回退系统内核
    开机之后突然发现nvidia-smi检查不到驱动了,发现因为内核自动更新导致的,这里介绍一下内核回退的方法。第一步:查看当前内核版本uname-a我的当前内核版本是:6.5.0-14-generic第二步:查看系统现存的内存版本grepmenuentry/boot/grub/grub.cfg我的系统存在两个内核版本:6.5.0-14-......
  • Ubuntu22.04安装Mysql
    1、下载mysql1.1使用仓库安装工具下载wgethttps://dev.mysql.com/get/mysql-apt-config_0.8.29-1_all.deb安装使用sudodpkg-i./mysql-apt-config_0.8.29-1_all.deb1.2安装mysql更新仓库sudoaptupgradesudoaptupdate安装mysqlsudoapt-getinstall......