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

该博客详细介绍了如何使用UDP协议实现一个简单的网络群聊聊天室。服务器通过链表保存用户信息,处理登录、群聊和退出等操作,并将消息广播给其他在线用户。客户端则负责登录、发送群聊消息和退出。代码中使用了多进程来实现服务器的接收和发送数据功能。
摘要由CSDN通过智能技术生成

UDP 客户端 和 服务器 通信

在这里插入图片描述

要求

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

提示

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

代码实现

服务器—01server.c

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

//创建节点的函数
int create_node(node_t **pnew, struct sockaddr_in cli_addr);

//使用头插法插入数据
int insert_into_list_by_head(node_t *phead, struct sockaddr_in cli_addr);

int main(int argc, const char *argv[])
{
    if (3 != argc)
    {
        printf("Usage : %s <IP> <PORT>\n", argv[0]);
        exit(-1);
    }
    // 1.创建套接字     // IPV4使用,//UDP
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (-1 == sockfd)
    {
        ERRLOG("socket error");
    }

    // 2.填充服务器网络信息结构体
    struct sockaddr_in server_addr;
    //清空、填充0
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET; // IPV4
    //端口号  填 8888 9999 6789 ...都可以
    //将无符号2字节整型  主机-->网络
    server_addr.sin_port = htons(atoi(argv[2]));
    // ip地址 要么是当前Ubuntu主机的IP地址 或者
    //如果本地测试的化  使用  127.0.0.1 也可以
    //将strptr所指的字符串转换成32位的网络字节序二进制值。
    server_addr.sin_addr.s_addr = inet_addr(argv[1]);

    //创建头节点-------链表
    //可以使用返回值的方式,也可以使用地址传递的方式
    node_t *phead = NULL;
    create_node(&phead, server_addr);

    //结构体长度
    socklen_t server_addr_len = sizeof(server_addr);

    // 3.将套接字和网络信息结构体绑定//强制类型转换//网络信息结构体
    if (-1 == bind(sockfd, (struct sockaddr *)&server_addr,
                   server_addr_len))
        ERRLOG("bind error");

    //用来保存客户端信息的结构体
    // UPD网络通信
    //如果给发送端回信,就必须保存发送端的网络信息结构体
    struct sockaddr_in client_addr, client_addr_o;
    memset(&client_addr, 0, sizeof(client_addr));
    socklen_t client_addr_len = sizeof(client_addr);
    //-------------------------------------------------------------------------------
    udp_t user_d; //客户端数据
    node_t *ptemp = NULL;
    node_t *pdel = NULL;

    pid_t pid;
    pid = fork();
    if (pid < 0)
    {
        ERRLOG("fork error");
    }
    else if (pid == 0) //子进程节收数据
    {
        while (1)
        {
            //阻塞接收客户端发来的数据                              //强制类型转换//发送端的网络信息结构体
            if (-1 == recvfrom(sockfd, &user_d, sizeof(udp_t), 0, (struct sockaddr *)&client_addr, &client_addr_len))
                ERRLOG("recvfrom error");
            if (user_d.OS == 1)
            {
                //在服务器显示
                printf("客户端 (%s:%d) >",
                       inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
                printf("[%s]上线\n", user_d.name);
                //遍历链表,将消息转发给除了自己之外的所有人
                ptemp = phead;
                while (ptemp->next != NULL)
                {
                    ptemp = ptemp->next;
                    if (-1 == sendto(sockfd, &user_d, sizeof(udp_t), 0,
                                     (struct sockaddr *)&(ptemp->cli_addr), sizeof(ptemp->cli_addr)))
                        ERRLOG("sendto error");
                }
                //再将自己-使用头插法插入数据
                insert_into_list_by_head(phead, client_addr);
            }
            else if (user_d.OS == 2 || user_d.OS == 3)
            {
                if (user_d.OS == 2)
                {
                    printf("[%s]发来消息\n", user_d.name);
                    ptemp = phead;
                    while (ptemp->next != NULL)
                    {
                        ptemp = ptemp->next;
                        if (memcmp(&(ptemp->cli_addr), &client_addr, sizeof(struct sockaddr_in)))
                        {
                            if (-1 == sendto(sockfd, &user_d, sizeof(udp_t), 0,
                                             (struct sockaddr *)&(ptemp->cli_addr), client_addr_len))
                                ERRLOG("sendto error");
                        }
                    }
                }
                else if (user_d.OS == 3)
                {
                    printf("[%s]退出\n", user_d.name);
                    ptemp = phead;
                    while (ptemp->next != NULL)
                    {
                        if (memcmp(&(ptemp->next->cli_addr), &client_addr, sizeof(struct sockaddr_in)))
                        {
                            //不是自己就转发
                            ptemp = ptemp->next; //只有不删除节点的情况下  ptemp才往后走
                            if (-1 == sendto(sockfd, &user_d, sizeof(udp_t), 0,
                                             (struct sockaddr *)&(ptemp->cli_addr), client_addr_len))
                                ERRLOG("sendto error");
                        }
                        else
                        {
                            //是自己 就将自己在链表中删除
                            pdel = ptemp->next;
                            ptemp->next = pdel->next;
                            free(pdel);
                            pdel = NULL;
                        }
                    }
                }
            }
            else //父进程发来的系统消息
            {
                ptemp = phead;
                while (ptemp->next != NULL)
                {
                    ptemp = ptemp->next;
                    if (-1 == sendto(sockfd, &user_d, sizeof(udp_t), 0,
                                 (struct sockaddr *)&(ptemp->cli_addr), sizeof(ptemp->cli_addr)))
                        ERRLOG("sendto error");
                }
            }
        }
    }
    else //父进程发送系统消息
    {
        while (1)
        {
            printf("input > ");
            memset(user_d.buff2, 0, sizeof(user_d.buff2));
            scanf("%s", user_d.buff2);
            user_d.OS = 0;
            //把消息发给子进程
            if (-1 == sendto(sockfd, &user_d, sizeof(udp_t), 0,
                             (struct sockaddr *)&server_addr, server_addr_len))
                ERRLOG("sendto error");
        }
    }
    close(sockfd);
    return 0;
}

//创建节点的函数
int create_node(node_t **pnew, struct sockaddr_in cli_addr)
{
    *pnew = (node_t *)malloc(sizeof(node_t));
    if (NULL == *pnew)
    {
        ERRLOG("空间分配失败\n");
    }
    (*pnew)->cli_addr = cli_addr;
    (*pnew)->next = NULL;
    return 0;
}
//使用头插法插入数据
int insert_into_list_by_head(node_t *phead, struct sockaddr_in cli_addr)
{
    if (NULL == phead)
    {
        ERRLOG("入参为NULL 请检查\n");
    }
    //创建新节点
    node_t *pnew = NULL;
    create_node(&pnew, cli_addr);
    //将新节点头插到原链表里
    pnew->next = phead->next;
    phead->next = pnew;
    return 0;
}

客户端—02client.c

//----------------------------------------客户端---------------------------------
#include "./uDP.h"
int main(int argc, const char *argv[])
{
    if (3 != argc)
    {
        printf("Usage : %s <IP> <PORT>\n", argv[0]);
        exit(-1);
    }

    // 1.创建套接字     // IPV4使用,//UDP
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (-1 == sockfd)
    {
        ERRLOG("socket error");
    }

    // 2.填充服务器网络信息结构体
    struct sockaddr_in server_addr;
    //清空、填充0
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET; // IPV4
    //端口号  填 8888 9999 6789 ...都可以
    //将无符号2字节整型  主机-->网络
    server_addr.sin_port = htons(atoi(argv[2]));
    // ip地址 要么是当前Ubuntu主机的IP地址 或者
    //如果本地测试的化  使用  127.0.0.1 也可以
    //将strptr所指的字符串转换成32位的网络字节序二进制值。
    server_addr.sin_addr.s_addr = inet_addr(argv[1]);

    //结构体长度
    socklen_t server_addr_len = sizeof(server_addr);

    //-------------------------------------------------------------------------------
    char buff[128] = {0};
    udp_t user_d; //客户端数据

SCANF_T:
    printf("input 操作码----------1登录 > ");
    scanf("%d", &user_d.OS);
    if (user_d.OS == 1)
    {
        printf("input your name> ");
        scanf("%s", user_d.name);
        strcpy(buff,user_d.name);
        //将数据发给服务器                                    //强制类型转换   //网络信息结构体
        if (-1 == sendto(sockfd, &user_d, sizeof(udp_t), 0, (struct sockaddr *)&server_addr, server_addr_len))
            ERRLOG("sendto error");
    }
    else
    {
        goto SCANF_T;
    }

    pid_t pid;
    pid = fork();
    if (pid < 0)
    {
        ERRLOG("fork error");
    }
    else if (pid == 0) //子进程节收数据
    {
        while (1)
        {
            //接收服务器的应答
            //客户端就无须再保存服务器的网络信息结构体了
            //因为server_addr 没有改变
            if (-1 == recvfrom(sockfd, &user_d, sizeof(udp_t), 0, NULL, NULL))
                ERRLOG("recvfrom error");    
            if (user_d.OS == 0)
            {
                printf("系统消息  > [%s]\n", user_d.buff2);
            }
            if (user_d.OS == 1)
            {
                printf("[%s]上线了\n", user_d.name);
            }
            if (user_d.OS == 2)
            {
                printf("%s >  %s\n", user_d.name, user_d.buff);
            }
            if (user_d.OS == 3)
            {
                printf("[%s]下线了\n", user_d.name);
            }
        }
        exit(EXIT_SUCCESS); //退出进程的时候会刷新缓冲区
    }
    else//父进程节发数据
    {
    SCANF_T2:
        printf("input 操作码--2群聊---3退出 > ");
        scanf("%d", &user_d.OS);
        if (user_d.OS == 3)
        {
            //将数据发给服务器                      //强制类型转换   //网络信息结构体
            if (-1 == sendto(sockfd, &user_d, sizeof(udp_t), 0, (struct sockaddr *)&server_addr, server_addr_len))
                ERRLOG("sendto error");
        }
        else if (user_d.OS == 2)
        {
            while (1)
            {
                scanf("%s", user_d.buff);
                if (strcmp(user_d.buff, "quit") == 0)
                {
                    user_d.OS = 3;
                }
                //将数据发给服务器                      //强制类型转换   //网络信息结构体
                if (-1 == sendto(sockfd, &user_d, sizeof(udp_t), 0, (struct sockaddr *)&server_addr, server_addr_len))
                    ERRLOG("sendto error");

                if (user_d.OS == 3)
                {
                    exit(-1);
                }
            }
        }
        else
        {
            goto SCANF_T2;
        }
        
        wait(NULL); //阻塞等待回收子进程的资源
    }

    close(sockfd);

    return 0;
}

头文件—uDP.h

//-----------------基于UDP的网络群聊聊天室--------------------------
#ifndef __uDP_H__
#define __uDP_H__

#include <stdio.h>
/*socket-bind-listen-accept*/
#include <sys/types.h>
#include <sys/socket.h>
/*memset*/
#include <string.h>
/*sockaddr_in结构体*/
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
/*inet_addr*/
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
/*close*/
#include <unistd.h>
/*exit*/
#include <stdlib.h>
/*wait*/
#include <sys/wait.h>
/*select*/
#include <sys/select.h>
#define ERRLOG(errmsg)                                       \
	do                                                       \
	{                                                        \
		printf("%s--%s(%d):", __FILE__, __func__, __LINE__); \
		perror(errmsg);                                      \
		exit(-1);                                            \
	} while (0)

typedef struct 
{
    int OS;//操作码-1登录///2群聊---3退出
    char name[20];//用户名
    char buff[1024];//客户端数据
	char buff2[1024];//服务器数据
}udp_t;

typedef struct {
    struct sockaddr_in cli_addr;//数据域  保存客户端信息的结构体
	socklen_t len; 
}A_t;
//链表的节点
typedef struct __NODE{
    struct sockaddr_in cli_addr;//数据域  保存客户端信息的结构体
    struct __NODE *next;//下一节点的地址
}node_t;

#endif

执行结果

在这里插入图片描述

注意

  1. 链表只需插入第一次连接的(新登录的用户)网路信息结构体插入链表
//阻塞接收客户端发来的数据                            
if (-1 == recvfrom(sockfd, &user_d, sizeof(udp_t), 0,
			(struct sockaddr *)&client_addr,&client_addr_len))
	ERRLOG("recvfrom error");
if (user_d.OS == 1)
{
//在服务器显示
	printf("客户端 (%s:%d) >",
	inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
	printf("[%s]上线\n", user_d.name);
	//遍历链表,将消息转发给除了自己之外的所有人
	ptemp = phead;
	while (ptemp->next != NULL)
	{
	ptemp = ptemp->next;
	if (-1 == sendto(sockfd, &user_d, sizeof(udp_t), 0,
(struct sockaddr *)&(ptemp->cli_addr),sizeof(ptemp->cli_addr)))
	ERRLOG("sendto error");
	}
	//再将自己-使用头插法插入数据
	insert_into_list_by_head(phead, client_addr);
}
  1. 群聊时,将消息转发给除了自己之外的所有人
if (user_d.OS == 2)
{
    printf("[%s]发来消息\n", user_d.name);
    ptemp = phead;
    while (ptemp->next != NULL)
    {
        ptemp = ptemp->next;
        //判断不是自己再转发,是自己就不用发了
        if (memcmp(&(ptemp->cli_addr), &client_addr,
        						 sizeof(struct sockaddr_in)))
        {
           if (-1 == sendto(sockfd, &user_d, sizeof(udp_t), 0,
     (struct sockaddr *)&(ptemp->cli_addr), client_addr_len))
                ERRLOG("sendto error");
        }
    }
}
  1. 退出时,判断ptemp的下一节点,若将自己从链表中删除则不需往后走
ptemp = phead;
while (ptemp->next != NULL)
{
	//此时 判断ptemp的下一节点
    if (memcmp(&(ptemp->next->cli_addr), &client_addr, 
    							  sizeof(struct sockaddr_in)))
    {//不是自己就转发
    	//只有不删除节点的情况下  ptemp才往后走
        ptemp = ptemp->next; 
        if (-1 == sendto(sockfd, &user_d, sizeof(udp_t), 0,
      (struct sockaddr *)&(ptemp->cli_addr), client_addr_len))
            ERRLOG("sendto error");
    }
    else
    {
        //是自己 就将自己在链表中删除
        pdel = ptemp->next;
        ptemp->next = pdel->next;
        free(pdel);
        pdel = NULL;
    }
}
  1. 为实现父进程发送系统消息,将消息发给子进程
	printf("input > ");
	memset(user_d.buff2, 0, sizeof(user_d.buff2));
	scanf("%s", user_d.buff2);
	user_d.OS = 0;
	//把消息发给子进程
	if (-1 == sendto(sockfd, &user_d, sizeof(udp_t), 0,
			(struct sockaddr *)&server_addr, server_addr_len))
    ERRLOG("sendto error");

精简如下

UDP的网络群聊聊天室精简

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值