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 *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; |
| |
| } |
| } |
- 显示菜单选项:函数首先打印出聊天室服务系统的主菜单,包括选项
登录
、注册
和 退出系统
。
- 读取用户输入:函数使用
getchar()
函数从标准输入读取一个字符代表用户的选择。
- 处理额外的输入:为了防止之后的输入受到之前输入的影响,使用一个循环来清除输入缓冲区中的剩余字符(包括回车符和换行符)。
- 根据输入切换状态:
- 如果用户输入
1
,函数返回 STATUS_LOGIN
状态,表示用户选择了登录。
- 如果输入
2
,则返回 STATUS_REGIST
状态,表示用户选择了注册。
- 如果输入
3
,则打印退出提示并返回 STATUS_QUIT
状态,表示用户选择了退出程序。
- 处理无效输入:如果用户输入了除
1
、2
或 3
以外的字符,函数会认为这是一个无效输入。此时,它会提示用户输入不合法,并重新展示菜单,要求用户重新输入。
- 代码实现:
fun_st1_menu()
| |
| ClientState fun_st1_menu() { |
| int input, ch; |
| printf("===== 聊天室服务系统 ======\n"); |
| printf("1 登录\n"); |
| printf("2 注册\n"); |
| printf("3 退出系统\n"); |
| printf("==========================\n"); |
| printf("请输入您的选择: "); |
| |
| while (1) { |
| |
| 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 *jsonObject = cJSON_CreateObject(); |
| |
| char username[MAX_SIZE]; |
| char password[MAX_SIZE]; |
| printf("========= 登录 =========\n"); |
| getUsernameAndPassword(username, password); |
| |
| |
| cJSON_AddStringToObject(jsonObject, "username", username); |
| cJSON_AddStringToObject(jsonObject, "password", password); |
| |
| cJSON_AddNumberToObject(jsonObject, "message_type", MSG_LOGIN); |
| |
| |
| sendJsonMessage(sockfd, jsonObject); |
| cJSON_Delete(jsonObject); |
| |
| |
| } |
- 消息发送:封装好的 JSON 消息通过 TCP 套接字发送到服务端。
| |
| { |
| "username": "user123", |
| "password": "password123", |
| "message_type": 0 |
| } |
- 响应处理:客户端根据服务端返回的响应判断登录是否成功。成功时,程序会保存用户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; |
| 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); |
| 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 |
| } |
| |
| 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'; |
| |
| |
| cJSON *message = cJSON_Parse(response); |
| if (message == NULL) { |
| |
| printf("\n无效的 JSON 数据:%s\n", response); |
| } |
| else { |
| |
| 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) { |
| |
| |
| |
| |
| |
| 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); |
| |
| |
| 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
数组。
| |
| 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) { |
| |
| 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, |
| "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 *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); |
| |
| |
| |
| return response; |
| } |
5.4.4 处理退出请求 handleQuitMessage()
处理客户端发送的退出消息 (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 字符串,并通过套接字发送给目标客户端。
- 资源清理:
- 解锁客户端列表:
| 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 库进行处理。
-
多线程:实现了一个接收消息的线程,以实时显示来自其他用户的消息。
-
链表管理客户端:定义了一个链表来管理在线客户端,确保在添加和删除客户端时正确地处理链表的链接和内存分配。
-
互斥问题:使用互斥锁来保护对在线用户列表的访问,确保在多线程环境下的数据一致性和线程安全。
-
心得:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端