网络编程基础项目二:UDP聊天室

目录

项目功能介绍

问题思考

补充参考函数 

程序流程图

服务器

客户端

代码流程

代码

服务器

客户端

 总结


项目功能介绍

利用UDP协议,实现一套聊天室软件。服务器端记录客户端的地址,客户端发送消息后,服务器群发给各个客户端软件。

问题思考

 客户端会不会知道其它客户端地址?

不知道,服务器完成,服务器存储连接的客户端信息---》链表

有几种消息类型?

        登录

        聊天

        退出

客户端如何同时处理发送和接收?

客户端不仅需要读取服务器消息,而且需要发送消息。读取需要调用recvfrom,发送需要先调用fgets,两个都是阻塞函数。所以必须使用多任务来同时处理,可以使用多进程或者多线程来处理。

补充参考函数 

链表节点结构体:

struct node{

struct sockaddr_in addr;//data memcmp

struct node *next;

};

消息对应的结构体(同一个协议)

typedef struct msg_t

{

int type;//'L' C Q enum un{login,chat,quit};

char name[32];//用户名

char text[128];//消息正文

}MSG_t;

int memcmp(void *s1,void *s2,int size);//对比       相等时返回值为0

程序流程图

服务器

客户端

代码流程

代码

服务器

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <dirent.h>
#include <sys/stat.h>
#include <signal.h>
#include <pthread.h>

enum type_t
{
    login,
    chat,
    quit,
};

// 消息对应的结构体
typedef struct msg_t
{
    int type;
    char name[32];
    char text[128];
} MSG_t;

// 链表
typedef struct node
{
    struct sockaddr_in addr;
    struct node *next;
} node_t, *node_p;

// 登录
void Login(int sockfd, MSG_t msg, node_p p, struct sockaddr_in caddr)
{
    sprintf(msg.text, "%s %s", msg.name, "已登录!!!");
    while (p->next != NULL)
    {
        p = p->next;
        sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(p->addr), sizeof(p->addr));
    }
    node_p pnew = (node_p)malloc(sizeof(node_t));
    if (NULL == pnew)
    {
        perror("p malloc err");
        return;
    }
    pnew->addr = caddr;
    pnew->next = NULL;
    p->next = pnew;
}

// 聊天
void Chat(int sockfd, MSG_t msg, node_p p, struct sockaddr_in caddr)
{
    while (p->next != NULL)
    {
        p = p->next;
        if (memcmp(&(p->addr), &caddr, sizeof(caddr)))
        {
            sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(p->addr), sizeof(p->addr));
        }
    }
}

// 退出
void Quit(int sockfd, MSG_t msg, node_p p, struct sockaddr_in caddr)
{
    sprintf(msg.text, "%s %s", msg.name, "已退出!!!");
    while (p->next != NULL)
    {
        if (memcmp(&(p->next->addr), &caddr, sizeof(caddr)))
        {
            p = p->next;
            sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(p->addr), sizeof(p->addr));
        }
        else
        {
            node_p ptail = p->next;
            p->next = ptail->next;
            free(ptail);
            ptail = NULL;
        }
    }
}

node_p create()
{
    node_p p = (node_p)malloc(sizeof(node_t));
    if (NULL == p)
    {
        perror("p malloc err");
        return NULL;
    }
    p->next = NULL;
    return p;
}
int main(int argc, char const *argv[])
{

    if (argc != 2)
    {
        printf("%s <port>\n", argv[0]);
        return -1;
    }
    int ret;
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("socket err");
        return -1;
    }
    printf("sockfd:%d\n", sockfd);

    struct sockaddr_in saddr, caddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[1]));
    // saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
    saddr.sin_addr.s_addr = INADDR_ANY;
    int len = sizeof(caddr);

    if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("bind err");
        return -1;
    }
    printf("bind success\n");

    MSG_t msg;
    node_p p = create();
    // 服务器可以给所有用户发送“公告”
    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork err");
        return -1;
    }
    else if (pid == 0)
    {
        while (1)
        {
            fgets(msg.text, sizeof(msg.text), stdin);
            if (msg.text[strlen(msg.text) - 1] == '\n')
                msg.text[strlen(msg.text) - 1] = '\0';
            msg.type = chat;
            strcpy(msg.name, "server say ");
            sendto(sockfd, &msg, sizeof(MSG_t), 0, (struct sockaddr *)&saddr, sizeof(saddr));
        }
    }
    else
    {

        while (1)
        {
            memset(msg.text, 0, sizeof(msg.text));
            ret = recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&caddr, &len);
            if (ret < 0)
            {
                perror("recv err\n");
                return -1;
            }

            switch (msg.type)
            {
            case 0:
                printf("%s 上线\n", msg.name);
                Login(sockfd, msg, p, caddr);
                break;
            case 1:
                printf("%s %s\n", msg.name, msg.text);
                Chat(sockfd, msg, p, caddr);
                break;
            case 2:
                printf("%s 下线\n", msg.name);
                Quit(sockfd, msg, p, caddr);
                break;
            }
        }
        close(sockfd);
        return 0;
    }
}

客户端

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <dirent.h>
#include <sys/stat.h>
#include <signal.h>
#include <pthread.h>

enum type_t
{
    login,
    chat,
    quit,
};

// 链表
typedef struct node
{
    struct sockaddr_in addr;
    struct node *next;
} node_t, *node_p;

// 消息对应的结构体
typedef struct msg_t
{
    int type;
    char name[32];
    char text[128];
} MSG_t;

int main(int argc, char const *argv[])
{
    MSG_t msg;
    int ret;
    char buf[128];
    if (argc != 3)
    {
        printf("%s <ip> <port>\n", argv[0]);
        return -1;
    }

    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("socket err");
        return -1;
    }
    printf("%d\n", sockfd);

    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[2]));
    saddr.sin_addr.s_addr = inet_addr(argv[1]);
    int len = sizeof(saddr);

    msg.type = login;
    printf("登录账号:\n");
    fgets(msg.name, sizeof(msg.name), stdin);
    if (msg.name[strlen(msg.name) - 1] == '\n')
        msg.name[strlen(msg.name) - 1] = '\0';

    if (sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
    {
        perror("send1 err");
        return -1;
    }

    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork err");
        return -1;
    }
    else if (pid == 0)
    {
        while (1)
        {
            memset(msg.text, 0, sizeof(msg.text));
            printf("请输入消息:\n");
            fgets(msg.text, sizeof(msg.text), stdin);
            if (msg.text[strlen(msg.text) - 1] == '\n')
                msg.text[strlen(msg.text) - 1] = '\0';
            if (strcmp(msg.text, "quit") == 0)
            {
                printf("退出\n");
                msg.type = quit;
                sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&saddr, sizeof(saddr));
                break;
            }
            else
            {
                msg.type = chat;
                if (sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&saddr, sizeof(saddr))<0)
                {
                    perror("send2 err");
                    return -1;
                }
            }
        }
    }
    else
    {
        while (1)
        {
            if ((recvfrom(sockfd, &msg, sizeof(msg), 0, NULL, NULL)) < 0)
            {
                perror("recvfrom err.");
                return -1;
            }
            printf("%s:%s\n", msg.name, msg.text);
        }
    }
    close(sockfd);
    return 0;
}

 总结

一、整体架构和功能

 
  1. 整体架构

    • 基于 UDP 协议实现了一个简单的多人聊天系统。服务器和客户端通过发送和接收特定结构体(MSG_t)封装的消息来进行通信。
    • 服务器端维护一个链表(node_t)来存储已登录客户端的网络地址信息。
  2. 主要功能

    • 登录功能:客户端发送登录消息(login类型),服务器接收后将客户端信息添加到链表中,并通知其他已登录客户端该用户已登录。
    • 聊天功能:客户端发送聊天消息(chat类型),服务器将消息转发给除发送方之外的其他已登录客户端。
    • 退出功能:客户端发送退出消息(quit类型),服务器从链表中删除该客户端信息,并通知其他已登录客户端该用户已退出。
    • 服务器公告功能:服务器可以发送公告消息给所有已登录客户端。
 

二、服务器端分析

 
  1. 数据结构和枚举定义

    • 枚举类型type_t:定义了消息的类型,包括login(登录)、chat(聊天)和quit(退出),使消息处理逻辑更加清晰。
    • 结构体MSG_t:用于封装消息,包含消息类型(type)、发送者姓名(name)和消息内容(text)。
    • 结构体node_tnode_p:定义了链表节点,用于存储客户端的网络地址信息。
  2. 初始化部分

    • 命令行参数检查:检查启动服务器时是否提供了正确的端口号作为命令行参数。
    • 套接字创建和绑定:创建 UDP 套接字(socket函数),并将其与指定的 IP 地址和端口号绑定(bind函数)。
  3. 消息处理函数

    • Login函数
      • 当接收到登录消息时,构建登录通知消息(将用户名和登录状态组合)。
      • 遍历链表,将登录通知消息发送给除发送方之外的所有已登录客户端。
      • 创建新的链表节点,将登录客户端的地址信息添加到链表中。
    • Chat函数
      • 接收到聊天消息后,遍历链表,将消息发送给除发送方之外的所有已登录客户端。
    • Quit函数
      • 构建退出通知消息(将用户名和退出状态组合)。
      • 遍历链表,删除退出客户端的节点,并将退出通知消息发送给其他已登录客户端。
  4. 主循环部分

    • 服务器在一个无限循环中接收客户端发送的消息(recvfrom函数)。
    • 根据消息的类型(通过type字段判断)调用相应的处理函数(LoginChatQuit)。
  5. 服务器公告实现

    • 通过fork创建一个子进程,父进程负责处理客户端的消息,子进程用于发送服务器公告。
    • 子进程从标准输入读取公告内容,构建聊天类型的消息(type设置为chatname设置为server say),并发送给所有已登录客户端。
 

三、客户端分析

 
  1. 数据结构定义

    • 与服务器端类似,定义了type_t枚举类型、MSG_t结构体和node_tnode_p结构体。
  2. 初始化部分

    • 命令行参数检查:检查启动客户端时是否提供了正确的服务器 IP 地址和端口号作为命令行参数。
    • 套接字创建:创建 UDP 套接字。
  3. 登录和消息发送流程

    • 客户端首先发送登录消息(type设置为login),包含用户输入的登录账号。
    • 通过fork创建一个子进程,子进程负责从标准输入读取消息内容,并根据消息内容判断是普通聊天消息还是退出消息。
      • 普通聊天消息(type设置为chat)直接发送给服务器。
      • 退出消息(type设置为quit)发送给服务器后,子进程退出。
    • 父进程在一个无限循环中接收服务器转发的消息(recvfrom函数),并将消息内容打印输出。
 

四、数据传输和处理

 
  1. 消息封装和解析

    • 服务器和客户端都使用MSG_t结构体来封装消息,通过网络传输该结构体的二进制数据。
    • 在接收端(服务器或客户端),直接将接收到的数据解析为MSG_t结构体进行处理。
  2. 网络地址处理

    • 在服务器端,使用struct sockaddr_in来存储客户端的网络地址信息。在LoginChatQuit函数中,需要根据客户端的网络地址来发送消息。
    • 客户端在发送消息时,需要指定服务器的网络地址(通过命令行参数获取并填充到struct sockaddr_in结构体中)。
 

五、注意事项和潜在问题

 
  1. 错误处理

    • 在代码中,对于系统调用(如socketbindrecvfromsendto等)的返回值进行了检查,如果返回值小于 0,则表示调用失败,通过perror函数输出错误信息。
  2. 缓冲区溢出

    • 代码中使用固定大小的缓冲区(如MSG_t结构体中的text数组),在接收和处理消息时,如果消息内容长度超过缓冲区大小,可能会导致缓冲区溢出。
  3. 多线程 / 进程安全性

    • 服务器端使用了fork创建子进程来实现服务器公告功能,在多进程环境下,需要注意进程间资源共享和同步的问题。
    • 虽然代码中没有明显的线程相关操作,但如果要对程序进行扩展(如添加图形界面等),可能会引入线程,需要考虑线程安全性。
  4. 链表操作的健壮性

    • 服务器端使用链表来存储客户端信息,在LoginQuit等函数中涉及链表的插入和删除操作。在并发环境下(多个客户端同时登录或退出),可能需要对链表操作进行加锁保护,以确保链表的一致性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值