基于UDP的网络群聊聊天室

本文详细介绍了如何使用UDP协议构建一个聊天室系统,涵盖了链表节点结构、自定义应用层协议、服务器和客户端函数,以及系统提供的关键函数如socket、bind、listen等。聊天室具备用户登录、群聊、退出通知和系统消息等功能,通过单链表管理客户端信息。代码示例展示了服务器和客户端的完整实现,实现了用户间消息的实时传递。
摘要由CSDN通过智能技术生成

目录

聊天室的功能:

链表节点结构体

自定义应用层协议结构体,用来传输数据

服务器程序及客户端程序所使用的函数

系统提供的部分函数

1.socket

2.bind

3.listen

4.fork

5.recvfrom

6.sendto

7.close

8.inet_addr

9.htons

自定义函数,实现程序功能:

1.创建链表节点函数:

2.登陆操作函数

3.群聊函数

4.退出操作函数

服务器程序完整代码:

客户端程序完整代码:


聊天室的功能:

        1.有新用户登录,其他在线的用户可以收到登录信息

        2.有用户群聊,其他在线的用户可以收到群聊信息

        3.有用户退出,其他在线的用户可以收到退出信息

        4.服务器可以发送系统信息

实现上述功能所需知识:

        单链表的创建及头插法插入新节点,

        

链表节点结构体

        数据域:存储客户端网络信息结构体

        指针域,下个节点的地址

typedef struct node {
    struct sockaddr_in c_addr; //数据域,存储用户网络信息结构体
    struct node* next; //指针域,
} user_list_t;

自定义应用层协议结构体,用来传输数据

        code 操作码:根据操作码的不同,服务器端执行对客户端的数据执行不同的操作

        name:客户端用户名

        buff:客户端发送的数据

#define N 128

typedef struct c_s {
    char code; //操作码
    char name[N];
    char buff[N];
} msg_t;

服务器程序及客户端程序所使用的函数

系统提供的部分函数

1.socket

int socket(int domain, int type, int protocol);

头文件:

#include <sys/types.h>      
#include <sys/socket.h>

功能:创建套接字

参数:

        domain:指定通信域

        type:套接字类型

        protocol:附加协议

返回值:

        成功返回所创建的套接字文件都文件描述符

        失败返回-1

2.bind

 int bind(int sockfd, const struct sockaddr *addr,
                socklen_t addrlen);

 头文件:

#include <sys/types.h>        
#include <sys/socket.h>

功能:将服务器的网络信息结构体和套接字绑定

参数:

        sockfd:套接字

        addr:网络信息结构体

        addrlen: addr的大小

返回值:

        成功返回0

        失败返回-1

3.listen

int listen(int sockfd, int backlog);

头文件:

#include <sys/types.h>        
#include <sys/socket.h>

功能:将套接字设置为被动监听状态

参数:

        sockfd:套接字

        backlog:允许同时连接到服务器的客户端的个数

返回值:

        成功返回0

        失败返回-1

4.fork

   pid_t fork(void);

头文件: 

#include <sys/types.h>
#include <unistd.h>

功能:创建进程

参数:无

返回值:

        -1,创建失败

        0,子进程

        >0,父进程

5.recvfrom

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                        struct sockaddr *src_addr, socklen_t *addrlen);

头文件:

 #include <sys/types.h>
 #include <sys/socket.h>

功能:在套接字上接收一条消息

参数:

        sockfd:套接字

        buf:存储接收数据的缓冲区的首地址

        len:想要接收到数据的大小

        flags:标志位

        src_addr:客户端网络信息结构体,

        addrlen:src_addr的大小

返回值:

        成功:实际接收数据的字节数

        失败:-1

6.sendto

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                      const struct sockaddr *dest_addr, socklen_t addrlen);

头文件:

 #include <sys/types.h>
 #include <sys/socket.h>

功能:向套接字上发送一条消息

参数:

        sockfd:套接字

        buf:想要发送数据的的首地址

        len:发送数据的大小

        flags:标志位

        dest_addr:接收方的网络信息结构体,

        addrlen:dest_addr的大小

返回值:

        成功 实际发送的数据的字节数

        失败 -1

7.close

    int close(int fd);

头文件:

 #include <unistd.h>

功能:关闭套接字文件

参数:fd:文件标识符

返回值:

        成功0

        失败-1

8.inet_addr

      in_addr_t inet_addr(const char *cp);

头文件:

       #include <sys/socket.h>
       #include <netinet/in.h>
       #include <arpa/inet.h>

功能:将无符号四字节整形的IP地址转换为网络字节序

参数:无符号四字节整形的IP地址

返回值:

        成功:网络字节序IP地址

        失败:-1

9.htons

uint16_t htons(uint16_t hostshort);

头文件:

  #include <arpa/inet.h>

功能:将主机字节序的端口号转换为网络字节序

参数:hostshort:short类型的无符号整数

返回值:

        成功:返回网络字节序的端口号

        失败:-1

自定义函数,实现程序功能:

1.创建链表节点函数:

功能:创建链表节点并初始化

参数:无

返回值:

        失败,返回NULL

        成功,返回创建的节点的首地址


user_list_t* create_linklist(void)
{
    //创建链表节点
    user_list_t* L = NULL;
    if (NULL == (L = (user_list_t*)malloc(sizeof(user_list_t)))) {
        perror("carete user_list_t error");
        return NULL;
    }
    //初始化
    L->next = NULL;
    return L;
}

2.登陆操作函数

功能:执行登陆操作,(将客户端登陆的消息发送给链表中其他的所有节点,并创建新节点,将客户端的信息头插法,添加到链表中)

参数:

        sockaddr_in clientaddr:发送消息的客户端的网络信息结构体,服务器程序的套接字

        sockfd:服务器的套接字

        L:链表头结点指针

        msg:客户端发送的消息结构体

返回值:

        -1:失败

        0:成功

int do_login(struct sockaddr_in clientaddr, int sockfd, user_list_t* L, msg_t msg)

{

    user_list_t* ptemp = L;

    while (NULL != ptemp->next) {

        ptemp = ptemp->next;

        if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&ptemp->c_addr, sizeof(ptemp->c_addr))) {

            perror("sendto error\n");

            return -1;

        }

    }

    //将新用户信息添加到链表中

    user_list_t* pnew = create_linklist();

    pnew->c_addr = clientaddr;

    pnew->next = L->next;

    L->next = pnew;

    return 0;

}

3.群聊函数

功能:实现聊天室群聊功能,客户端发送消息,服务器将消息转发给其他所有的客户端

参数:

        sockaddr_in clientaddr:发送消息的客户端的网络信息结构体,服务器程序的套接字

        sockfd:服务器的套接字

        L:链表头结点指针

        msg:客户端发送的消息结构体

返回值:

        -1:失败

        0:成功

int do_chat(struct sockaddr_in clientaddr, int sockfd, user_list_t* L, msg_t msg)
{
    user_list_t* ptemp = L;
    while (NULL != ptemp->next) {
        ptemp = ptemp->next;
        if (memcmp(&(clientaddr), (&ptemp->c_addr), sizeof(ptemp->c_addr))) {
            if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, (const struct sockaddr*)&ptemp->c_addr, sizeof(ptemp->c_addr))) {
                perror("sendto error\n");
                return -1;
            }
        }
    }
    return 0;
}

4.退出操作函数

功能:实现客户端退出登陆操作,将客户端退出的消息发送给其他所有客户端,并将退出的客户端的信息,从链表中删除

参数:

        sockaddr_in clientaddr:发送消息的客户端的网络信息结构体,服务器程序的套接字

        sockfd:服务器的套接字

        L:链表头结点指针

        msg:客户端发送的消息结构体

返回值:

        -1:失败

        0:成功

int do_quit(struct sockaddr_in clientaddr, int sockfd, user_list_t* L, msg_t msg)
{
    user_list_t* ptemp = L;
    while (NULL != ptemp->next) {
        if (memcmp(&(ptemp->next->c_addr), &(clientaddr), sizeof(clientaddr))) {
            ptemp = ptemp->next;
            if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, (const struct sockaddr*)&ptemp->c_addr, sizeof(ptemp->c_addr))) {
                perror("sendto error\n");
                return -1;
            }
        } else {
            user_list_t* Q = ptemp->next;
            ptemp->next = Q->next;
            free(Q);
            Q = NULL;
        }
    }
}

服务器程序完整代码:

#include <arpa/inet.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#define N 128

typedef struct node {
    struct sockaddr_in c_addr; //数据域,存储用户网络信息结构体
    struct node* next; //指针域,
} user_list_t;

typedef struct c_s {
    char code; //操作码
    char name[N];
    char buff[N];
} msg_t;

//创建链表节点
user_list_t* create_linklist(void);
int do_login(struct sockaddr_in clientaddr, int sockfd, user_list_t* L, msg_t msg);
int do_chat(struct sockaddr_in clientaddr, int sockfd, user_list_t* L, msg_t msg);
int do_quit(struct sockaddr_in clientaddr, int sockfd, user_list_t* L, msg_t msg);

int main(int argc, const char* argv[])
{
    if (3 != argc) {
        perror("open files num error");
        exit(-1);
    }

    //创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (-1 == sockfd) {
        perror("socket error");
        exit(-1);
    }

    //服务器网络信息结构体
    struct sockaddr_in serveraddr;
    //初始化
    memset(&serveraddr, 0, sizeof(serveraddr));
    //填充
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]); //网络字节序,四字节整形
    serveraddr.sin_port = htons(atoi(argv[2])); //网络字节序端口号,
    socklen_t serveraddr_len = sizeof(serveraddr);

    //将套接字与网络信息结构体绑定

    if (-1 == bind(sockfd, (struct sockaddr*)&serveraddr, serveraddr_len)) {
        perror("bind error");
        exit(-1);
    }

    //保存客户端网络信息结构体
    struct sockaddr_in clientaddr;
    socklen_t clientaddr_len = sizeof(clientaddr);

    msg_t msg;

    //创建父子进程,收发数据
    pid_t pid = fork();

    if (-1 == pid) {
        perror("fork error");
        exit(-1);
    } else if (0 == pid) {
        //子进程接收数据并处理

        //创建保存客户端信息的网络信息结构体的链表头节点
        user_list_t* L = create_linklist();

        while (1) {
            memset(&msg, 0, sizeof(msg));
            memset(&clientaddr, 0, sizeof(clientaddr));
            if (-1 == (recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&clientaddr, &clientaddr_len))) {
                perror("recvfrom error");
                exit(-1);
            }
            printf("%8s:%s\n", msg.name, msg.buff);
            switch (msg.code) {
            case 'L':
                //操作L,登陆操作,将客户端的网络结构体存储到链表中,并将登陆消息发送给链表所有客户端
                do_login(clientaddr, sockfd, L, msg);
                break;
            case 'C':
                do_chat(clientaddr, sockfd, L, msg);
                break;
            case 'Q':
                do_quit(clientaddr, sockfd, L, msg);
                break;
            }
        }
    } else if (0 < pid) {
        //父进程发送系统消息
        //将操作码置为发消息操作
        msg.code = 'C';
        //将发送的消息的用户名改为服务器
        strcpy(msg.name, "server");

        while (1) {
            fgets(msg.buff, N, stdin);
            msg.buff[strlen(msg.buff) - 1] = '\0';

            if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&serveraddr, serveraddr_len)) {
                perror("sendto error");
                exit(-1);
            }
        }
    }
    close(sockfd);

    return 0;
}

user_list_t* create_linklist(void)
{
    //创建链表节点
    user_list_t* L = NULL;
    if (NULL == (L = (user_list_t*)malloc(sizeof(user_list_t)))) {
        perror("carete user_list_t error");
        return NULL;
    }
    //初始化
    L->next = NULL;
    return L;
}

//登陆操作函数
int do_login(struct sockaddr_in clientaddr, int sockfd, user_list_t* L, msg_t msg)
{
    user_list_t* ptemp = L;
    while (NULL != ptemp->next) {
        ptemp = ptemp->next;
        if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&ptemp->c_addr, sizeof(ptemp->c_addr))) {
            perror("sendto error\n");
            return -1;
        }
    }
    //将新用户信息添加到链表中
    user_list_t* pnew = create_linklist();
    pnew->c_addr = clientaddr;
    pnew->next = L->next;
    L->next = pnew;
    return 0;
}
//群聊函数
int do_chat(struct sockaddr_in clientaddr, int sockfd, user_list_t* L, msg_t msg)
{
    user_list_t* ptemp = L;
    while (NULL != ptemp->next) {
        ptemp = ptemp->next;
        if (memcmp(&(clientaddr), (&ptemp->c_addr), sizeof(ptemp->c_addr))) {
            if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, (const struct sockaddr*)&ptemp->c_addr, sizeof(ptemp->c_addr))) {
                perror("sendto error\n");
                return -1;
            }
        }
    }
    return 0;
}
//退出操作函数
int do_quit(struct sockaddr_in clientaddr, int sockfd, user_list_t* L, msg_t msg)
{
    user_list_t* ptemp = L;
    while (NULL != ptemp->next) {
        if (memcmp(&(ptemp->next->c_addr), &(clientaddr), sizeof(clientaddr))) {
            ptemp = ptemp->next;
            if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, (const struct sockaddr*)&ptemp->c_addr, sizeof(ptemp->c_addr))) {
                perror("sendto error\n");
                return -1;
            }
        } else {
            user_list_t* Q = ptemp->next;
            ptemp->next = Q->next;
            free(Q);
            Q = NULL;
        }
    }
}

客户端程序完整代码:

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

#define N 128

typedef struct c_s {
    char code; //操作码
    char name[N]; //!注:大小必须与服务器保持一致
    char buff[N];
} msg_t;

int main(int argc, const char* argv[])
{

    if (3 != argc) {
        perror("open files num error");
        return -1;
    }

    //创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (-1 == sockfd) {
        perror("socket error");
        exit(-1);
    }

    //服务器网络信息结构体
    struct sockaddr_in serveraddr;
    //初始化
    memset(&serveraddr, 0, sizeof(serveraddr));
    //填充
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]); //网络字节序,四字节整形
    serveraddr.sin_port = htons(atoi(argv[2])); //网络字节序端口号,
    socklen_t serveraddr_len = sizeof(serveraddr);

    msg_t msg;
    memset(&msg, 0, sizeof(msg));
    //输入用户名
    printf("input username > ");
    fgets(msg.name, N, stdin);
    msg.name[strlen(msg.name) - 1] = '\0';

    //将用户名发送到服务器完成登陆操作
    msg.code = 'L';
    strcpy(msg.buff, "加入群聊");
    if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&serveraddr, serveraddr_len)) {
        perror("sendto error");
        exit(-1);
    }
    printf("%s %s\n", msg.name, msg.buff);

    //创建父子进程,收发数据

    pid_t pid = fork();
    if (-1 == pid) {
        perror("fork error");
        exit(-1);
    } else if (0 == pid) {
        //子进程接收数据
        //接收系统消息
        while (1) {
            memset(&msg, 0, sizeof(msg));
            if (-1 == recvfrom(sockfd, &msg, sizeof(msg), 0, NULL, NULL)) {
                perror("sendto error");
                exit(-1);
            }
            printf("%8s:[%s]\n", msg.name, msg.buff);
        }
    }

    else if (0 < pid) {
        //父进程发送数据,
        while (1) {
            //将操作码默认设置为发送数据C
            msg.code = 'C';
            fgets(msg.buff, sizeof(msg.buff), stdin);
            msg.buff[strlen(msg.buff) - 1] = '\0';

            if (!strcmp(msg.buff, "quit")) {
                msg.code = 'Q';
                strcpy(msg.buff, "退出群聊");
            }
            if (-1 == sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&serveraddr, serveraddr_len)) {
                perror("sendto error");
                exit(-1);
            }
            if (!strcmp(msg.buff, "退出群聊")) {
                break;
            }
        }
        kill(pid, SIGKILL);
        wait(NULL);
        close(sockfd);
    }

    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值