Linux&C语言简单实现基于UDP的网络群聊聊天室recvfrom/sendto-传输层-精简

UDP 客户端 和 服务器 通信

在这里插入图片描述

要求

有新用户登录,其他在线的用户可以收到登录信息
有用户群聊,其他在线的用户可以收到群聊信息
有用户退出,其他在线的用户可以收到退出信息
服务器可以发送系统信息

提示

客户端登录之后,为了实现一边发送数据一边接收数据,可以使用多进程或者多线程
服务器既可以发送系统信息,又可以接收客户端信息并处理,可以使用多进程或者多线程
服务器需要给多个用户发送数据,所以需要保存每一个用户的信息,使用链表来保存 数据传输的时候要定义结构体,结构体中包含操作码、用户名以及数据
在这里插入图片描述

代码实现

服务器—01server.c

//-------------------------服务器-------------------------
#include "./uDP.h"

typedef struct __MSG{
    char code;//'L' 登录  'C' 群聊  'Q' 退出
    char name[32];//用户名
    char text[128];
}msg_t;

//链表的节点
typedef struct __NODE{
    struct sockaddr_in client_addr;
    struct __NODE *next;
}node_t;

//创建链表的节点
void create_node(node_t **phead);
//将新登录的用户的网路信息结构体插入链表:头插
void do_login(node_t *phead, struct sockaddr_in client_addr, msg_t msg, int sockfd);
//群聊--遍历链表--群发
void do_chat(node_t *phead, struct sockaddr_in client_addr, msg_t msg, int sockfd);
//退出--群发--删除
void do_quit(node_t *phead, struct sockaddr_in client_addr, msg_t msg, int sockfd);

int main(int argc, char *argv[]){
    if(3 != argc){
		printf("Usage : %s <IP> <PORT>\n", argv[0]);
		exit(-1);
	}
    //创建套接字
    int sockfd = 0;
    if(-1 == (sockfd = socket(AF_INET, SOCK_DGRAM, 0))){
        ERRLOG("socket error");
    }
    //填充服务器网络信息结构体
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(atoi(argv[2]));
    server_addr.sin_addr.s_addr = inet_addr(argv[1]);
    socklen_t server_addr_len = sizeof(server_addr);

    //将套接字与服务器的网络信息结构体绑定
    if(-1 == bind(sockfd, (struct sockaddr *)&server_addr, server_addr_len)){
        ERRLOG("bind error");
    }
    //用户信息结构体
    msg_t msg;
    memset(&msg, 0, sizeof(msg_t));

    pid_t pid = 0;
    if(-1 == (pid = fork())){
        ERRLOG("fork error");
    }else if(0 == pid){//子进程逻辑
        //接收数据

        //定义一个结构体,用于保存客户端的信息
        struct sockaddr_in client_addr;
        memset(&client_addr, 0, sizeof(client_addr));
        socklen_t client_addr_len = sizeof(client_addr);

        //创建保存客户端网络信息结构体链表的头结点
        node_t *phead = NULL;
        create_node(&phead);

        while(1){
            memset(&msg, 0, sizeof(msg_t));
            if(-1 == recvfrom(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&client_addr, &client_addr_len)){
                ERRLOG("recvfrom error");
            }
            printf("%s : %s\n", msg.name, msg.text);
            switch(msg.code){
                case 'L':
                    //将新登录的用户的网路信息结构体插入链表:头插
                    do_login(phead, client_addr, msg, sockfd);
                    break;
                case 'C':
                    //群聊--遍历链表--群发
                    do_chat(phead, client_addr, msg, sockfd);
                    break;
                case 'Q':
                    //退出--群发--删除
                    do_quit(phead, client_addr, msg, sockfd);
                    break;
            }
        }
        //关闭套接字
        close(sockfd);
    }else{//父进程逻辑
        //发送系统消息
        //把父进程看成客户端  以群聊的方式将 系统消息发给子进程
        while(1){
            memset(&msg, 0, sizeof(msg_t));
            msg.code = 'C';
            strcpy(msg.name, "server");
            fgets(msg.text, 128, stdin);
            msg.text[strlen(msg.text)-1] = '\0';
            if(-1 == sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&server_addr, server_addr_len)){
                ERRLOG("recvfrom error");
            }
        }
        //关闭套接字
        close(sockfd);
    }
    return 0;
}

//创建链表的节点
void create_node(node_t **phead){
    *phead = (node_t *)malloc(sizeof(node_t));
    memset(*phead, 0, sizeof(node_t));
}

//登录
void do_login(node_t *phead, struct sockaddr_in client_addr, msg_t msg, int sockfd){
    //先遍历链表 将 "xxx 登录" 的消息发给所有人
    node_t *ptemp = phead;
    while(ptemp->next != NULL){
        ptemp = ptemp->next;
        if(-1 == sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&(ptemp->client_addr), sizeof(ptemp->client_addr))){
            ERRLOG("sendto error");
        }
    }
    //将新登录的用户的网路信息结构体插入链表:头插
    node_t *pnew = NULL;
    create_node(&pnew);
    pnew->client_addr = client_addr;
    pnew->next = phead->next;
    phead->next = pnew;
}   

//群聊
void do_chat(node_t *phead, struct sockaddr_in client_addr, msg_t msg, int sockfd){
    //遍历链表,将消息转发给除了自己之外的所有人
    node_t *ptemp = phead;
    while(ptemp->next != NULL){
        ptemp = ptemp->next;
        //不是自己再转发,是自己就不用发了
        if(memcmp(&(ptemp->client_addr), &client_addr, sizeof(struct sockaddr_in))){
            if(-1 == sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&(ptemp->client_addr), sizeof(ptemp->client_addr))){
                ERRLOG("sendto error");
            }
        }
    }
}

//退出
void do_quit(node_t *phead, struct sockaddr_in client_addr, msg_t msg, int sockfd){
    //遍历链表,不是自己就转发"xxx 退出"的消息
    //是自己  就将自己在链表中删除
    node_t *ptemp = phead;
    node_t *pdel = NULL;
    while(ptemp->next != NULL){
        if(memcmp(&(ptemp->next->client_addr), &client_addr, sizeof(struct sockaddr_in))){
            //不是自己就转发
            ptemp = ptemp->next;//只有不删除节点的情况下  ptemp才往后走
            if(-1 == sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&(ptemp->client_addr), sizeof(ptemp->client_addr))){
                ERRLOG("sendto error");
            }
        }else{
            //是自己 就将自己在链表中删除
            pdel = ptemp->next;
            ptemp->next = pdel->next;
            free(pdel);
            pdel = NULL;
        }
    }
}

客户端—02client.c

//--------------------------------客户端------------------
#include "./uDP.h"

typedef struct __MSG
{
    char code;     //'L' 登录  'C' 群聊  'Q' 退出
    char name[32]; //用户名
    char text[128];
} msg_t;

int main(int argc, char *argv[])
{
    if (3 != argc)
    {
        printf("Usage : %s <IP> <PORT>\n", argv[0]);
        exit(-1);
    }
    //创建套接字
    int sockfd = 0;
    if (-1 == (sockfd = socket(AF_INET, SOCK_DGRAM, 0)))
    {
        ERRLOG("socket error");
    }
    //填充服务器网络信息结构体
    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(atoi(argv[2]));
    server_addr.sin_addr.s_addr = inet_addr(argv[1]);
    socklen_t server_addr_len = sizeof(server_addr);

    //组装第一个包
    msg_t msg;
    memset(&msg, 0, sizeof(msg_t));
    printf("请输入用户名:");
    fgets(msg.name, 32, stdin);
    msg.name[strlen(msg.name) - 1] = '\0';
    msg.code = 'L'; //登录
    strcpy(msg.text, "加入群聊");
    //先将xxx 加入群聊的消息发给服务器
    if (-1 == sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&server_addr, server_addr_len))
    {
        ERRLOG("sendto error");
    }

    pid_t pid = 0;
    if (-1 == (pid = fork()))
    {
        ERRLOG("fork error");
    }
    else if (0 == pid)
    { //子进程逻辑
        //接收数据
        while (1)
        {
            if (-1 == recvfrom(sockfd, &msg, sizeof(msg_t), 0, NULL, NULL))
            {
                ERRLOG("recvfrom error");
            }
            printf("%s : %s\n", msg.name, msg.text);
        }
    }
    else
    { //父进程逻辑
        while (1)
        {
            //发送数据
            fgets(msg.text, 128, stdin);
            msg.text[strlen(msg.text) - 1] = '\0';

            //需要根据输入内容的不同组装不同的包
            if (0 == strcmp(msg.text, "quit"))
            { //表示输入的是退出的消息
                msg.code = 'Q';
                strcpy(msg.text, "退出群聊");
            }
            else
            {
                msg.code = 'C';
            }
            //将消息发给服务器
            if (-1 == sendto(sockfd, &msg, sizeof(msg_t), 0, (struct sockaddr *)&server_addr, server_addr_len))
            {
                ERRLOG("sendto error");
            }
            //如果将退出的消息发给了服务器,那么客户端就可以break然后退出了
            if (0 == strcmp(msg.text, "退出群聊"))
            {
                break;
            }
        }
        //让子进程自杀
        kill(pid, SIGKILL);
        wait(NULL);
        //关闭套接字
        close(sockfd);
    }

    return 0;
}

执行结果

在这里插入图片描述

注意

  1. 链表只需插入第一次连接的(新登录的用户)网路信息结构体插入链表
//先遍历链表 将 "xxx 登录" 的消息发给所有人
node_t *ptemp = phead;
while (ptemp->next != NULL)
{
    ptemp = ptemp->next;
    if (-1 == sendto(sockfd, &msg, sizeof(msg_t), 0, 
    (struct sockaddr *)&(ptemp->client_addr), sizeof(ptemp->client_addr)))
    {
        ERRLOG("sendto error");
    }
}
//将新登录的用户的网路信息结构体插入链表:头插
node_t *pnew = NULL;
create_node(&pnew);
pnew->client_addr = client_addr;
pnew->next = phead->next;
phead->next = pnew;
  1. 群聊时,将消息转发给除了自己之外的所有人
//遍历链表,将消息转发给除了自己之外的所有人
node_t *ptemp = phead;
while (ptemp->next != NULL)
{
    ptemp = ptemp->next;
    //不是自己再转发,是自己就不用发了
    if (memcmp(&(ptemp->client_addr), &client_addr, 
    								sizeof(struct sockaddr_in)))
    {
        if (-1 == sendto(sockfd, &msg, sizeof(msg_t),0,
        (struct sockaddr *)&(ptemp->client_addr), sizeof(ptemp->client_addr)))
        {
            ERRLOG("sendto error");
        }
    }
}
  1. 退出时,判断ptemp的下一节点,若将自己从链表中删除则不需往后走
//遍历链表,不是自己就转发"xxx 退出"的消息
//是自己  就将自己在链表中删除
node_t *ptemp = phead;
node_t *pdel = NULL;
while (ptemp->next != NULL)
{
    if (memcmp(&(ptemp->next->client_addr), &client_addr,
    								sizeof(struct sockaddr_in)))
    {
        //不是自己就转发
        ptemp = ptemp->next; //只有不删除节点的情况下  ptemp才往后走
        if (-1 == sendto(sockfd, &msg, sizeof(msg_t),0,	
        (struct sockaddr *)&(ptemp->client_addr),sizeof(ptemp->client_addr)))
        {
            ERRLOG("sendto error");
        }
    }
    else
    {
        //是自己 就将自己在链表中删除
        pdel = ptemp->next;
        ptemp->next = pdel->next;
        free(pdel);
        pdel = NULL;
    }
}
  1. 为实现父进程发送系统消息,将消息发给子进程
//把父进程看成客户端  以群聊的方式将 系统消息发给子进程
while (1)
{
    memset(&msg, 0, sizeof(msg_t));
    msg.code = 'C';
    strcpy(msg.name, "server");
    fgets(msg.text, 128, stdin);
    msg.text[strlen(msg.text) - 1] = '\0';
    if (-1 == sendto(sockfd, &msg, sizeof(msg_t), 0, 
    		(struct sockaddr *)&server_addr, server_addr_len))
    {
        ERRLOG("recvfrom error");
    }
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值