网络聊天室(Linux-C语言) -项目

项目要求

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

一定要根据项目要求去编写代码。。。。

理解

网络聊天室相当于我们手机QQ里的群聊,一个人发送消息,群聊中已经登录的用户都可以接受到消息。A退出群聊,群里登录的人就可以看到A已经退出群聊。B加入群聊,已经登录的用户也会看到B加入群聊。

问题思考

1.如何去实现通信?
我们学过网络间进程通信(服务器与客户端),这样我们就可以接受发数据了。
2.服务器要做的事?
收到客户端的消息,执行对应的操作。

●有几种消息类型需要实现?
登录服务器存储新的客户端的地址。把某个客户端登录的消息发给其它客户端。
聊天:服务器只需要把某个客户端的聊天消息转发给所有其它客户端
退出:服务器删除退出客户端的地址,并把退出消息发送给其它客户端

3.客户端要做的事?
连接服务器成功后,给服务器发送一个消息,表示你已经加入群聊,服务器接收到消息给各个已经登录的用户发送**用户加入群聊。从终端获取你发送的消息给服务器。客户端退出时给服务器发送消息。

解决问题:

1.服务器如何存储客户端的地址?

数据结构中的链式存储。

2.如何把聊天消息转发给其他用户?

通过遍历链表发送消息。

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

UDP客户端不会直接互连,所以不会获知其它客户端地址,所有客户端地址存储在服务器端。

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

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

重点来啦,要先画出程序流程图再去写代码

服务器端

客户端

在这里插入图片描述
两个结构体
一个用于链表存储客户端的地址。
一个用于客户端消息的发送(一个是要执行的操作类型,一个是用户名,一个是消息正文)。

直接上代码
head.h(头文件)
#ifndef __HEAD_H__
#define __HEAD_H__
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>

enum type_t
{
   Login,
   Chat,
   Quit,
};

//定义消息结构体   
typedef struct mag_t
{
   int type;//消息类型
   char name[32];//用户名
   char text[128];//消息内容
}MSG_t;

//链表的节点
typedef struct node_t
{
    struct sockaddr_in caddr;
    struct node_t *next;
}list_t;
 
#endif
Server
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include "head.h"

struct sockaddr_in serveraddr, caddr;

//创建有头单向链表
list_t *createList(void);
int login_client(int sockfd,MSG_t msg,list_t *p,struct sockaddr_in caddr);
int chat_client(int sockfd,MSG_t msg,list_t *p,struct sockaddr_in caddr);
int quit_client(int sockfd,MSG_t msg,list_t *p,struct sockaddr_in caddr);

void *pthread(void *arg)
{
    MSG_t msg;
    int sockfd=(*(int *)arg);
    msg.type=Chat;
    strcpy(msg.name,"server");
    while(1)
    {
        fgets(msg.text,sizeof(msg.text),stdin);
        if(msg.text[strlen(msg.text)-1]=='\n')
         msg.text[strlen(msg.text)-1]='\0';
         sendto(sockfd,&msg, sizeof(msg), 0,
            (struct sockaddr *)&serveraddr,sizeof(serveraddr)); 
    }
    pthread_exit(NULL);  
}

int main(int argc, char const *argv[])
{
      if(argc != 2)
    {
        printf("please input %s <ip> <port>\n",argv[0]);
        return -1;
    }
    //1.创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("socket err.");
        return -1;
    }
    //填充服务器端ip和端口
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(atoi(argv[1]));
    serveraddr.sin_addr.s_addr = inet_addr("0.0.0.0");

    socklen_t len = sizeof(caddr);

    //2.绑定
    if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
    {
        perror("bind err.");
        return -1;
    }
    //创建链表
    list_t *p=createList();

    //创建线程-服务器端发送消息
    pthread_t tid;
    pthread_create(&tid,NULL,pthread,&sockfd);
    pthread_detach(tid);

    //循环收发消息
    MSG_t msg;
    int recvbyte;
    while (1)
    {
        //接收 recvfrom
        recvbyte = recvfrom(sockfd,&msg, sizeof(msg), 0,
                            (struct sockaddr *)&caddr, &len);
        if (recvbyte < 0)
        {
            perror("recvfrom err.");
            return -1;
        }
        switch(msg.type)
        {
            case Login:
            login_client(sockfd,msg,p,caddr);
            break;
            case Chat: 
            chat_client(sockfd,msg,p,caddr);
            break;
            case Quit:
            quit_client(sockfd,msg,p,caddr);
            break;
        }
       
    }
    close(sockfd);
    return 0;
}

//创建有头单向链表
list_t *createList(void)
{
    //1.创建一个无效头节点:数据域无效,指针域有效
    list_t *p=(list_t *)malloc(sizeof(list_t));
    if(NULL == p)
    {
        perror("malloc head node err.");
        return NULL;
    }
    //2.初始化节点 空链表
    p->next=NULL;
    return p;
}

//1.客户端登录-服务器工作:
//1》遍历链表,将谁登录的消息发送所有已经登录的用户
//2》将新登录的客户端的ip和端口保存到链表中
int login_client(int sockfd,MSG_t msg,list_t *p,struct sockaddr_in caddr)
{   
    list_t *pnew=NULL;
    //1》遍历链表,将谁登录的消息发送所有已经登录的用户
     sprintf(msg.text,"%s login.",msg.name);
     while(p->next != NULL)
     {
         p=p->next;
         sendto(sockfd,&msg,sizeof(msg),0,\
         (struct sockaddr *)&(p->caddr),sizeof(p->caddr));
     }
     //2》将新登录的客户端的ip和端口保存到链表中
     //1-创建新节点保存
      pnew=(list_t *)malloc(sizeof(list_t));
      if(NULL == pnew)
      {
          perror("malloc new node err.");
          return -1;
      }
      //2-初始化
      pnew->caddr=caddr;
      pnew->next=NULL;
      //3-将节点连接到链表最后 p保存的链表最后一个节点的地址
      //直接链接最后就可以
      p->next=pnew;
      return 0;
}

//2.聊天-服务工作:
//1》将消息发送给所有除自己已经登录的用户
int chat_client(int sockfd,MSG_t msg,list_t *p,struct sockaddr_in caddr)
{
    //1》将消息发送给所有除自己已经登录的用户
    //1-遍历链表,只有不是自己的ip和端口就发送消息
    while(p->next != NULL)
    {
        p=p->next;
        if(memcmp(&(p->caddr),&caddr,sizeof(caddr)) != 0)
        {
            sendto(sockfd,&msg,sizeof(msg),0,\
         (struct sockaddr *)&(p->caddr),sizeof(p->caddr));
        }
    }
    return 0;
}

//3。客户端推出-服务器工作
//1》将推出的用户消息发送给还登录着的用户
//2》从链表中删除推出用户的ip和端口
int quit_client(int sockfd,MSG_t msg,list_t *p,struct sockaddr_in caddr)
{
    list_t *pdel=NULL;
    while(p->next != NULL)
    {
        if(memcmp(&(p->next->caddr),&caddr,sizeof(caddr))==0)
        {
            pdel=p->next;
            p->next=pdel->next;
            free(pdel);
            pdel=NULL;
        }else
        {
            p=p->next;
            sendto(sockfd,&msg,sizeof(msg),0,\
         (struct sockaddr *)&(p->caddr),sizeof(p->caddr));
        }
    }
    return 0;
}
client
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <string.h>
#include <signal.h>
#include "head.h"

int sockfd;
MSG_t msg;
struct sockaddr_in serveraddr;

void handler(int sig)
{
     msg.type=Quit;
     sendto(sockfd,&msg, sizeof(msg), 0,
            (struct sockaddr *)&serveraddr,sizeof(serveraddr)); 
      exit(-1);
}

int main(int argc, char const *argv[])
{
    if(argc != 3)
    {
        printf("please input %s <ip> <port>\n",argv[0]);
        return -1;
    }
    //1.创建套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        perror("socket err.");
        return -1;
    }
    //填充服务器端ip和端口
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(atoi(argv[2]));
    serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
    
    //ctrl + c 
    signal(SIGINT,handler);

    //循环收发消息
    int recvbyte;

    //1.登录
    msg.type=Login;
    //从终端获取用户名
    printf("please input name>>");
    fgets(msg.name,sizeof(msg.name),stdin);
    if(msg.name[strlen(msg.name)-1]=='\n')
      msg.name[strlen(msg.name)-1]='\0';
   //发送登录消息 sendto
    sendto(sockfd,&msg, sizeof(msg), 0,
         (struct sockaddr *)&serveraddr,sizeof(serveraddr)); 
   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';

        if(strncmp(msg.text,"quit",4) == 0)
        {
            msg.type=Quit;
            sendto(sockfd,&msg, sizeof(msg), 0,
            (struct sockaddr *)&serveraddr,sizeof(serveraddr)); 
            kill(getppid(),SIGKILL);
            exit(-1);
        }else{
            msg.type=Chat;
        }

        //发送消息 sendto
         sendto(sockfd,&msg, sizeof(msg), 0,
         (struct sockaddr *)&serveraddr,sizeof(serveraddr)); 
    }
   }else
   {
       //循环接受
       while(1)
       {
           int ret=recvfrom(sockfd,&msg, sizeof(msg), 0,
                           NULL, NULL);
            if(ret < 0)
            {
                perror("recvfrom err.");
                return -1;
            }
            printf("%s:%s\n",msg.name,msg.text);
       }
   }
    close(sockfd);
    return 0;
}
补充说明

枚举:
枚举是C语言中的一种基本数据类型,它可以让数据更简洁,更易读。

枚举语法定义格式为:

​​​​​​​enum 枚举名
{
    枚举元素1,枚举元素2,……     //注意,各元素之间用逗号隔开
};                            //注意,末尾有分号;

枚举是用来干嘛的
枚举在C语言中其实是一些符号常量集。直白点说:枚举定义了一些符号,这些符号的本质就是int类型的常量,每个符号和一个常量绑定。这个符号就表示一个自定义的一个识别码,编译器对枚举的认知就是符号常量所绑定的那个int类型的数字。(总的来说就是将一个单词定义为一个数字识别码,可以通过判断数字进行操作,通常和switch case搭配使用)
一般情况下我们都不明确指定这个符号所对应的数字,而让编译器自动分配。
如:

enum type_t
{
   Login,  //默认定义为1
   Chat,   //默认定义为2
   Quit,   //默认定义为3
};

编译器自动分配的原则是:从0开始依次增加。如果用户自己定义了一个值,则从那个值开始往后依次增加。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值