Linux多人聊天(二)

在上次所写的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);			
}

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值