2025.8.31基于UDP的网络聊天室项目
今天我们来看一下第三个项目,基于UDP的网络聊天室,我们来简单看一下下面的任务需求
- 如果有用户登录,其他用户可以收到这个人的登录信息
- 如果有人发送信息,其他用户可以收到这个人的群聊信息
- 如果有人下线,其他用户可以收到这个人的下线信息
- 服务器可以发送系统信息
我们来简单的分析一下要我们都做些什么事情,我们要设计的整体架构采用 UDP客户端 - 服务器Linux系统下的C语言 架构。服务器作为核心枢纽,负责接收、处理客户端的消息,并向各个客户端转发相应信息;客户端则用于用户交互,发送登录、聊天、下线等消息,以及接收服务器推送的各类信息。
关于数据结构设计我们要创造消息结构体(struct Msg):包含 type(消息类型,如 'L' 表示登录、'C' 表示聊天、'Q' 表示下线、'S' 表示系统消息)、name(发送消息的用户名)、text(消息内容)等字段,用于统一封装各类消息,方便服务器和客户端进行数据传输与解析。
客户端信息存储:服务器需维护一个容器(如链表、数组、集合等),用于存放已登录客户端的地址信息(如套接字、网络地址等)以及对应的用户名等标识信息,以便服务器能准确地向各个客户端转发消息。
一、服务器逻辑:消息接收与分类处理:持续监听客户端的连接与消息。当接收到消息后,根据 Msg 结构体中的 type 字段进行分类处理。
若类型为 'L'(登录):将该客户端的信息加入存储容器,并构造包含该用户登录信息的消息,向容器内所有其他客户端转发,让其他用户知晓有人登录。
若类型为 'C'(聊天):直接构造包含该用户聊天内容的消息,转发给所有其他客户端,实现群聊消息共享。
若类型为 'Q'(下线):从存储容器中移除该客户端的信息,构造包含该用户下线信息的消息,转发给所有其他客户端,告知其他用户有人下线。
系统消息发送:服务器可主动构造 type 为 'S' 的消息,向所有已登录的客户端(即存储容器内的客户端)发送系统通知,比如 “系统维护通知” 等。
二、客户端逻辑
消息发送:提供用户交互界面,让用户输入用户名、选择操作(登录、发送消息、下线等),并将相应信息封装成 struct Msg 格式的消息发送给服务器。
消息接收与展示:持续监听服务器推送的消息,接收到消息后,解析 struct Msg,并在客户端界面展示出来,让用户能看到其他用户的登录、聊天、下线信息以及系统消息。
网络通信实现:可选用套接字(Socket)编程来实现网络通信,在 UDP 协议下,能保证消息并发执行传递给个客户端,确保登录、聊天、下线等消息能准确到达服务器和客户端。
我们大致了解了要干什么,现在我们来明确一下思路,关于通信模型:采用 C/S 架构,服务器作为消息转发中心,使用 UDP 协议 (SOCK_DGRAM) 实现无连接通信,适合简单群聊场景;核心数据结构:定义Chatmsg结构体统一消息格式,通过type字段区分消息类型,服务器端用Client_data结构体维护客户端信息,包括网络地址、用户名和在线状态。
服务器核心逻辑:
1、绑定固定 IP 和端口,持续接收客户端消息
2、对不同类型消息 (type) 进行分类处理:
登录消息 ('L'):添加客户端到列表,广播登录通知
聊天消息 ('C'):直接广播给所有在线用户
退出消息 ('Q'):标记客户端离线,广播退出通知
3、通过broadcast_message函数实现消息群发,自动排除发送者
客户端核心逻辑:
1、使用多线程实现 "发送消息" 和 "接收消息" 并行处理
2、主线程负责获取用户输入并发送消息
3、子线程持续监听服务器发来的消息并格式化显示
4、处理特殊指令 "quit" 实现优雅退出,发送退出通知
为大家奉上我的源码及注释:
服务器:
#include <myhead.h>
#define PORT 6666 // 服务器端口号
#define IP "192.168.0.103" // 服务器IP地址
#define CLIENT_MAX 100 // 最大客户端连接数// 消息结构体定义
typedef struct
{char type; // 消息类型:L(登录)/C(聊天)/Q(退出)char name[20]; // 发送者用户名char text[100]; // 消息内容
} Chatmsg;// 客户端信息结构体
typedef struct
{struct sockaddr_in addr; // 客户端网络地址信息char name[20]; // 客户端用户名int is_online; // 在线状态标记(1:在线,0:离线)
} Client_data;Client_data clients[CLIENT_MAX]; // 客户端列表
int client_count = 0; // 当前在线客户端数量// 广播消息给所有在线客户(排除发送者)
void broadcast_message(int oldfd, Chatmsg *msg, struct sockaddr_in *sender)
{for (int i = 0; i < client_count; i++){// 判断客户端在线且不是消息发送者if (clients[i].is_online && !(clients[i].addr.sin_addr.s_addr == sender->sin_addr.s_addr && clients[i].addr.sin_port == sender->sin_port)){sendto(oldfd, msg, sizeof(Chatmsg), 0, (struct sockaddr *)&clients[i].addr, sizeof(clients[i].addr));}}
}// 添加客户端到列表(已存在则更新状态)
void add_client(struct sockaddr_in *client_addr, char *name)
{// 检查是否已存在该客户端for (int i = 0; i < client_count; i++){if (clients[i].addr.sin_addr.s_addr == client_addr->sin_addr.s_addr && clients[i].addr.sin_port == client_addr->sin_port){clients[i].is_online = 1;strncpy(clients[i].name, name, sizeof(clients[i].name)-1);return;}}// 添加新客户端if (client_count < CLIENT_MAX){clients[client_count].addr = *client_addr;strncpy(clients[client_count].name, name, sizeof(clients[client_count].name)-1);clients[client_count].is_online = 1;client_count++;}
}// 移除客户端(标记为离线)
void remove_client(struct sockaddr_in *client_addr)
{for (int i = 0; i < client_count; i++){if (clients[i].addr.sin_addr.s_addr == client_addr->sin_addr.s_addr && clients[i].addr.sin_port == client_addr->sin_port){clients[i].is_online = 0;break;}}
}int main(int argc, const char *argv[])
{// 创建UDP套接字int oldfd = socket(AF_INET, SOCK_DGRAM, 0);if (oldfd == -1){perror("socket");return -1;}// 配置服务器地址信息struct sockaddr_in server = {.sin_family = AF_INET,.sin_port = htons(PORT),.sin_addr.s_addr = inet_addr(IP)};// 绑定套接字到指定IP和端口if (bind(oldfd, (struct sockaddr *)&server, sizeof(server)) == -1){perror("bind");close(oldfd);return -1;}printf("服务器启动成功,监听端口: %d\n", PORT);Chatmsg msg; // 接收消息缓冲区struct sockaddr_in client; // 客户端地址信息socklen_t client_len = sizeof(client);// 初始化客户端列表memset(clients, 0, sizeof(clients));// 主循环:持续接收和处理消息while (1){recvfrom(oldfd, &msg, sizeof(Chatmsg), 0, (struct sockaddr *)&client, &client_len);// 根据消息类型处理switch (msg.type){case 'L': // 登录消息add_client(&client, msg.name);printf("[%s] 登录了聊天室\n", msg.name);strcpy(msg.text, "加入了聊天室");broadcast_message(oldfd, &msg, &client);break;case 'C': // 聊天消息printf("[%s] 说: %s\n", msg.name, msg.text);broadcast_message(oldfd, &msg, &client);break;case 'Q': // 退出消息printf("[%s] 退出了聊天室\n", msg.name);strcpy(msg.text, "离开了聊天室");broadcast_message(oldfd, &msg, &client);remove_client(&client);break;default:printf("收到未知类型消息: %c\n", msg.type);break;}}close(oldfd);return 0;
}
客户端:
#include <myhead.h>#define PORT 6666 // 服务器端口号
#define IP "192.168.0.103" // 服务器IP地址// 消息结构体定义(与服务器保持一致)
typedef struct
{char type; // 消息类型:L(登录)/C(聊天)/Q(退出)char name[20]; // 发送者用户名char text[100]; // 消息内容
} Chatmsg;int oldfd; // 套接字描述符
char username[20]; // 本地用户名
int running = 1; // 控制接收线程运行状态// 接收消息线程函数
void *recv_message(void *arg)
{struct sockaddr_in server_addr;socklen_t server_addr_len = sizeof(server_addr);Chatmsg msg;while (running){// 接收服务器消息ssize_t recv_len = recvfrom(oldfd, &msg, sizeof(Chatmsg), 0,(struct sockaddr *)&server_addr, &server_addr_len);if (recv_len < 0){perror("recvfrom 失败(接收消息)");sleep(1);continue;}// 确保字符串结束符msg.text[sizeof(msg.text)-1] = '\0';msg.name[sizeof(msg.name)-1] = '\0';printf("\r\033[K"); // 清空当前行(为了不影响输入提示)// 根据消息类型显示不同格式if (msg.type == 'L'){printf("[系统] %s %s\n", msg.name, msg.text);}else if (msg.type == 'C'){printf("[%s] 说: %s\n", msg.name, msg.text);}else if (msg.type == 'Q'){printf("[系统] %s %s\n", msg.name, msg.text);}printf("请输入消息(输入 quit 退出): ");fflush(stdout); // 刷新输出缓冲区}return NULL;
}int main(int argc, const char *argv[])
{// 创建UDP套接字oldfd = socket(AF_INET, SOCK_DGRAM, 0);if (oldfd == -1){perror("socket创建失败");return -1;}// 配置服务器地址信息struct sockaddr_in server = {.sin_family = AF_INET,.sin_port = htons(PORT),.sin_addr.s_addr = inet_addr(IP)};// 获取用户名printf("请输入用户名: ");fgets(username, sizeof(username), stdin);username[strcspn(username, "\n")] = '\0'; // 去除换行符// 发送登录消息Chatmsg login_msg = {'L'};strcpy(login_msg.name, username);strcpy(login_msg.text, "加入了聊天室");sendto(oldfd, &login_msg, sizeof(login_msg), 0, (struct sockaddr *)&server, sizeof(server));// 创建接收消息线程pthread_t tid;if (pthread_create(&tid, NULL, recv_message, NULL) != 0){perror("创建线程失败");return -1;}// 处理用户输入char input[100];while (1){printf("请输入消息(输入 quit 退出): ");fgets(input, sizeof(input), stdin);input[strcspn(input, "\n")] = '\0'; // 去除换行符// 退出逻辑if (strcmp(input, "quit") == 0){Chatmsg quit_msg = {'Q'};strcpy(quit_msg.name, username);strcpy(quit_msg.text, "离开了聊天室");sendto(oldfd, &quit_msg, sizeof(quit_msg), 0, (struct sockaddr *)&server, sizeof(server));running = 0; // 终止接收线程sleep(1); // 等待线程结束break;}// 发送聊天消息Chatmsg chat_msg = {'C'};strcpy(chat_msg.name, username);strcpy(chat_msg.text, input);sendto(oldfd, &chat_msg, sizeof(chat_msg),0,(struct sockaddr *)&server, sizeof(server));}pthread_join(tid, NULL); // 等待接收线程结束close(oldfd); // 关闭套接字printf("已退出聊天室\n");return 0;
}
就这样,我们完成了一个简单的聊天室,在这里我们涉及了到了,UDP通信,多线程处理,数据结构等多个知识点
1. 网络编程基础UDP 协议:使用 SOCK_DGRAM 创建 UDP 套接字,实现无连接的数据包传输。UDP 适合简单通信场景,但不保证可靠性(代码中未处理丢包重传)。
socket():创建套接字描述符,指定协议族(AF_INET 表示 IPv4)和传输类型。
bind():将服务器套接字绑定到指定 IP 和端口,用于监听客户端消息。
sendto()/recvfrom():UDP 专用的发送 / 接收函数,需指定目标地址和地址长度。
网络地址结构:使用 struct sockaddr_in 存储 IP 地址(sin_addr)、端口号(sin_port)和协议族(sin_family),通过 inet_addr() 转换 IP 字符串为网络字节序,htons() 转换端口号为网络字节序。
2. 多线程编程
客户端使用 pthread_create() 创建子线程,专门负责接收服务器消息(recv_message 函数),主线程负责处理用户输入和发送消息,实现 “发送” 与 “接收” 并行。
线程同步与控制:通过全局变量 running 控制子线程的运行状态,退出时使用 pthread_join() 等待子线程结束,避免资源泄露。
3. 数据结构与内存操作自定义结构体:Chatmsg:统一消息格式,包含消息类型(type)、用户名(name)和内容(text),确保客户端与服务器的消息解析一致。
Client_data:服务器用于存储客户端信息(网络地址、用户名、在线状态),通过数组 clients 管理多个客户端。
我们来简单的使用一下这个代码
gcc 文件名.c -o 程序名编译一下服务器
gcc 文件名.c -o 程序名 -lpthread 编译一下客户端,注意这里面的客户端包含多线程,编译的时候要加上-lpthread
然后我们分屏执行一下程序看看有什么效果
看得出来,服务器监听等待客户端加入响应。
来看一下我们的程序都实现了哪些效果
1. 多客户端登录与通知
当新客户端(如用户 “wubai”“张三”)登录时,服务器会向所有在线客户端广播该用户的登录信息。其他客户端能收到类似 “[系统] wubai 加入了聊天室”“[系统] 张三 加入了聊天室” 的提示,让所有用户知晓新成员的加入。
2. 群聊消息实时同步
客户端发送的聊天消息(如 “你好,我叫伍柏”“你好,我叫张三”“张三你好,你来自哪里”“你好,伍柏,我来自河南” 等),会通过服务器转发给所有在线的其他客户端。每个客户端都能实时看到其他用户发送的消息,实现群聊功能。
3. 客户端下线通知
当客户端(如用户 “wubai”)输入 “quit” 退出时,会向服务器发送下线消息。服务器收到后,会向所有在线客户端广播该用户的下线信息,其他客户端会收到 “[系统] wubai 离开了聊天室” 的提示,让大家知道有用户下线。
4. 服务器作为消息枢纽
服务器在整个过程中,负责接收客户端的登录、聊天、下线等消息,并将这些消息转发给对应的目标客户端(登录和下线消息是广播给所有客户端,聊天消息是转发给除发送者外的所有客户端),起到了消息中转站的作用,保障了多客户端之间的通信。
以上我们就把这个小项目完成了,这个项目涉及的知识点也是挺多的,大家可以互相借鉴学习。