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
状态,表示用户选择了退出程序。
- 如果用户输入
- 处理无效输入:如果用户输入了除
1
、2
或3
以外的字符,函数会认为这是一个无效输入。此时,它会提示用户输入不合法,并重新展示菜单,要求用户重新输入。 - 代码实现:
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); // 解锁
}
客户端管理模块的关键在于维护一个链表,该链表存储了所有当前连接的客户端信息。通过 addClient
和 removeClient
两个函数,服务器能够有效地管理客户端的连接和断开,确保了资源的正确分配和释放。互斥锁的使用保证了在多线程环境下的线程安全。
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_id
和content
。user_id
是消息发送者的用户 ID,而content
是发送的消息内容。 - 根据
user_id
查找对应的用户名。
- 从传入的
- 广播消息:
- 调用
broadcastMessageToAllExceptSender
函数,将接收到的消息广播给除发送者之外的所有在线用户。 - 广播内容包括发送者的用户 ID 和消息内容。
- 调用
- 创建响应:
- 创建一个 cJSON 对象作为响应。这个响应包含消息类型(
MSG_SEND
),表示消息已被成功接收,并准备发送给其他用户。
- 创建一个 cJSON 对象作为响应。这个响应包含消息类型(
// 处理 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 对象时,它的内存管理权转移给了父对象。因此,我不应该单独删除子对象,否则会导致重复释放内存。
-
多线程资源共享:在多线程环境下,对全局变量的访问需要特别小心。使用互斥锁来保护全局变量,避免了数据竞争和不一致的问题。
-