Linux c 基于UDP实现多人聊天室

1.数据报格式套接字

数据报格式套接字(代码为SOCK_DGRAM),采用此方法计算机只管发送数据而不去验证发送数据的正确性,不论传输是否被接收,数据流是否有丢失,都不再重新发送,特征如下:

1.强调快速传输而非传输顺序;

2.传输的数据可能丢失也可能损毁;

3.限制每次传输的数据大小;

4.数据的发送和接收是并发的。

总之,数据报套接字是一种不可靠的、不按顺序传递的、以追求速度为目的的套接字。

以上总结于套接字有哪些类型?socket有哪些类型?

2.采用UDP协议

而数据报套接字采用的是(User Datagram Protocol)协议,本次聊天室采用UDP协议虽然可能会导致数据丢失,但是聊天并不去强调内容的正确性,而应该强调实时性,并且数据丢失只是小概率事件。

3.实现

流程:  server 接收来自client的消息并转发给其他client, 并且自身也能发布公告

           client 发送消息给server,接收来自server的信息(包括群发内容和公告)

server.c

在服务器定义了两个结构体,INFOusr_list分别去接受客户端的信息和保存接入客户端的信息。

typedef int protocol; //自己定义的协议 1为用户登录信息 2为普通信息 3为退出信息
typedef struct{
    char name[13]; //用户昵称(自定义)
    char msg[64];  //正文内容
    protocol P;    //协议
}INFO;
 
typedef struct usr_list{
    struct sockaddr_in usr_addr; //保存接入用户的信息
    struct usr_list *next;        //指针
}usr_node,*List;

代码

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

typedef int protocol; //自定义协议 1登录信息 2普通信息 3退出信息
typedef struct sockaddr_in IN;
int len_s = sizeof(struct sockaddr_in);
IN serv_addr,clnt_addr;

typedef struct{
    char name[13];
    char msg[64];
    protocol P;
}INFO;
int len_info = sizeof(INFO);

typedef struct usr_list{
    IN usr_addr;
    struct usr_list *next;
}usr_node,*List;
List list; //创建用户链表为全局变量,之后利用线程分别实现接收转发消息和server自身发送消息

void init(); //初始化用户链表
void get_len();//得到链表长度(在测试时用,本例子无实际意义)
void insert_node(IN usr_addr);  //插入节点
void del_node(IN usr_addr);    //删除节点

void send_log(int serv_fd,INFO info,IN clnt_addr);  //发送登录信息
void send_msg(int serv_fd,INFO info,IN clnt_addr);  //发送普通信息
void send_off(int serv_fd,INFO info,IN clnt_addr);  //发送离线信息
void serv_send(int serv_fd,char *buf);              //server发送消息

void *thread_handler(void *); //子线程任务,实现接收转发
int main(int argc, char *argv[])
{ 
    if( argc < 3){
        printf("input-> %s <ip> <port>n",argv[0]);
        exit(0);
    }
    int ret;
    int serv_fd;
    pthread_t thread;
    init();                             //初始化链表
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(atoi(argv[2])); //端口号作为终端输入的第三个参数
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]); //IP地址作为终端输入的第二个参数
  //  serv_addr.sin_port = htons(6060);
  //  serv_addr.sin_addr.s_addr = inet_addr("192.168.17.80");
    
    serv_fd = socket(AF_INET,SOCK_DGRAM,0); //创建套接字(SOCK_DGRAM)
    if(serv_fd < 0){
        perror("socket");
        exit(0);
    }
    ret = bind(serv_fd,(struct sockaddr*)&serv_addr,sizeof(serv_addr)); //绑定
    if(ret < 0){
        perror("bind");
        exit(0);
    }
    printf("server open...n");
    pthread_create(&thread,NULL,thread_handler,&serv_fd); //创建子线程
    while(1){
        char buf[64] = {0};
        char msg[48] = {0};
        strcat(buf,"server announce:");
        fgets(msg,48,stdin);          //接收server需要公告的信息
        strcat(buf,msg);               //将信息添加到数组 
        serv_send(serv_fd,buf);        //server群发;
        if( strcmp(msg,"exitn") == 0) //server是否主动关闭,与exitn比较时应该fgets会接收n,推荐strncmp
            exit(0);
        memset(msg,0,48); //清空
        memset(buf,0,64); //清空
    }
    return 0;
} 

void *thread_handler(void *arg){
    int serv_fd = *(int *)arg;
    int ret;
    while(1){
            INFO info;
            //接收来自用户端的信息,保存在info中
            ret = recvfrom(serv_fd,&info,len_info,0,(struct sockaddr*)&clnt_addr,&len_s);        
            printf("recv a message from %s:",info.name);
            if(ret < 0){
                perror("recvfrom");
                exit(0);
            }
            //根据自定义协议来进行相应操作
            switch(info.P){
                case 1:{send_log(serv_fd,info,clnt_addr); break;}
                case 2:{send_msg(serv_fd,info,clnt_addr); break;}
                case 3:{send_off(serv_fd,info,clnt_addr); break;}
            }
        }
}

void serv_send(int serv_fd,char *buf){
    usr_node *p = list->next;
    printf("you send a msg to clientsn");
    while(p){
        //对链表保存的用户依次发送
        sendto(serv_fd,buf,strlen(buf),0,(struct sockaddr*)&(p->usr_addr),len_s);
        p = p->next;
    }
}

void send_off(int serv_fd,INFO info,IN clnt_addr){
    usr_node *p = list->next;
    char buf[100] = {0};
    strcat(buf,info.name);
    strcat(buf,"Offlinen"); //将用户昵称+Offline保存到buf缓冲区进行群发
    //buf[strlen(buf)] = '';
    printf("%sn","Offline");
    while(p){
        sendto(serv_fd,buf,strlen(buf),0,(struct sockaddr*)&(p->usr_addr),len_s);
        p = p->next;
    }
    del_node(clnt_addr); //删除退出用户节点
}

void send_msg(int serv_fd,INFO info,IN clnt_addr){
    usr_node *p = list->next;
    char buf[100] = {0};
    strcat(buf,info.name);
    strcat(buf,":");
    strcat(buf,info.msg);  //将昵称+:+内容保存到缓冲区
    buf[strlen(buf)] = ''; //接收的最后一个字符为'n',删除
    printf("%s",info.msg);
    //群发
    while(p){
        //判断是不是用户自身
        if(p->usr_addr.sin_port != clnt_addr.sin_port || (p->usr_addr.sin_addr.s_addr & clnt_addr.sin_addr.s_addr) != clnt_addr.sin_addr.s_addr)
        sendto(serv_fd,buf,strlen(buf),0,(struct sockaddr*)&(p->usr_addr),len_s);
        p = p->next;
    }
}

void send_log(int serv_fd,INFO info,IN clnt_addr){
    if(list == NULL)
        printf("list is nulln");
    usr_node *p = list->next;
    char buf[30] = {0};
    strcat(buf,info.name);
    strcat(buf," login...n"); //将昵称+login...保存到缓冲区群发
    int i = 1;
    printf("%sn","login..."); 
    while(p){
        sendto(serv_fd,buf,strlen(buf),0,(struct sockaddr*)&(p->usr_addr),len_s);
       // printf("%dn",i++);
        p = p->next;
    }
    insert_node(clnt_addr); //增加新用户节点
}
//删除节点
void del_node(IN usr_addr){
    List p = list->next,s = list; //p为需要删除节点,s为p的前驱节点
    int i = 1;
    while(p && p->next!= NULL){
        if(p->usr_addr.sin_port == usr_addr.sin_port && (p->usr_addr.sin_addr.s_addr & clnt_addr.sin_addr.s_addr) == clnt_addr.sin_addr.s_addr)
            break;
        p = p->next;
        s = s->next;
    //    printf("%dn",i++);
    }
    //多余的步骤 只需要s->next = p->next;
    if(p->next == NULL){
        s->next = NULL;
        free(p);
        return ;
    }
    s->next = p->next;
    free(p);
}
//尾插法(头插更简单)
void insert_node(IN usr_addr){
    usr_node *node,*p = list;
    int i = 1;
    while( p->next != NULL ){
        p = p->next;
      //  printf("%dn",i++);
    }
    node = (List)malloc(sizeof(usr_node));
    node->usr_addr = usr_addr;
    node->next = p->next;
    p->next = node;
   // get_len();
}
//得到链表长度(去头结点)
void get_len(){
    usr_node *p = list->next;
    int i = 0;
    while(p){
        i++;
        p = p->next;
    }
    printf("%dn",i);
}
//初始化
void init(){
    list = (List )malloc(sizeof(usr_node));
    list->next = NULL;
}

client.c

client使用了一个与server相同结构体INFO用于存储发送的信息

代码

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <netinet/in.h>
#define TRUE 1
#define FALSE 0
typedef int Bool;
typedef int protocol;
typedef struct{
    char name[13];
    char msg[64];
    protocol P;
}INFO;    //INFO结构体,保存消息内容
//用户输入信息操作
INFO create_msg(INFO info,char *name){
    char msg[64];   //缓冲区
    fgets(msg,64,stdin);  
    strcpy(info.name,name);  //将用户自定义昵称保存在INFO的name数组中
    strcpy(info.msg,msg);    //将fgets得到的正文保存在INFO的msg数组中
    if(strcmp(msg,"exitn") == 0){   
        info.P = 3;     // 3为退出协议
        printf("offlinen");
    }else
        info.P = 2;   // 2为正文
    return info;
}
int main(int argc, char *argv[])
{ 
    if( argc < 3){
        printf("input-> %s <ip> <port>",argv[0]);
        exit(0);
    }
    int serv_fd;
    int ret;
    INFO info;
    struct sockaddr_in serv_addr,clnt_addr;
    int len_s,len_c;
    //Bool jug_exit = FALSE;
    pid_t pid;
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(atoi(argv[2]));
    serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
    
    serv_fd = socket(AF_INET,SOCK_DGRAM,0);   //创建套接字(SOCK_DGRAM)
    if(serv_fd < 0){
        perror("socket");
        exit(0);
    }
    printf("connected...n");
    len_s = sizeof(serv_addr);
    char name[13];
    printf("set your name:");
    fgets(name,13,stdin);     //设置昵称
    name[strlen(name)-1] = '';
    strcpy(info.name,name);
    info.P = 1;   //1 为登录协议
    sendto(serv_fd,&info,sizeof(info),0,(struct sockaddr*)&serv_addr,len_s); //将登录信息发送给server
    printf("=================communication area=================n");
    pid = fork(); //创建两个进程,一个用来发送消息,一个用来接收server和其他client的消息
    if(pid < 0){
        perror("fork");
        exit(0);
    }
    if(pid == 0){   //接收进程
        char is_exit[20];
        strcat(is_exit,name);
        strcat(is_exit,"Offlinen");//定义判断退出字符串
        while(1){
            char buf[64];
            //接收来自server发送或转发的消息
            recvfrom(serv_fd,&buf,sizeof(buf),0,(struct sockaddr*)&serv_addr,&len_s);
            if(strcmp(buf,is_exit) == 0) //如果server返回的是与退出字符串相同的字符串则离线
                 exit(0);
            printf("%s",buf); //显示接收到的消息
            memset(buf,0,64);
       }
    }else{      //发送进程
    char msg[64] = {0};
    while(1){
        info = create_msg(info,name);   //调用函数,将将要发送的消息保存到info里面;
        //发送到server
        ret = sendto(serv_fd,&info,sizeof(info),0,(struct sockaddr*)&serv_addr,len_s);
        if(ret < 0){
            perror("recvfrom");
            exit(0);
        }
        //如果是退出协议,则退出进程
        if(info.P == 3){
            jug_exit = TRUE;
            wait(0);
            break;
        }
        memset(&info,0,sizeof(info));
        }
    }
    return 0;
} 

效果展示

 基本功能已大致实现,但任需要优化。

总结

 整体项目并不复杂,只需要想清楚server和client分别需要做什么事。server为了保存每次连入进来的client信息,使用了链表,对链表的操作就是简单的插入,删除操作,只是在server中对链表的改变时需要使用互斥pv,避免在收发消息时进行链表的增加删除(本次项目并未实现)。这算数最近学习c语言实现的一个稍微综合一点的项目,虽然不多,却也有所收获,便发布一篇帖子去记录,代码中可能有许多不规范的地方,逻辑可能并不严谨,还请各位不吝赐教。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>