Linux多人聊天(一)

多人聊天室算是socket网络编程中比较基础的一个功能了,它主要由服务器和客户端两部分组成。其中客户端比较容易实现,只需要完成发送和接收消息的功能,而服务器则需要对每个客户端发送的数据进行分析,判断出消息的类型,从而决定是保存,删除还是转发。下面进行具体的说明:(基于TCP协议)

客户端:

预编译和全局变量的声明:

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

#define MAX_name 50      
#define MAX_mess 1024 

int fd; 
char recvs[MAX_mess];

创建和连接套接口:

char pre_name[55]={"NAME_C*"}; //姓名前缀,标识用户信息
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信息
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);
}
printf("please input your name:");
fgets(name,sizeof(name),stdin);
strcat(pre_name,name);
send(fd,pre_name,strlen(pre_name),0);

这一部分算是连接前的准备部分了,首先在打开客户端时应该指定服务器的ip地址与端口号,并将其存入struct sockaddr_in结构体中,接着申请一个socket,将之与服务器相连(客户端的socket会自动bind()),连接成功后输入一个用户名作为在聊天室中的一个标识,而NAME_C*前缀则是为了方便服务器区分消息的类型。

发送消息:

while(1)
{
	char pre_mess[1030]={"MESS_C*"};  //消息内容前缀
	memset(sends,'\0',sizeof(sends));
	fgets(sends,sizeof(sends),stdin);
	if(!strcmp(sends,"exit!\n"))
	{
		send(fd,sends,strlen(sends),0);
		printf("you choose to exit!!\n");
		close(fd);
		exit(0);
         }
	strcat(pre_mess,sends);
	send(fd,pre_mess,strlen(pre_mess),0);
}

这一部分比较简单,构建一个死循环,不断地接收标准输入中输入的消息并加上前缀标识后发送给服务器,仅当输入exit!时可以退出程序。

接收消息:

pthread_t tid;
pthread_create(&tid,NULL,pthread_recv,NULL);
void *pthread_recv(void* ptr)  //监听是否有消息传入
{
    while(1)
    {
    	int ret;
    	memset(recvs,0,sizeof(recvs));
    	if((ret=recv(fd,recvs,sizeof(recvs)-1,0))>0)
    	{
    			printf("%s",recvs);
    	 }
      else{  
        exit(1); 
       }
    }
}

该部分创建了一个线程,不断的监控服务器传来的消息并直接打印出来。

发送与接收消息这两部分也可以在单线程中使用select来监控,即监控标准输入是否可读以及与服务器连接的套接口是否可读。

服务器端:

全局变量和函数的声明:

#include <stdio.h>
#include <netdb.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 5   //最大用户数量
#define MAX_BAG 1030  //数据包最大长度
#define MAX_MESS 1025 //消息最大长度

struct client   //存储客户端套接字和用户姓名
{
	int cfd;
	char cname[50];
}usr[USR_LIMIT];
int sum=0;   //当前用户总数
fd_set rfds;  //select判断可读事件

void *recv_mess();  //监控并接收各客户端发送的消息
void SendToClient();  //给除当前用户的其他用户转发消息
char JudgeType();  //判断消息类型

开启监听前的准备:

if(argc<=2)
{
	printf("%s ip_address port_number!\n",argv[0]);
	exit(1);
}
		
int listenfd,ret;
int port=atoi(argv[2]);
char *ip=argv[1];
struct sockaddr_in server;
struct sockaddr_in client;
socklen_t sin_size=sizeof(struct sockaddr_in);
	
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");

和客户端一样,在输入时需要加上主机的ip和准备使用的端口号(与客户端一致),接着便是申请套接口,绑定套接口,然后便是开启监听。由于TCP协议会在进程结束后开启一种TIME WAIT的状态,导致端口号在短时间内无法再次使用,在调试时造成了麻烦,可以使用setsockopt函数对其属性进行设置,取消TIME WAIT状态,之后便是等待客户端的连接了。

监听连接:

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)
	{
		struct sockaddr_in peerAddr;
		unsigned int peerLen;char ipAddr[20];
		getpeername(fd,(struct sockaddr *)&peerAddr, &peerLen);
	        usr[sum].cfd=fd;
		printf("ADD ONE USER\n");
		printf("peeraddr is %s\n", inet_ntop(AF_INET, &peerAddr.sin_addr, ipAddr, sizeof(ipAddr)));
	        printf("NOW THE NUMBER OF ONLINE USER(S):%d\n",sum+1);
						sum++;
	}
}	
	 

持续监听是否有客户端请求连接,若数目超过限制对客户端发出提示西你想后便关闭该套接口,若连接成功,服务器端显示增加新用户并显示当前在线人数。

监听并处理消息:

pthread_t tid;
pthread_create(&tid,NULL,recv_mess,NULL);
void *recv_mess(void *ptr)  //接收并处理数据包,提取信息
{
   char bag[MAX_BAG];
   char mess[MAX_MESS];
   struct timeval time={0,0}; //设置select为非阻塞
   while(1) //持续监控各套接口的消息
   {
      int max_fd=usr[0].cfd;
      FD_ZERO(&rfds);
      for(int i=0;i<sum;i++)
      {
	FD_SET(usr[i].cfd,&rfds);
        if(usr[i].cfd>max_fd)
        max_fd=usr[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(usr[i].cfd,&rfds))
		{
		   memset(mess,0,sizeof(mess));
		   memset(bag,0,sizeof(bag));
		   if(read(usr[i].cfd,bag,1029)<=0)  //读取出错,删除该接口,sum减1
		   {
		      printf("\nrecv bag from %d failed!\n",usr[i].cfd);
		      close(usr[i].cfd);
		      printf("cfd  %d  exit!!\n",usr[i].cfd);
		      printf("REDUCE ONE USER\n");
		      printf("NOW THE NUMBER OF ONLINE USER(S):%d\n",sum-1);
		      SendToClient(i,usr[i].cname);
		      SendToClient(i," exit the chat!!\n");
		      for(int j=i+1;j<sum;j++)
		      {usr[j-1].cfd=usr[j].cfd;strcpy(usr[j-1].cname,usr[j].cname);}
		      sum--;i--;continue;
		    }
		printf("\nrecv bag from %d\n",usr[i].cfd); //记录接收信息的端口
		printf("%s",bag); 
                switch(JudgeType(bag,mess))  //解析数据包,提取信息类型
		{
		   case 'n'://用户姓名,保存,通知
		   { 
		      int k;
		      mess[strlen(mess)-1]='\0';
		      for(k=0;k<(sum-1);k++)
               	      {		   
                         if(!strcmp(mess,usr[k].cname))   //姓名重复,关闭端口
		         {
		            send(usr[i].cfd,"this name is used!!please change one!!\n",39,0);
			    printf("cfd  %d  exit!!\n",usr[i].cfd);
			    printf("REDUCE ONE USER\n");
		            printf("NOW THE NUMBER OF ONLINE USER(S):%d\n",sum-1);
			    close(usr[i].cfd);
			    break;
			  }
		       }if(k<(sum-1)) {sum--;i--;break;}
  	   	    send(usr[i].cfd,"welcome to the chat room!!\n",27,0);
		    strcpy(usr[i].cname,mess);
		    SendToClient(i,usr[i].cname);	
		    SendToClient(i," join the chat!!\n");	
		    send(usr[i].cfd,"online user(s):\n",16,0);
		    if(sum==1) send(usr[i].cfd,"no one online now!!\n",20,0);
		    else
                      for(int j=0;j<sum;j++)
		      {
			  if(j!=i)
			  {
			    send(usr[i].cfd,usr[j].cname,strlen(usr[j].cname),0);
		   	    send(usr[i].cfd,"\n",1,0);
			   }
		       }
		       break;
		    }
		   case 'm'://聊天内容,转发 
		   {
			SendToClient(i,usr[i].cname);
			SendToClient(i,":");		
			SendToClient(i,mess);	
		        break;
		   }
		   case 'e'://退出,关闭cfd,删除用户信息,通知
		   {
			close(usr[i].cfd);
			printf("cfd  %d  exit!!\n",usr[i].cfd);
			printf("REDUCE ONE USER\n");
			printf("NOW THE NUMBER OF ONLINE USER(S):%d\n",sum-1);
			SendToClient(i,usr[i].cname);
			SendToClient(i," exit the chat!!\n");
			for(int j=i+1;j<sum;j++)
			{usr[j-1].cfd=usr[j].cfd;strcpy(usr[j-1].cname,usr[j].cname);}
			sum--;i--;
			break;
		    }
		}
	    }	
	}
     }
   }
}

char JudgeType(char *bag,char *mess)  //解析信息类型
{
	if(!strcmp(bag,"exit!\n")) return 'e';
	strcpy(mess,bag+7);
	*(bag+7)='\0';
	if(!strcmp(bag,"NAME_C*")) return 'n';
	else if(!strcmp(bag,"MESS_C*")) return 'm';
	else return ' ';
}

void SendToClient(int i,char *mess)  //给其他端口发送信息
{
    for(int j=0;j<sum;j++)
    {
	if(i!=j){
		printf("send message to cfd  %d\n",usr[j].cfd);
		send(usr[j].cfd,mess,strlen(mess),0);
	}
     }
}

该部分同样是创建了一个监听的线程,并且使用select对各个客户端口进行了可读事件的监听,一旦收到某个客户端的消息,先通过其前缀判断消息的类型,对于姓名则将其与相应的client fd保存起来,对于已使用过的姓名给该端口发送提示消息之后将该端口关闭;对于消息,则将其直接转发给其他的在线用户;对于退出(exit!)的消息则关闭该套接口并将姓名从数组中去除,同时使用户数量减一。

由于客户端异常关闭时,若服务器依旧向该端口发送信息会产生一个SIGPIPE信号,而该信号会导致服务器进程的关闭,因此我们需要忽略掉这个信号:

void hander()
{
	fprintf(stderr, "BROKEN PIPE!!\n");
}
signal(SIGPIPE,hander); //处理SIGPIPE信号

注:程序中使用了pthread_create()函数,因此在编译时应该加上-pthread参数,如gcc -Wall -o chat chat.c -pthread

        由于代码是分块粘贴的,部分的变量声明未粘贴过来

问题:由于客户端的监听消息线程一直在运行,而用户消息的输入也是在终端,因此会导致在输入过程中接收到的消息会直接打印出来,很不美观。

  • 7
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值