程序:利用FIFO实现本地聊天室
程序功能:实现本地进程间的相互通信
程序分析:通过FIFO管道可以实现本地(本机)上无血缘关系(进程不是fork()出来的)的进程间的通讯,而FIFO管道支持“一写多读”,“多写一读”。我们可以利用这一特点建立本地聊天室的模型。
服务器提供一条公共的管道,各个客户端通过这条公共管道发送消息给服务器,服务器接收到消息后,对消息包进行解析后交给相应的函数处理。如果是一个客户端发给另一个客户端的消息,就将消息通过私有管道转发下去,交给对应的客户端。
程序种使用的知识点:
(1)临时文件(unlink):因为通信时需要给每个客户端都创建一条私有的通道,如果使用普通的方式创建文件,就会产生大量的管道文件。不过如果使用数据库,就可以创建普通的文件,因为管道时根据客户端的客户名创建的,如果每次输入的名字的不同就产生不同的文件,使用数据库,可以通过读入客户机的文件名来解决这个问题。
(2)修该文件属性(fcntl):无论是客户端还是服务器都要求在可以发送和接收消息,这就要求文件时非阻塞的。使得文件获得非阻塞的属性的方法有两种:(1)在文件打开时将文件将文件修改为非阻塞的(私有管道)。(2)在文件打开后通过fcntl函数将文件的属性修改为非阻塞(STDIN_FILENO(会在程序运行时,默认打开))。
(3)字符串处理:在客户端相互发送消息时,需要输入接收方的客户机名称,规定格式为[接收方姓名:消息内容]。客户会输入一串字符串,需要进程处理,将字符串以第一个“:”为分割符,分割为两部分。如果没有按格式输入则会将消息群发(发给客户端列表上的每一个客户端)。
(4)深拷贝:在给字符串进程赋值一定要注意,使用strcpy函数,而不是简单的使用“=”。
(5)范型指针:将消息结构体转换为“范型指针”(void*),然后是使用char*接收(也可以直接使用viod*接收)。
程序中存在的问题:
(1)在客户端的管理上,使用了数组。数组虽然使用比较简单,但后期会有很多的麻烦,如空间浪费,客户端退出的处理不够灵活。
(2)在客户端登录时,没有对数据进程越界判断。
(3)没有对客户端的输入的客户名进行检查(会造成消息混乱)。
(4)数据包使用了定长包,处理上比较简单,但造成了浪费。
(5)客户端没有处理服务器退出的机制(虽然函数会自动关闭,但显得代码不是很完善)。
运行环境:CentOS7.0+Gcc
注意事项:公共管道使用mkfifo在控制台下直接建立的,然后使用了宏定义和绝对路径。
#define SERVER_FIFO "/home/kk/code/c++/localChatRoom/SERVER_FIFO"
默认私有管道的名称为客户机名称,创建在当前目录下,零食文件,使用后会被自动删除。
客户端和服务器的退出命令都为:quit-->()
客户端退出时,会关闭私用管道和公共管道(它自己的那一条的写端)
服务器退出时,会给每一个在线的客户端的发送消息,然后关闭私有的管道的写端和公共管道的读端。
在输入时不能输入空格,因为scanf函数遇空格会终止,会将消息从空格处截断,后面的消息会变成群发。可以利用sacnf的格式控制来解决。也可以使用gets_s函数(需要将文件改为Cpp),不推荐使用gets函数(没有错误检测)。
代码:
//localServer.h
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#define SERVER_FIFO "/home/kk/code/c++/localChatRoom/SERVER_FIFO"
struct client
{
char clientName[20];//客户端名字
int fifoDis;//私有管道的描述符
};
typedef struct client CL;
//用来记录客户机的数量
int clientlen=0;
//利用数组将存储客户队列(不方便,而且会浪费),可以改造为链表(最好)。
CL clientDueue[100];
struct messagePacket
{
int messageNo;//消息编号
char senderName[20];//消息发送方
char receiverName[20];//消息接收方
char data[1024];//数据采用不定长消息
};
typedef struct messagePacket MSP;
//公共管道
int serFifo;
//服务器启动标志
int startFlags=0;
//初始化,负责初始化服务器。
void initServer();
//负责接收客户端发送的包
void receiverPacket();
//负责将客户端发送的包解析
void parsingPacket(MSP *msp);
//负责客户端登陆,将客户端插入客户队列中,并创建私有管道
void clientLogin(char* loginName);
//负责将消息发送到对应的接受方
void messageSend(MSP *pMsp);
//负责客户端的退出,将客户端从客户队列中删除,并删除创建的管道
void clientQuit(char* quitName);
//负责关闭服务器,关闭打开的管道和删除客户机列表
void closeServer();
//负责处理输入的数据
void messageHanle(char* pMes);
//localServer.c
#include "localServer.h"
#define BUFSIZE 1068
void initServer()
{
//将STDIN_FILENO修改为非阻塞
int serFlags=fcntl(STDIN_FILENO,F_GETFL);
serFlags|=O_NONBLOCK;
fcntl(STDIN_FILENO,F_SETFL,serFlags);
//以非阻塞只读的方式打开管道
serFifo=open(SERVER_FIFO,O_RDONLY|O_NONBLOCK);
if(serFifo<0)
{
perror("SERVER OPEN:");
exit(1);
}
printf("服务器已启动,正在监听...\n");
startFlags=1-startFlags;
}
void receiverPacket()
{
char buf[BUFSIZE];
MSP *msp;
int len=read(serFifo,buf,sizeof(buf));
if(len>0)
{
msp=(MSP*)buf;
parsingPacket(msp);
}
}
void parsingPacket(MSP *msp)
{
//根据相应的功能号,调用相应的函数。
switch(msp->messageNo)
{
case 0:
clientLogin(msp->senderName);
break;
case 1:
messageSend(msp);
break;
case 2:
clientQuit(msp->senderName);
break;
}
}
void clientLogin(char* loginName)
{
//不能直接赋值,会造成浅拷贝
strcpy(clientDueue[clientlen].clientName,loginName);
char path[23]="./";
strcat(path,loginName);
//确保创建的文件的权限为分配权限
umask(0);
//创建管道
mkfifo(path,0777);
//将管道的文件描述符存入数组中
clientDueue[clientlen].fifoDis=open(path,O_WRONLY);
char buf[]="您和服务器的连接已经成功建立,可以开始通讯了\n";
write(clientDueue[clientlen].fifoDis,buf,sizeof(buf));
//这里应该将管道创建为临时的,如果是使用数据库,可以创建为永久的
unlink(path);
//没有对cientlen进行限制
++clientlen;
}
void clientQuit(char* quitName)
{
//最好是利用链表管理登录的客户机
int i=0;
for(i=0;i<clientlen;i++)
{
if(strcmp(quitName,clientDueue[i].clientName)==0)
{
//关闭对应的私有通过
close(clientDueue[i].fifoDis);
clientDueue[i].fifoDis=-1;
clientDueue[i].clientName[0]='\0';
break;
}
}
printf("%s已退出\n",quitName);
}
void messageSend(MSP *pMes)
{
int i=0;
char* buf=(void*)pMes;
if(strlen(pMes->receiverName)!=0)
{
//单发
for(i=0;i<clientlen;++i)
{
if(strcmp(pMes->receiverName,clientDueue[i].clientName)==0)
{
write(clientDueue[i].fifoDis,buf,BUFSIZE);
break;
}
}
}
else
{
//群发
for(i=0;i<clientlen;++i)
{
write(clientDueue[i].fifoDis,buf,BUFSIZE);
}
}
}
void messageHanle(char* pMes)
{
if(strcmp(pMes,"quit-->()")==0)
{
closeServer();
}
//可以继续增加一些命令(显示有几个客户端,客户端的管道描述符等)
}
void closeServer()
{
char buf[]="服务器维护中,请稍后登录。";
int i=0;
for(i=0;i<clientlen;++i)
{
if(clientDueue[i].fifoDis!=-1)
{
write(clientDueue[i].fifoDis,buf,strlen(buf));
close(clientDueue[i].fifoDis);
}
}
close(serFifo);
startFlags=1-startFlags;
printf("以关闭所有管道,服务器安全退出");
}
int main()
{
initServer();
char mes[1024];
while(startFlags)
{
receiverPacket();
if(scanf("%s",mes)!=EOF)
{
messageHanle(mes);
}
}
return 0;
}
//localClient.h
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#define SERVER_FIFO "/home/kk/code/c++/localChatRoom/SERVER_FIFO"
int linkFlags=0;//连接标志
int serFifo;//公共管道文件描述符
int cliFifo;//客户端私有端道文件描述符
char clientName[20];//客户端名称
struct messagePacket
{
int messageNo;//消息编号
char senderName[20];//消息发送方
char receiverName[20];//消息接收方
char data[1024];//数据采用定长消息
};
typedef struct messagePacket MSP;
//初始化客户大端
void initClient();
//登陆服务器
void loginServer();
//处理用户输入的数据
void messageHanle(char* pMes);
//向服务器发送消息
void sendSerMes(int mesNO);
//向其他用户发送消息
void sendMessage(char* receiverName,char* data);
//接收消息
void receiverMes();
//关闭客户端
void closeClient();
//localClient.c
#include "localClient.h"
#define BUFSIZE 1068
void initClient()
{
loginServer();
//将连接标志置为1.
linkFlags=1-linkFlags;
//将STDIN文件属性修改为非阻塞
int flags=fcntl(STDIN_FILENO,F_GETFL);
flags |= O_NONBLOCK;
fcntl(STDIN_FILENO,F_SETFL,flags);
}
void loginServer()
{
printf("请输入客户端名称(不超过20个字符):\n");
//write(STDIN_FILENO,clientName,20);
scanf("%s",clientName);
serFifo=open(SERVER_FIFO,O_WRONLY|O_NONBLOCK);
if(serFifo<0)
{
perror("open server fifo");
exit(1);
}
sendSerMes(0);
char path[23]="./";
strcat(path,clientName);
//测试管道是否创建成功
while(access(path,F_OK)!=0);
cliFifo=open(path,O_RDONLY|O_NONBLOCK);
if(cliFifo<0)
{
perror("open client fifo");
}
printf("私有管道创建成功\n");
}
void sendSerMes(int mesNO)
{
MSP msp;
char *buf;
msp.messageNo=mesNO;
strcpy(msp.senderName,clientName);
buf=(void*)&msp;
write(serFifo,buf,sizeof(msp));
}
void messageHanle(char* pMes)
{
//将“quit-->()”设置为退出消息
if(strcmp(pMes,"quit-->()")==0)
{
sendSerMes(2);
closeClient();
return;
}
//发送数据格式为:接受者姓名:消息内容
//如果数据不符合规范,则将消息转为群发。
int i=0;
int j=0;
char receiverName[20];
char data[1024];
while(pMes[i]!='\0'&&pMes[i]!=':')
{
receiverName[i]=pMes[i];
++i;
}
receiverName[i]='\0';
if(pMes[i]==':')
{
//将:跳过
++i;
}
else
{
i=0;
receiverName[0]='\0';
}
while(pMes[i]!='\0')
{
data[j++]=pMes[i++];
}
data[j]='\0';
sendMessage(receiverName,data);
}
void sendMessage(char* receiverName,char* data)
{
MSP msp;
char *buf;
msp.messageNo=1;
strcpy(msp.senderName,clientName);
strcpy(msp.receiverName,receiverName);
strcpy(msp.data,data);
buf=(void*)&msp;
write(serFifo,buf,sizeof(msp));
}
void receiverMes()
{
char buf[BUFSIZE];
int len=read(cliFifo,buf,sizeof(MSP));
MSP *pMes=NULL;
pMes=(void*)buf;
if(len>0&&pMes->messageNo==1)
{
printf("%s:%s\n",pMes->senderName,pMes->data);
}
else if(len>0)
{
printf("系统提示:%s",buf);
}
}
void closeClient()
{
//将连接标志置为0
linkFlags=1-linkFlags;
//关闭私有管道
close(cliFifo);
//关闭公共管道
close(serFifo);
printf("以关闭所以管道,客户端安全退出\n");
}
int main()
{
initClient();
char mesBuf[1024];
while(linkFlags)
{
//scanf()默认遇空格终止scanf("%49[^\n]",mesBuf)!=EOF
//int len=write(STDIN_FILENO,mesBuf,BUFSIZE);
if(scanf("%s",mesBuf)!=EOF)
{
messageHanle(mesBuf);
}
receiverMes();
}
return 0;
}
结果展示:
图1、服务器端
图2、客户端1
图3、客户端2
图4、客户端3
如果发现问题,请留言,我会查证后修改,万分感谢。