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()应答之前,允许在进入队列中等待的连接数目是10if(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 = 533if(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(¤t_time);printf("%s\n",ctime(¤t_time));printf("%s\n",buf);}}close(sfd);return 0;
}
//编译代码
//gcc client.c -o client
服务器可以在特定的端口监听客户端的连接请求,若连接成功,服务器采用广播的形式向当前所有连接客户端发送该客户端登录成功消息多个客户端可以同时登录,在源码文件中可以配置最多群聊同时在线人数。服务端接收到客户端发送的群聊信息后,也会采用广播的形式通知其他客户端,其他客户端接收后打印输出信息。这样就实现了简单版本模拟群聊。
这个版本有几个痛点
- 只有一个群,如果想同时在多个群群聊怎么办?
- 退出群聊后,群聊信息就没有了,如果想查看历史群聊信息怎么办?
- 用户在不同的群聊发送信息,服务端怎么将用户发送的信息广播给当前在线的且与发送信息的用户在同一群聊的用户?
更新版本
问题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 100typedef 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(¤t_time);char *str = ctime(¤t_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()应答之前,允许在进入队列中等待的连接数目是10if(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 = 533if(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(¤t_time);printf("%s",ctime(¤t_time));printf("%s\n\n",buf);}}close(sfd);return 0;
}
//编译代码
//gcc client.c -o client
程序演示
先看一下record目录,此时没有群聊文件
创建两个线程模拟两个客户端,并加入到群号为111111的群里
两个客户端正常通信。此时再查看record目录,发现多了一个111111.txt文件,证明此文件是用户加入群聊后自动创建的。
并且从上图可以看到每个客户端在进入群聊后都会去加载当前群的历史群聊信息
此时,再运行两个客户端,加入群号为222222的群中
可以发现,在222222群聊中发消息,消息只会出现在222222的群聊中,而111111中并没有,如此也证明用户在不同群聊中发送消息,消息只会被广播给与发送消息的用户在同一群聊中的在线用户这一功能实现了。