多人聊天室算是socket网络编程中比较基础的一个功能了,它主要由服务器和客户端两部分组成。其中客户端比较容易实现,只需要完成发送和接收消息的功能,而服务器则需要对每个客户端发送的数据进行分析,判断出消息的类型,从而决定是保存,删除还是转发。下面进行具体的说明:(基于TCP协议)
预编译和全局变量的声明:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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;imax_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
该部分同样是创建了一个监听的线程,并且使用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
由于代码是分块粘贴的,部分的变量声明未粘贴过来
问题:由于客户端的监听消息线程一直在运行,而用户消息的输入也是在终端,因此会导致在输入过程中接收到的消息会直接打印出来,很不美观。