2025.8.31基于UDP的网络聊天室项目

发布于:2025-09-01 ⋅ 阅读:(21) ⋅ 点赞:(0)

        今天我们来看一下第三个项目,基于UDP的网络聊天室,我们来简单看一下下面的任务需求

项目需求:
  1. 如果有用户登录,其他用户可以收到这个人的登录信息
  2. 如果有人发送信息,其他用户可以收到这个人的群聊信息
  3. 如果有人下线,其他用户可以收到这个人的下线信息
  4. 服务器可以发送系统信息

        我们来简单的分析一下要我们都做些什么事情,我们要设计的整体架构采用 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. 服务器作为消息枢纽
服务器在整个过程中,负责接收客户端的登录、聊天、下线等消息,并将这些消息转发给对应的目标客户端(登录和下线消息是广播给所有客户端,聊天消息是转发给除发送者外的所有客户端),起到了消息中转站的作用,保障了多客户端之间的通信。

        以上我们就把这个小项目完成了,这个项目涉及的知识点也是挺多的,大家可以互相借鉴学习。


网站公告

今日签到

点亮在社区的每一天
去签到