Linux-Socket实现模拟群聊(多人聊天室)

Linux-Socket实现模拟群聊(多人聊天室)

简单版本
服务端源码
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>

#define MAX 100
typedef struct Client{
    //socket文件描述符
    int cfd;
    //客户端名称
    char name[50];
}Client;
//设置最多群聊人数
Client client[MAX] = {};
size_t count = 0;

//初始化互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

//广播函数
void broadcast(char *msg, Client c){
    pthread_mutex_lock(&mutex);
    //给除了当前客户端的其他所有客户端发消息
    for(size_t i = 0; i < count; i++){
        if(client[i].cfd != c.cfd){
            if(send(client[i].cfd,msg,strlen(msg),0) <= 0){
                break;
            }
        }
    }
    pthread_mutex_unlock(&mutex);
}

//处理与每个客户端的交互
void *pthread_run(void *arg){
    Client c = *(Client*)(arg);
    while(1){
        char buf[1024] = {};
        strcpy(buf,c.name);
        strcat(buf," :");
        int ret = recv(c.cfd,buf + strlen(buf), 1024 - strlen(buf), 0);
        //如果没有接收到该客户端的消息,说明该客户端离线
        if(ret <= 0){
            for(size_t i = 0; i < count; i++){
                if(client[i].cfd == c.cfd){
                    //把该客户端的信息从客户端列表中删除
                    client[i] = client[count - 1];
                    count--;
                    strcpy(buf,c.name);
                    strcat(buf,"已退出群聊");
                    break;
                }
            }
            broadcast(buf,c);
            close(c.cfd);
            return NULL;
        }else{
            //接收到了客户端消息,则广播该消息
            broadcast(buf,c);
        }
    }
}

int main(int argc, char *argv[]){
    const char *ip;
    unsigned short int port;
    //如果没有指定ip地址和端口号,则使用默认ip地址(本机)和端口号
    if(argc < 3){
        ip = "127.0.0.1";
        port = 533;
    }else{
        ip = argv[1];
        port = atoi(argv[2]);
    }
    //使用TCP/IP(V4)协议
    int sfd = socket(AF_INET,SOCK_STREAM,0);
    if(sfd == -1){
        perror("socket err\n");
        return -1;
    }

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    //将port转换为网络字节序(大端模式)
    addr.sin_port = htons(port);
    //将点分十进制的IPv4地址转换成网络字节序列的长整型
    addr.sin_addr.s_addr = inet_addr(ip);
    socklen_t addrlen = sizeof(addr);
    //将ip地址绑定套接字
    int ret = bind(sfd,(struct sockaddr*)(&addr), addrlen);
    if( ret == -1){
        perror("bind error\n");   
        return -1;
    }
    //监听链接请求队列,accept()应答之前,允许在进入队列中等待的连接数目是10
    if(listen(sfd,10) == -1){
        perror("listen error\n");
        return -1;
    }
    printf("服务器已启动...\n");
    while(1){
        struct sockaddr_in caddr;
        socklen_t len = sizeof(caddr);
       
        int cfd = accept(sfd,(struct sockaddr*)(&caddr),&len);
        if(cfd == -1){
            perror("accept error\n");
            return -1;
        }
        //单次通信最大数据长度
        char buf[100] = {};
        recv(cfd,&client[count].name,50,0);
        //将该客户端保存到客户端列表
        client[count].cfd = cfd;
        //创建一个线程处理此次连接
        pthread_t tid;
        strcpy(buf,client[count].name);
        strcat(buf,"已加入群聊");
        broadcast(buf,client[count]);
        ret = pthread_create(&tid,NULL,pthread_run,(void*)(&client[count]));
        count++;
        if(ret != 0){
            printf("pthread_create: %s\n",strerror(ret));
            continue;
        }
        printf("有一个客户端成功连接:ip <%s> port [%hu]\n",inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));
    }
    
    return 0;
}

//编译代码
//gcc server.c -o server -lpthread
客户端源码
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<string.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>

int main(int argc, char *argv[]){
    const char *ip;
    unsigned short int port;
    //如果没指明,默认是ip = "127.0.0.1",port = 533
    if(argc < 3){
        ip = "127.0.0.1";
        port = 533;
    }else{
        ip = argv[1];
        port = atoi(argv[2]);
    }
    int sfd = socket(AF_INET,SOCK_STREAM,0);
    if(sfd == -1){
        perror("socket error\n");
        return -1;
    }
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = inet_addr(ip);
    socklen_t addrlen = sizeof(addr);

    int ret = connect(sfd,(const struct sockaddr*)(&addr),addrlen);
    if(ret == -1){
        perror("connect error\n");
        return -1;
    }
    char name[50];
    printf("请输入你的群聊昵称:");
    fgets(name,49,stdin);
    send(sfd,name,strlen(name) - 1, 0);
    //创建两个进程,父进程负责收消息,子进程负责发消息
    pid_t pid = fork();
    if(pid == -1){
        perror("fork error\n");
    }else if(pid == 0){
        while(1){
            char buf[1024] = {};
            fgets(buf,1023,stdin);
            if(send(sfd,buf,strlen(buf) + 1,0) <= 0){
                break;
            }
        }
    }else{
        while(1){
            char buf[1024] = {};
            if(recv(sfd,buf,1024,0) <= 0){
                break;
            }
            time_t current_time;
            time(&current_time);
            printf("%s\n",ctime(&current_time));
            printf("%s\n",buf);
        }
    }
    close(sfd);
    return 0;
}
//编译代码
//gcc client.c -o client

服务器可以在特定的端口监听客户端的连接请求,若连接成功,服务器采用广播的形式向当前所有连接客户端发送该客户端登录成功消息多个客户端可以同时登录,在源码文件中可以配置最多群聊同时在线人数。服务端接收到客户端发送的群聊信息后,也会采用广播的形式通知其他客户端,其他客户端接收后打印输出信息。这样就实现了简单版本模拟群聊。

这个版本有几个痛点

  1. 只有一个群,如果想同时在多个群群聊怎么办?
  2. 退出群聊后,群聊信息就没有了,如果想查看历史群聊信息怎么办?
  3. 用户在不同的群聊发送信息,服务端怎么将用户发送的信息广播给当前在线的且与发送信息的用户在同一群聊的用户?
更新版本

问题1解决方案:

给每个群聊设置一个群聊标识(群号),在启动客户端时,通过输入不同的群聊标识来进入不同的群。

问题2解决方案:

服务端为每一个群聊创建一个文本文件放入record目录中,以此文本文件存储群聊信息

在服务端Client结构体中加入address属性来记录当前群聊所对应的文本文件的地址

用户运行客户端程序,输入群号来加入群聊,如果该群号所对应的群不存在,那么服务端就为该群创建一个文本文件。如果该群号所对应的文本文件存在于record目录中,那么客户端程序就加载并打印该文件的内容

如此便实现了查看历史信息

问题3解决方案:

在服务端Client结构体中增加一个属性pid来记录当前客户端连接所加入的群聊的群号。用户每次运行客户端程序,需要输入要加入的群的群号,然后发送给服务端,服务端将此号赋值给表示当前连接的Client结构体中的pid属性。同一用户打开多个窗口运行客户端程序,在服务器的角度是创建了多个连接,会被当做不同用户看待,但是在用户的角度,就相当于在多个群进行聊天。 只要客户端与服务端每次通信时携带当前所在群的群号,在由服务端解析出群号,使用广播函数时判断当前所有客户端连接对应的群号和解析出的群号是否一致,一致就转发消息。

如此便实现了用户在群聊中发消息,服务端转发消息时只转发给与发送消息的用户在同一个群中的在线用户

核心概念是一个客户端与服务器的连接只能加入一个群,而同一用户通过打开不同的窗口运行客户端程序来创建多个连接,以此来加入不同的群

服务端代码
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<unistd.h>
#include<string.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<sys/types.h>
#include<dirent.h>
#include<sys/stat.h>
#include<time.h>
#include<fcntl.h>

#define MAX 100

typedef struct Client{
    //socket文件描述符
    int cfd;
    //客户端名称
    char name[50];
    //群号,6位
    char id[7];
    //群聊信息文件地址
    char address[128];
}Client;
//设置最多群聊人数
Client client[MAX] = {};
size_t count = 0;

//初始化互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//保存聊天记录
void save(char *msg, Client c){
    char record[1024] = {};
    time_t current_time;
    time(&current_time);
    char *str = ctime(&current_time);
    int fd;
    fd = open(c.address,O_APPEND | O_WRONLY);
    if(fd == -1){
        perror("server open record error\n");
        return;
    }
    sprintf(record,"%s%s\n\n",str,msg);
    int ret = write(fd,record,strlen(record));
    if(ret == -1){
        perror("wirte record error\n");
        return;
    }
    close(fd);
}

//广播函数
void broadcast(char *msg, Client c){
    pthread_mutex_lock(&mutex);
    save(msg,c);
    //广播给与当前用户在同一群聊中的所有其他用户
    for(size_t i = 0; i < count; i++){
        if(client[i].cfd != c.cfd && strcmp(client[i].id,c.id) == 0){
            if(send(client[i].cfd,msg,strlen(msg),0) <= 0){
                break;
            }
        }
    }
    pthread_mutex_unlock(&mutex);
}
//判断群号是否存在于record目录里,否创建文件
void exits(Client c){
    DIR *db;
    struct dirent *p;
    db = opendir("/root/linux/communicate/record");
    char temp[20];
    sprintf(temp,"%s%s",c.id,".txt");
    int flag = 0;
    while((p = readdir(db))){
        if(strcmp(p->d_name,temp) == 0){
            flag = 1;
              break;
        }
    }
    if(flag == 0){
        umask(0);
        int ret = creat(c.address,0666);
        if(ret == -1) perror("creat record error\n");
    }
    closedir(db);
}
//对每一个客户端连接都创建一个线程处理
void *pthread_run(void *arg){
    Client c = *(Client*)(arg);
    exits(c);
    //单次通信最大数据长度
    char buf[100] = {};
    strcpy(buf,c.name);
    strcat(buf,"已加入群聊");
    broadcast(buf,c);
    while(1){
        char buf[1024] = {};
        strcpy(buf,c.name);
        strcat(buf," :");
        int ret = recv(c.cfd,buf + strlen(buf), 1024 - strlen(buf), 0);
        //如果没有接收到该客户端的消息,说明该客户端离线
        if(ret <= 0){
            for(size_t i = 0; i < count; i++){
                if(client[i].cfd == c.cfd){
                    //把该客户端的信息从客户端列表中删除
                    client[i] = client[count - 1];
                    count--;
                    strcpy(buf,c.name);
                    strcat(buf,"已退出群聊");
                    break;
                }
            }
            broadcast(buf,c);
            close(c.cfd);
            return NULL;
        }else{
            //接收到了客户端消息,则广播该消息
            broadcast(buf,c);
        }
    }
}

//接收用户要加入的群号和用户昵称,并将客户端保存到客户端列表
void receive(int cfd){
    char temp[128] = {};
    recv(cfd,temp,128,0);
    int i = 0;
    while(i < 6){
        client[count].id[i] = temp[i];
        i++;
    }
    client[count].id[i] = '\0';
    int j = 0;
    while(i < strlen(temp)){
        client[count].name[j] = temp[i];
        i++;
        j++;
    }
    client[count].name[i] = '\0';
    sprintf(client[count].address,"%s/%s%s","/root/linux/communicate/record",client[count].id,".txt");
    client[count].cfd = cfd;
}

//服务端socket初始化
int inet_init(const char *ip, unsigned short int port){
    //使用TCP/IP(V4)协议
    int sfd = socket(AF_INET,SOCK_STREAM,0);
    if(sfd == -1){
        perror("socket err\n");
        return -1;
    }

    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    //将port转换为网络字节序(大端模式)
    addr.sin_port = htons(port);
    //将点分十进制的IPv4地址转换成网络字节序列的长整型
    addr.sin_addr.s_addr = inet_addr(ip);
    socklen_t addrlen = sizeof(addr);
    //将ip地址绑定套接字
    int ret = bind(sfd,(struct sockaddr*)(&addr), addrlen);
    if( ret == -1){
        perror("bind error\n");   
        return -1;
    }
    //监听链接请求队列,accept()应答之前,允许在进入队列中等待的连接数目是10
    if(listen(sfd,10) == -1){
        perror("listen error\n");
        return -1;
    }
    return sfd;
}

int main(int argc, char *argv[]){
    const char *ip;
    unsigned short int port;
    //如果没有指定ip地址和端口号,则使用默认ip地址(本机)和端口号
    if(argc < 3){
        ip = "127.0.0.1";
        port = 533;
    }else{
        ip = argv[1];
        port = atoi(argv[2]);
    }

    int sfd = inet_init(ip, port);
    if(sfd == -1){
        perror("server socket init error\n");
        return -1;
    }
    printf("服务器已启动...\n");
    while(1){
        struct sockaddr_in caddr;
        socklen_t len = sizeof(caddr);
       
        int cfd = accept(sfd,(struct sockaddr*)(&caddr),&len);
        if(cfd == -1){
            perror("accept error\n");
            return -1;
        }
        receive(cfd);
        //创建一个线程处理此次连接
        pthread_t tid;
        int ret = pthread_create(&tid,NULL,pthread_run,(void*)(&client[count]));
        count++;
        if(ret != 0){
            printf("pthread_create: %s\n",strerror(ret));
            continue;
        }
        printf("有一个客户端成功连接:ip <%s> port [%hu]\n",inet_ntoa(caddr.sin_addr),ntohs(caddr.sin_port));
    }
    
    return 0;
}
//编译代码
//gcc server.c -o server -lpthread
客户端代码
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<string.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<fcntl.h>
#include<dirent.h>
#include<sys/stat.h>
//打印历史信息
void print_history(char *id){
    char filename[128] = {};
    sprintf(filename,"%s/%s%s","/root/linux/communicate/record",id,".txt");
    int fd;
    fd = open(filename,O_RDONLY);
    if(fd == -1){
        perror("client open record error\n");
    }
    int len;
    char buf[1024];
    while((len = read(fd,buf,1024)) != 0){
        printf("%s",buf);
        memset(buf,'\0',1024);
    }
    printf("------------历史群聊信息-----------\n");
    close(fd);
}
int main(int argc, char *argv[]){
    const char *ip;
    unsigned short int port;
    //如果没指明,默认是ip = "127.0.0.1",port = 533
    if(argc < 3){
        ip = "127.0.0.1";
        port = 533;
    }else{
        ip = argv[1];
        port = atoi(argv[2]);
    }
    int sfd = socket(AF_INET,SOCK_STREAM,0);
    if(sfd == -1){
        perror("socket error\n");
        return -1;
    }
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(port);
    addr.sin_addr.s_addr = inet_addr(ip);
    socklen_t addrlen = sizeof(addr);

    int ret = connect(sfd,(const struct sockaddr*)(&addr),addrlen);
    if(ret == -1){
        perror("connect error\n");
        return -1;
    }
    char name[50];
    char id[8];
    printf("请输入群号:");
    fgets(id,8,stdin);
    printf("请输入你的群聊昵称:");
    fgets(name,49,stdin);
    char temp[128];
    strncat(temp,id,6);
    strncat(temp,name,strlen(name) - 1);
    send(sfd, temp, strlen(temp), 0);
    char cutid[7] = {};
    strncat(cutid,id,6);
    sleep(1);
    print_history(cutid);
    //创建两个进程,父进程负责收消息,子进程负责发消息
    pid_t pid = fork();
    if(pid == -1){
        perror("fork error\n");
    }else if(pid == 0){
        while(1){
            char buf[1024] = {};
            fgets(buf,1023,stdin);
            if(send(sfd,buf,strlen(buf) + 1,0) <= 0){
                break;
            }
            printf("\n");
        }
    }else{
        while(1){
            char buf[1024] = {};
            if(recv(sfd,buf,1024,0) <= 0){
                break;
            }
            time_t current_time;
            time(&current_time);
            printf("%s",ctime(&current_time));
            printf("%s\n\n",buf);
        }
    }
    close(sfd);
    return 0;
}
//编译代码
//gcc client.c -o client
程序演示

先看一下record目录,此时没有群聊文件

在这里插入图片描述

创建两个线程模拟两个客户端,并加入到群号为111111的群里

在这里插入图片描述

两个客户端正常通信。此时再查看record目录,发现多了一个111111.txt文件,证明此文件是用户加入群聊后自动创建的。
在这里插入图片描述

并且从上图可以看到每个客户端在进入群聊后都会去加载当前群的历史群聊信息

此时,再运行两个客户端,加入群号为222222的群中

在这里插入图片描述

可以发现,在222222群聊中发消息,消息只会出现在222222的群聊中,而111111中并没有,如此也证明用户在不同群聊中发送消息,消息只会被广播给与发送消息的用户在同一群聊中的在线用户这一功能实现了。

  • 37
    点赞
  • 100
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值