在上次所写的Linux多人聊天(一)中,仅实现了群聊的功能,本篇中实现了登录(/:login\n),注册(/:regis\n),查询个人信息(/:search\n),私聊(/:pchat\n)/群聊(/:gchat\n),清屏(/:clear\n),退出(/:exit!\n)等功能,其中账号,密码以及个人信息都是在数据库(mysql->test_db->users)中保存。
注意:mysql.h并没有在标准C函数库中,可以用sudo apt-get install libmysqlclient-dev命令下载,之后用whereis mysql.h即可找到该头文件的路径
客户端:gcc -Wall -o chat chat.c -pthread
预编译与(一)相同,此处不再列出。
该部分是一个在连接服务器成功之后显示出来的一个简单菜单,仅提供登录注册和退出选项。
void print()
{
printf("1.login\n2.regis\n3.exit\nplease input your choose:");
switch(getch())
{
case '1':printf("1\n");send(fd,"/:login\n",8,0);login();break;
case '2':printf("2\n");send(fd,"/:regis\n",8,0);regis();break;
case '3':printf("3\n");exit(0);break;
default :
printf("no such choice!!\n");
exit(0);
}
}
由于在输入密码时不能直接显示原本的字符,所以需要使用getch函数,但是Linux上无法直接使用,因此需要编写一个函数能够实现getch()的功能。
int getch(void)
{
struct termios tm, tm_old;
int fd = 0, ch;
if (tcgetattr(fd, &tm) < 0) {
return -1;
}
tm_old = tm;
cfmakeraw(&tm);
if (tcsetattr(fd, TCSANOW, &tm) < 0) {
return -1;
}
ch = getchar();
if (tcsetattr(fd, TCSANOW, &tm_old) < 0) {
return -1;
}
return ch;
}
创建套接字以及连接与(一)中操作没有区别,只是将其写在了自定义的函数中
void pre_connect(int argc,char *argv[])
{
struct sockaddr_in server;
if(argc<=2)
{
printf("%s ip_address port_number!!\n",argv[0]);
exit(1);
}
int port=atoi(argv[2]);
const char *ip=argv[1];
if((fd=socket(AF_INET,SOCK_STREAM,0))==-1)
{
printf("socket() error!!\n");
exit(1);
}
memset(&server,0,sizeof(struct sockaddr));
server.sin_family=AF_INET;
server.sin_port=htons(port);
inet_pton(AF_INET,ip,&server.sin_addr);
if(connect(fd,(struct sockaddr*)&server,sizeof(struct sockaddr))==-1)
{
printf("connect error!!\n");
exit(1);
}
}
登录:该部分需要与服务器交互完成,即输入并发送账号,确认账号正确与否,输入并发送密码,确认正确与否,登陆成功后会自动清一次屏,并打印出各种命令的操作方式。
void login()
{
char recvs[3]={'\0'};
char num[11]={'\0'};
char passwd[20]={'\0'};
printf("please input your number:");
fgets(num,sizeof(num),stdin);
send(fd,num,strlen(num),0); //向服务器发送账号,确认账号是否正确
recv(fd,recvs,3,0);
if(!strcmp(recvs,"1\n")) //若接收到1,则账户正确,输入密码
{
int i=0;
printf("please input your password:");
while((passwd[i]=getch())!='\r')
{
if(passwd[i]==127) //这里的迷之127,不知道为什么\b不可以
{
i--;passwd[i]='\0';printf("\b \b");
}
else
{
printf("*");++i;
}
}
passwd[i]='\n';
passwd[i+1]='\0';
send(fd,passwd,strlen(passwd),0);
memset(recvs,0,sizeof(recvs));
recv(fd,recvs,3,0);
if(!strcmp(recvs,"1\n")) //接收到1,密码匹配正确,登陆成功,清屏,打印命令说明
{
system("clear");
printf("\nlogin successfully!\n");
printf("* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n");
printf("* You can use cmd: /:pchat+enter+number switch to private chat! *\n");
printf("* You can use cmd: /:search+enter+number to search one's information! *\n");
printf("* You can use cmd: /:gchat+enter switch to group chat! *\n");
printf("* You can use cmd: /:clear+enter to clean the screen! *\n");
printf("* You can use cmd: /:exit!+enter to exit the chat! *\n");
printf("* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *\n");
}
else {printf("\nfailed to login!!\n");exit(1);}
}
else
{printf("number isn't exist!\n");exit(1);}
}
注册:该部分主要是用户信息的录入以及简单的格式判断,将信息发送给服务器后服务器会返回一个账号,然后退出程序
void regis()
{
char num[17]={'\0'},name[30],sex[4],age[4],sig[60],passwd[20];
printf("you will regis a number\n(you must input the choices with '*')\n");
while(1) //设置密码,6~18位
{
int i=0;
printf("please set a password for your number(*):");
while((passwd[i]=getch())!='\r')
{
if(passwd[i]==127){
i--;passwd[i]='\0';printf("\b \b");
}
else {
printf("*");
++i;
}
}
passwd[i]='\n';
passwd[i+1]='\0';
if(i<6||i>19) {printf("Unlawful cipher!!\n");}
else {printf("\n");break;}
}
send(fd,passwd,strlen(passwd),0);
while(1) //输入用户名
{
printf("please input your name(*):");
fgets(name,sizeof(name),stdin);
if(strcmp(name,"\n")) break;
else printf("Unlawful name!!\n");
}
send(fd,name,strlen(name),0);
while(1) //输入性别,仅限f/m
{
printf("please input your sex(f/m)(*):");
fgets(sex,sizeof(sex),stdin);
if(!strcmp(sex,"m\n")||!strcmp(sex,"f\n")) break;
else printf("Unlawful sex!!\n");
}
send(fd,sex,strlen(sex),0);
while(1) //输入年龄
{
printf("please input your age(*):");
fgets(age,sizeof(age),stdin);
if(strlen(age)>3||!(age[0]>='0'&&age[0]<='9')) printf("Unlawful age!!\n");
else break;
}
send(fd,age,strlen(age),0);
printf("please input your personality signature:");//个性签名,可不输入,回车跳过
fgets(sig,sizeof(sig),stdin);
send(fd,sig,strlen(sig),0);
recv(fd,num,16,0);if(strlen(num)!=9) printf("%s\n",num); //接收服务器返回的账号
else
printf("regis successfully!your number is:\n%s\n",num);
exit(0);
}
相较于(一)中客户端的接收线程,由于此次有多种命令因此在接收消息时多加了几种特例
void *pthread_recv(void* ptr)
{
while(1)
{
int ret;
memset(recvs,0,sizeof(recvs));
if((ret=read(fd,recvs,sizeof(recvs)-1))>0)
{
if(!strcmp(recvs,"/:clear\n")) //若服务器发送过来的是清屏消息,同样需要清屏一次
system("clear");
else if(!strcmp(recvs,"search successfully!\n")) //如果接收到这条消息,则服务器在数据库中查询到该账号信息,则紧接着应该对服务器发送过来的信息进行处理
{
recvs[strlen(recvs)-1]='\0';
printf("%s\n",recvs);
memset(recvs,0,sizeof(recvs));
recv(fd,recvs,sizeof(recvs),0);
char *p=recvs;int i=1;
char in[5][11]={"name:","sex:","age:","signature:"};
printf("%s",in[0]);
for(;*p!='\0';p++)
{
if(*p=='#')
{
printf("\n%s",in[i]);
++i;
}
else printf("%c",*p);
}
}else if(!strcmp(recvs,"search failed!!\n")){
printf("search error!!\n");
}else printf("%s",recvs);
}
else
exit(1);
}
}
相应的发送线程也有所变化
void send_server()
{
char sends[MAX_mess];
while(1)
{
memset(sends,'\0',sizeof(sends));
fgets(sends,sizeof(sends),stdin);
if(!strcmp(sends,"/:login\n")) //在登陆成功之后不得再次使用登录和注册功能
printf("you needn't to login!\n");
else if(!strcmp(sends,"/:regis\n"))
printf("you can't regis here\n");
else if(!strcmp(sends,"/:clear\n")) //用户以命令主动清屏
system("clear");
else
{
send(fd,sends,strlen(sends),0);
if(!strcmp(sends,"/:exit!\n"))
{
printf("you choose to exit!!\n");
close(fd);
exit(0);
}
else if(!strcmp(sends,"/:search\n")) //如果输入了查询命令,则紧接着应该输入对方的账号
{
char num[11]={'\0'};
fgets(num,sizeof(num),stdin);
system("clear");
end(fd,num,strlen(num),0);
}
}
}
}
最后是主函数
int main(int argc,char *argv[])
{
char str[40]={'\0'};
pre_connect(argc,argv);
recv(fd,str,sizeof(str),0); //连接成功后先接收一条消息,判断服务器是否满载
if(strcmp(str,"\n"))
{
printf("%s",str);
exit(1);
}
print();
pthread_t tid;
pthread_create(&tid,NULL,pthread_recv,NULL);
send_server();
}
服务器:gcc -Wall -o server server.c -l mysqlclient -L /usr/lib64/mysql -I /usr/include/mysql/ -pthread
由于新的功能的加入,本篇的服务器预编译处有所不同,因此列出
#include <stdio.h>
#include <netdb.h>
#include <mysql.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <bits/signum.h>
#include <errno.h>
#define USR_LIMIT 10 //最大用户数量
#define MAX_BAG 1025 //消息最大长度
struct pro //为void *cho_c()线程临时创建,保存其参数
{
int fd;
char bag[10];
};
struct client
{
int cfd; //客户端套接口
int stat; //判断群聊/私聊,以及私聊的目标端口
char num[11]; //当前客户账号
char name[30]; //当前客户姓名
}user[USR_LIMIT];
int sum=0; //当前用户总数
fd_set rfds; //select判断可读事件
void *cho_c();
void mode_cmd();
void listen_c();
void *recv_bag();
void pre_connect();
void SendToOther();
void SendToClient();
登录,注册和查询:其中有一些数据库的函数操作,详细请自行百度
void mode_cmd(int fd,char *bag) //由于登录,注册和查询功能需要与数据库交互,因此将这三个命令写成一个函数
{
MYSQL mysql,*sock;
mysql_init(&mysql);
if((sock=mysql_real_connect(&mysql,"ipv4 address","username","password","test_db",0,NULL,0))==NULL) //数据库连接,需要ip地址,用户名,用户密码和数据库名(test_db)
//若要远程连接数据库,可以参考此博客https://www.cnblogs.com/skyWings/p/5952795.html
{
fprintf(stderr,"Failed to connect to database:Error:%s\n",mysql_error(&mysql));
exit(1);
}
if(!strcmp(bag,"/:login\n"))
{
int ret=0;
char num[11]={'\0'},name[30]={'\0'},passwd[20]={'\0'},i_query[200];
recv(fd,num,sizeof(num),0);
num[strlen(num)-1]='\0';
MYSQL_RES *result;
MYSQL_ROW row;
mysql_query(&mysql,"select num,name from users");
result=mysql_store_result(&mysql);
while((row=mysql_fetch_row(result))!=NULL)
{
if(!strcmp(num,row[0]))
{
ret=1;strcpy(name,row[1]);
send(fd,"1\n",2,0);
break;
}
}
mysql_free_result(result);
if(!ret) send(fd,"0\n",2,0);
else{
recv(fd,passwd,sizeof(passwd),0);
passwd[strlen(passwd)-1]='\0';
sprintf(i_query,"select decode(passwd,'cry') from users where num='%s'",num);
mysql_query(&mysql,i_query);
result=mysql_store_result(&mysql);
row=mysql_fetch_row(result);
if(!strcmp(passwd,row[0]))
{
send(fd,"1\n",2,0);
user[sum].cfd=fd;
user[sum].stat=-2;
strcpy(user[sum].num,num);
strcpy(user[sum].name,name);
printf("ADD ONE USER(%s)\n",num);
printf("NOW THE NUMBER OF ONLINE USER(S):%d\n",sum+1);
sum++;
}
else send(fd,"0\n",2,0);
}
}
if(!strcmp(bag,"/:regis\n"))
{
printf("this client will regis a number!!\n");
char num[11]={'\0'},name[30]={'\0'},sex[4]={'\0'},age[4]={'\0'},sig[60]={'\0'},passwd[20]={'\0'};
recv(fd,passwd,sizeof(passwd),0);passwd[strlen(passwd)-1]='\0';
recv(fd,name,sizeof(name),0);name[strlen(name)-1]='\0';
recv(fd,sex,sizeof(sex),0);sex[strlen(sex)-1]='\0';
recv(fd,age,sizeof(age),0);age[strlen(age)-1]='\0';
recv(fd,sig,sizeof(sig),0);sig[strlen(sig)-1]='\0';
char i_query[200];
sprintf(i_query,"insert into users (passwd,name,sex,age,signature) values (encode('%s','cry'),'%s','%s',%d,'%s')",passwd,name,sex,atoi(age),sig);
if(mysql_query(&mysql,i_query)!=0) send(fd,"regis failed!!\n",15,0);
sprintf(i_query,"select num from users where name='%s'",name);
mysql_query(&mysql,i_query);
MYSQL_RES *result;
MYSQL_ROW row;
result=mysql_store_result(&mysql);
while((row=mysql_fetch_row(result))!=NULL)
strcpy(num,row[0]);
send(fd,num,strlen(num),0);
mysql_free_result(result);
}
if(!strcmp(bag,"/:search\n"))
{
int ret=0;
char num[11]={'\0'},inc[200]={'\0'};
mysql_query(&mysql,"select * from users");
MYSQL_RES *result;
MYSQL_ROW row;
result=mysql_store_result(&mysql);
recv(fd,num,sizeof(num),0);
num[strlen(num)-1]='\0';
while((row=mysql_fetch_row(result))!=NULL)
{
if(!strcmp(num,row[0]))
{
for(int j=2;j<=5;j++)
{
strcat(inc,row[j]);
strcat(inc,"#");
}
send(fd,"search successfully!\n",21,0);
send(fd,inc,strlen(inc),0);
ret=1;break;
}
}
if(!ret)
{
send(fd,"search failed!!\n",16,0);
}
mysql_free_result(result);
}
}
接收数据包,在其中包含了群聊私聊的切换以及退出命令的处理
void *recv_bag(void *ptr) //接收消息包
{
char bag[MAX_BAG];
struct timeval time={0,0}; //设置select为非阻塞
while(1) //持续监控各套接口的消息
{
int max_fd=user[0].cfd;
FD_ZERO(&rfds);
for(int i=0;i<sum;i++)
{
FD_SET(user[i].cfd,&rfds);
if(user[i].cfd>max_fd)
max_fd=user[i].cfd;
}
switch(select(max_fd+1,&rfds,NULL,NULL,&time))
{
case -1:printf("select error!!\n");exit(-1);break;
case 0:break;
default:
for(int i=0;i<sum;i++)
{
if(FD_ISSET(user[i].cfd,&rfds))
{
memset(bag,0,sizeof(bag));
if(read(user[i].cfd,bag,1024)<=0) //读取出错,删除该接口,sum减1
{
printf("\nrecv bag from %d failed!\n",user[i].cfd);
close(user[i].cfd);
printf("cfd %d exit!!\n",user[i].cfd);
printf("REDUCE ONE USER\n");
printf("NOW THE NUMBER OF ONLINE USER(S):%d\n",sum-1);
for(int j=i+1;j<sum;j++)
user[j-1]=user[j];
sum--;i--;continue;
}
printf("\nrecv bag from %d\n",user[i].cfd); //记录接收信息的端口
printf("%s",bag);
if(!strcmp(bag,"/:pchat\n")) //选择私聊模式
{
send(user[i].cfd,"/:clear\n",8,0); //向客户端发送清屏要求
send(user[i].cfd,"you choose the private chat\n",28,0);
send(user[i].cfd,"please input the other side's num:\n",35,0);
char who[11]={'\0'};
recv(user[i].cfd,who,sizeof(who),0);
who[strlen(who)-1]='\0';
int j;
for(j=0;j<sum;j++)
{
if(!strcmp(who,user[j].num))
{
user[i].stat=j;
break;
}
}
if(j>=sum) user[i].stat=0;
}
else if(!strcmp(bag,"/:gchat\n")) //切换为群聊模式
{
user[i].stat=-2;
if(sum>1)
{
SendToOther(i,user[i].name);
SendToOther(i,"(");
SendToOther(i,user[i].num);
SendToOther(i," join the group chat!\n");
}
send(user[i].cfd,"/:clear\n",8,0);
send(user[i].cfd,"you choose the group chat\n",26,0);
}
else if(!strcmp(bag,"/:exit!\n")) //客户退出,删除端口,sum-1
{
close(user[i].cfd);
printf("cfd %d exit!!\n",user[i].cfd);
printf("REDUCE ONE USER\n");
printf("NOW THE NUMBER OF ONLINE USER(S):%d\n",sum-1);
for(int j=i+1;j<sum;j++)
user[j-1]=user[j];
sum--;i--;
}
else if((!strcmp(bag,"/:search\n"))||(!strcmp(bag,"/:login\n"))||(!strcmp(bag,"/:regis\n")))
{
mode_cmd(user[i].cfd,bag);
}
else if(user[i].stat==-2) //-2为群聊模式
{
printf("send message to other\n");
SendToOther(i,user[i].name);
SendToOther(i,"(");
SendToOther(i,user[i].num);
SendToOther(i,":");
SendToOther(i,bag);
}
else if(user[i].stat>=-1) //私聊模式,-1为对方不在线或不存在状态,>0即为在线用户
{
SendToClient(i,user[i].stat,bag);
}
}
}
}
}
}
向其他客户端发送消息和向指定客户端发送消息
void SendToOther(int i,char *mess) //给其他端口发送信息
{
for(int j=0;j<sum;j++)
{
if(i!=j&&user[j].stat==-2){
send(user[j].cfd,mess,strlen(mess),0);
}
}
}
void SendToClient(int P_fr,int P_to,char *mess) //给指定端口发送消息
{
if(P_to==-1) //若为-1,发送不在线通知
{
send(user[P_fr].cfd,"the other side isn't on line\n",29,0);
}else
{
printf("%s send message to %s\n",user[P_fr].num,user[P_to].num);
send(user[P_to].cfd,user[P_fr].num,strlen(user[P_fr].num),0);
send(user[P_to].cfd,":",1,0);
send(user[P_to].cfd,mess,strlen(mess),0);
}
}
开启监听前的准备工作,与(一)相同
void pre_connect(int argc,char *argv[],int *listenfd)
{
if(argc<=2)
{
printf("%s ip_address port_number!\n",argv[0]);
exit(1);
}
int ret;
int port=atoi(argv[2]);
char *ip=argv[1];
struct sockaddr_in server;
memset(&server,0,sizeof(struct sockaddr_in));
server.sin_family=AF_INET;
server.sin_port=htons(port);
inet_pton(AF_INET,ip,&server.sin_addr);
if((*listenfd=socket(AF_INET,SOCK_STREAM,0))==-1)
{
printf("socked error\n");
exit(1);
}
int reuse=1; //取消TIME WAIT
setsockopt(*listenfd,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse));
if((ret=bind(*listenfd,(struct sockaddr*)&server,sizeof(struct sockaddr)))==-1)
{
printf("bind error!!\n");
exit(1);
}
if((ret=listen(*listenfd,5))==-1)
{
printf("listen error!!\n");
exit(1);
}
printf("waiting for client!!\n");
}
监听有连接请求的客户端并将登录注册放在一个单独的线程中处理
void *cho_c(void *cho) //由于登录和注册是阻塞式的,若写在主线程中会导致其他客户端无法正常运行,因此单独写一个线程
{
struct pro *c=(struct pro*)cho;
recv(c->fd,c->bag,sizeof(c->bag),0);
mode_cmd(c->fd,c->bag);
return NULL;
}
void listen_c(int *listenfd)
{
struct sockaddr_in client;
socklen_t sin_size=sizeof(struct sockaddr_in);
while(1) //监听是否有用户请求连接
{
int fd=accept(*listenfd,(struct sockaddr *)&client,&sin_size);
if(sum>=USR_LIMIT) //数量超限
{
printf("max number of clients reached!!\n");
send(fd,"max number of clients reached!!\n",32,0);
close(fd);
}
else if(fd>0)
{
char bag[10];
send(fd,"\n",1,0);
pthread_t tid;
struct pro cho;
cho.fd=fd;strcpy(cho.bag,bag);
pthread_create(&tid,NULL,cho_c,&cho);
}
}
}
主函数,以及忽略SIGPIPE信号的函数
void hander()
{
fprintf(stderr, "BROKEN PIPE!!\n");
}
int main(int argc,char *argv[])
{
int listenfd;
signal(SIGPIPE,hander); //处理SIGPIPE信号
pre_connect(argc,argv,&listenfd);
pthread_t tid;
pthread_create(&tid,NULL,recv_bag,NULL);
listen_c(&listenfd);
close(listenfd);
pthread_exit(&tid);
}