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

今天我们来看一下第三个项目,基于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" 实现优雅退出,发送退出通知

为大家奉上我的源码及注释:

服务器:

cs 复制代码
#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;
}

客户端:

cs 复制代码
#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() 转换端口号为网络字节序。

  1. 多线程编程

客户端使用 pthread_create() 创建子线程,专门负责接收服务器消息(recv_message 函数),主线程负责处理用户输入和发送消息,实现 "发送" 与 "接收" 并行。

线程同步与控制:通过全局变量 running 控制子线程的运行状态,退出时使用 pthread_join() 等待子线程结束,避免资源泄露。

  1. 数据结构与内存操作自定义结构体:Chatmsg:统一消息格式,包含消息类型(type)、用户名(name)和内容(text),确保客户端与服务器的消息解析一致。

Client_data:服务器用于存储客户端信息(网络地址、用户名、在线状态),通过数组 clients 管理多个客户端。

我们来简单的使用一下这个代码

gcc 文件名.c -o 程序名编译一下服务器

gcc 文件名.c -o 程序名 -lpthread 编译一下客户端,注意这里面的客户端包含多线程,编译的时候要加上-lpthread

然后我们分屏执行一下程序看看有什么效果

看得出来,服务器监听等待客户端加入响应。

来看一下我们的程序都实现了哪些效果

  1. 多客户端登录与通知

当新客户端(如用户 "wubai""张三")登录时,服务器会向所有在线客户端广播该用户的登录信息。其他客户端能收到类似 "[系统] wubai 加入了聊天室""[系统] 张三 加入了聊天室" 的提示,让所有用户知晓新成员的加入。

  1. 群聊消息实时同步

客户端发送的聊天消息(如 "你好,我叫伍柏""你好,我叫张三""张三你好,你来自哪里""你好,伍柏,我来自河南" 等),会通过服务器转发给所有在线的其他客户端。每个客户端都能实时看到其他用户发送的消息,实现群聊功能。

  1. 客户端下线通知

当客户端(如用户 "wubai")输入 "quit" 退出时,会向服务器发送下线消息。服务器收到后,会向所有在线客户端广播该用户的下线信息,其他客户端会收到 "[系统] wubai 离开了聊天室" 的提示,让大家知道有用户下线。

  1. 服务器作为消息枢纽

服务器在整个过程中,负责接收客户端的登录、聊天、下线等消息,并将这些消息转发给对应的目标客户端(登录和下线消息是广播给所有客户端,聊天消息是转发给除发送者外的所有客户端),起到了消息中转站的作用,保障了多客户端之间的通信。

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

相关推荐
K_i13426 分钟前
云原生网络基础:IP、端口与网关实战
网络·ip·接口隔离原则
m0_6515939143 分钟前
Netty网络架构与Reactor模式深度解析
网络·架构
大面积秃头1 小时前
Http基础协议和解析
网络·网络协议·http
Michael_lcf2 小时前
Java的UDP通信:DatagramSocket和DatagramPacket
java·开发语言·udp
我也要当昏君3 小时前
6.3 文件传输协议 (答案见原书 P277)
网络
Greedy Alg3 小时前
Socket编程学习记录
网络·websocket·学习
刘逸潇20054 小时前
FastAPI(二)——请求与响应
网络·python·fastapi
软件技术员4 小时前
使用ACME自动签发SSL 证书
服务器·网络协议·ssl
我也要当昏君4 小时前
6.4 电子邮件 (答案见原书 P284)
网络协议
Mongnewer5 小时前
通过虚拟串口和网络UDP进行数据收发的Delphi7, Lazarus, VB6和VisualFreeBasic实践
网络