//client.h
#ifndef _CLIENT_H__
#define _CLIENT_H__
#include<stdio.h>
#include<stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <errno.h>
#include <pthread.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#define MAXLEN 1024
struct message //信息结构
{
char flag[8]; //标识:"all","view","trans"...还可能是客户的名字
char name[12];
int size; //信息 msg 字段的字节长度
char msg[MAXLEN]; //信息的内容
};
struct msg //系统中用于信息交互的 消息队列
{
long type; //信息类型,要大于 0,小于 0 的 内核有特殊用途。一般用 进程号 表示,
//因为 消息队列 多用于不同进程间的通信,用 进程号 标识先应的接口,用于发收信息。
char text[4]; //本程序中,并不把数据放到 消息队列,而是放到 chitchat(客户聊天记录) 文件中
//用 消息队列,只是为了通过 其 发收 机制及时显示所接受到的信息,因为本程序的 输入
//与 显示 是分开的,即是用不同的进程,所以需要 消息队列。如果在 同一个 窗口下 则不需要
};
void HandleQuit(int signal); //客户退出
void * HandleRecvmsg(int *sockfd); //客户接受信息线程的处理操作
void * HandleSendfile(char *filename); //客户发送文件线程的处理操作
void * HandleRecvfile(struct message *msg); //客户接受文件线程的处理操作
int qid; //整数描述符,用其来指代将要的 消息队列,并初始化
int fd; // chitchat 文件的 文件描述符
int sockfd; //链接到服务器的套接口
int savefilefd; // 发送方:-1 是初始化,还没打开文件,大于 0,打开了的文件的 文件描述符
// 接收方:0 是接受方收到询问是否接受时,所设定的
char filetoname[12]; //用于文件传输,接收方的名字
char filename[12]; //发送方将要发送的 文件名
// chitchat 文件:客户端运行时会自动生成已保存客户本次的通话记录
#endif
// client_main
#include "client.h"
#define SERVER_PORT 9877
/*
extern int qid; //整数描述符,用其来指代将要的 消息队列,并初始化
extern int fd; // chitchat 文件的 文件描述符
extern int savefilefd; // 发送方:-1 是初始化,还没打开文件,大于 0,打开了的文件的 文件描述符
*/
int main(int argc,char *argv[])
{
struct sockaddr_in serveraddr; //服务器地址结构
struct message sendmsg; //将要发送的信息
struct message recvmsg; //服务器的回应信息
int choose; //操作选择
char buf[MAXLEN];
pthread_t pid; //接受线程id
char flag[8]; //发送信息的标识,all,view...
char name[12]; //发送者名字
qid = -1;
fd = -1;
savefilefd = -1;
// memset(&recvmsg,0,sizeof(structmessage));
// memset(&sendmsg,0,sizeof(structmessage));
if(argc < 2)
{
printf("请输入服务器的地址 (./out [ip地址])");
exit(1);
}
if((sockfd =socket(AF_INET,SOCK_STREAM,0)) < 0)
{
perror("socket error");
exit(1);
}
bzero(&serveraddr,sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr =inet_addr(argv[1]); //把 服务器地址赋值给 服务器地址结构
serveraddr.sin_port = htons(SERVER_PORT);
if(connect(sockfd,(structsockaddr*)&serveraddr,sizeof(struct sockaddr)) < 0)
{//与服务器链接
perror("connect error");
exit(1);
}
signal(SIGINT,HandleQuit); //退出是,启动 HandleQuit函数
//问题:该 信号 函数应放在那个位置
printf("----------------------------------\n");
printf("| |\n");
printf("| input a number to work |\n");
printf("|\t1.login\t\t\t|\n");
printf("|\t2.register\t\t|\n");
printf("|\t3.exit\t\t\t|\n");
printf("| |\n");
printf("----------------------------------\n");
printf("请输入你的选择:");
scanf("%d",&choose);
while(choose != 1 && choose !=2 && choose != 3)
{//输入不正确
printf("你输入的不是上面的选项,请重新输入:\n");
scanf("%d",&choose);
}
int n = 3; //有 3 次登录机会
char password[20];
char password_sure[20]; //密码确认
int done = -1; //输入密码正确
switch(choose)
{
case 1://登录操作
while(n)
{
printf("请输入你的帐号名字: ");
scanf("%s",name);
strcpy(sendmsg.name,name);
printf("请输入你的密码:");
scanf("%s",sendmsg.msg);
strcpy(sendmsg.flag,"login");
send(sockfd,(structmessage *)&sendmsg,sizeof(struct message),0);
printf("等待服务器链接...\n");
recv(sockfd,(structmessage *)&recvmsg,sizeof(struct message),0);
if(strcmp(recvmsg.msg,"登录成功") != 0)
{//登录不成功
printf("登录不成功\n");
--n;
printf("你还有 %d 次登录机会\n",n);
continue; //结束本次循环,等待下一次登录
}
//登录成功后的操作
printf("登录成功");
n = 0; //客户已登录,退出后要重新打开程序方可登录
pthread_create(&pid,NULL,(void*)HandleRecvmsg,(void*)&sockfd); //创建接受信息线程
while(1)
{
printf("开始发送信息...\n");
printf("请输入标识(flag):"); //私聊时,flag为 接受端的名字
scanf("%s",flag);
strcpy(sendmsg.flag,flag);
strcpy(sendmsg.name,name);
if(strcmp(sendmsg.flag,"all")== 0)
{//群发信息
printf("请输入你的内容:\n");
scanf("%s",buf);
strcpy(sendmsg.msg,buf);
send(sockfd,(structmessage *)&sendmsg,sizeof(struct message),0);
continue; //接受本次循环,可重新发送信息
}
elseif(strcmp(sendmsg.flag,"view") == 0)
{//查看在线客户
send(sockfd,(structmessage *)&sendmsg,sizeof(struct message),0);
continue; //接受本次循环,可重新发送信息
}
elseif(strcmp(sendmsg.flag,"trans") == 0)
{
if(savefilefd <0)
{//发送方
printf("请输入所要发送的文件:");
scanf("%s",filename);
strcpy(sendmsg.msg,filename);
printf("请输入文件的接收方: ");
scanf("%s",name);
strcpy(sendmsg.name,name);
send(sockfd,(structmessage*)&sendmsg,sizeof(struct message),0); //发送询问文件接收信息,agree |disagree
}
else
{//接收方 ,与 接受线程的 trans 的 (接受端)相呼应
scanf("%s",buf); //输入是否同意接受文件,agree|disagree
send(sockfd,(structmessage*)&sendmsg,sizeof(struct message),0); //发送回复信息给 发送端
}
//问题:如果几个客户同时传送文件给同一个客户,如何处理
continue;
}//end trans
else
{//私聊,因为 flag 与上面的都不符合,私聊时,flag为 接受端的名字
printf("请输入你的内容:\n");
scanf("%s",buf);
strcpy(sendmsg.msg,buf);
send(sockfd,(structmessage*)&sendmsg,sizeof(struct message),0);
}
}//end while(1)
}//end while(n)
break;
case 2://注册操作
printf("请输入你的 用户名:");
scanf("%s",name);
do
{//密码输入
printf("请输入你的密码:");
scanf("%s",password);
printf("请重新输入一次密码:");
scanf("%s",password_sure);
//问题:如何隐藏密码
if(strcmp(password,password_sure)== 0)
{
printf("密码输入正确");
done = 1;
}
else
{
printf("密码输入不正确,请重新输入");
done = -1;
}
}while(done < 0);
strcpy(sendmsg.flag,"reg");
strcpy(sendmsg.name,name);
strcpy(sendmsg.msg,password);
printf("向服务器发送注册请求...\n");
send(sockfd,(struct message*)&sendmsg,sizeof(struct message),0);
printf("等待服务器应答...\n");
recv(sockfd,(struct message*)&recvmsg,sizeof(struct message),0);
printf("%s\n",recvmsg.msg);
close(sockfd);
break;
case 3://退出
close(sockfd);
printf("程序已退出\n");
exit(0); //正确退出
break;
default:
printf("你的输入有误\n");
}
return 0;
}
/*
测试时遇到问题
问题 1:fread()的错误处理,即当 if((fread(...)==0))是,可能是 错误 或 EOF,用 if(feof(fd))则是EOF,否则不是
问题 2:struct message 的 msg字段 内容中如何处理有空格的语句???
预想解决方法:发送端还好,简单;但是 接收端麻烦: msg 内容都要扫描
*/
//client.c 功能函数的定义
#include "client.h"
/**************************
HandleQuit函数:客户端退出处理
其参数:intsignal:所接收到的信号
返回值:void
**************************/
void HandleQuit(int signal)
{
if(fd > 0)
{// chitchat 文件已打开
close(fd);
}
if(qid > 0)
{//创建了 消息队列
if(msgctl(qid,IPC_RMID,NULL) <0)
{
perror("删除消息队列失败");
close(savefilefd); //即使 消息队列 删除失败,也是要关闭 文件间的映射的,既要关闭文件描述符
exit(1);
}
}
close(savefilefd); //关闭 消息队列 成功,关闭 文件描述符
close(sockfd);
printf("程序正常退出...\n");
}
/**************************
HandleSendfile函数:客户传送文件的线程函数
其参数:charfilename[12]:发送方将要发送的 文件名
返回值:void*:(void*)0,(void*)1...0 表示正常退出,1表示出错,强制退出...
**************************/
void * HandleSendfile(char *filename)
{
char file[12]; //文件名
strcpy(file,filename);
struct message filedata; //该文件的相关信息
strcpy(filedata.flag,"transf"); //表示正在传送文件
strcpy(filedata.name,filetoname); //文件的接收方
ssize_t nread; //从被发送的文件中读到的数据字节数
int fd; //与被发送的文件相映射的 文件描述符
char buf[MAXLEN];
if((fd = open(file,O_RDONLY)) < 0)
{
perror("文件无法打开");
strcpy(filedata.msg,"end$");
send(sockfd,&filedata,sizeof(structmessage),0);
exit(1);
}
//问题:文件无法打开时,怎么办。直接发送 end$,结束该线程
do
{
nread = read(fd,buf,sizeof(buf));
if(nread < 0)
{//文件读取数据失败
perror("文件数据读取失败");
close(fd);
exit(1);
}
else if(nread == 0)
{
printf("文件读取完成\n");
strcpy(filedata.msg,"end$");
send(sockfd,&filedata,sizeof(structmessage),0);
}
else
{//有数据要传
filedata.size = nread; //数据的字节数
strcpy(filedata.msg,buf);
send(sockfd,&filedata,sizeof(structmessage),0);
}
}while(nread > 0); //有数据要传
close(fd); //关闭被传送的文件
savefilefd = -1; //有可以传文件了
return NULL;
}
/**************************
HandleRecvfile函数:接受端接收发送端传送文件的线程函数
其参数:structmessage *recvmsg:发送方发送过来的文件内容
返回值:void*:(void*)0,(void*)1...0 表示正常退出,1表示出错,强制退出...
**************************/
void * HandleRecvfile(struct message *msg)
{
struct message recvmsg = *msg; //把 接收线程 接收到的数据赋值到 接收文件线程 中
int nread; //将要写入文件的字节书数
if(strcmp(recvmsg.msg,"end$")== 0)
{//接受完发送端发送过来的文件
printf("接收文件完成\n");
close(savefilefd); //关闭刚接收完的文件
savefilefd = -1; //有可以接受或发送文件了
}
do
{//把数据写入文件中
nread =write(savefilefd,&recvmsg.msg,MAXLEN);
}while(nread > 0);
return NULL;
}
/**************************
HandleRecvmsg函数:客户端接受到服务器发来的消息
其参数:int*sockfd:链接套接口
返回值:void*:(void*)0,(void*)1...0 表示正常退出,1表示出错,强制退出...
**************************/
void * HandleRecvmsg(int *sockfd)
{
int connfd = *sockfd; //把 主线程 中的链接套接口 赋值到 接受信息线程的 套接口 中
int nread; //接受到的信息的字节数
char str[MAXLEN];
struct message recvmsg; //记录 接受到的信息
struct msg msg; //消息队列
time_t curtime; //表示当下时间
if((fd =open("chitchat",O_RDWR | O_CREAT | O_APPEND)) < 0)
{
perror("打开 聊天记录文件 失败");
exit(1);
}
if((qid = msgget(2222,IPC_CREAT |0666)) < 0)
{//显示的创建一个键值是为方便通讯
perror("创建 消息队列 不成功");
exit(1);
}
/*
在创建 消息队列 时,对于用户的权限不是很清楚,解决(下列图表)
数字值 符号值 说明
(八进制) 消息队列 信号量 共享内存区
0400 MSG_R SEM_R SHM_R 由用户(属主)读
0200 MSG_W SEM_A SHM_W 由用户(属主)写
0040 MSG_R >> 3 SEM_R >>3 SHM_R >> 3 由(属)组成员读
0020 MSG_W >> 3 SEM_A >>3 SHM_W >> 3 由(属)组成员写
0004 MSG_R >> 6 SEM_R >>6 SHM_R >> 6 由其他用户读
0002 MSG_W >> 6 SEM_A >>6 SHM_W >> 6 由其他用户写
记号 >> 3 的意思是将 值 右移 3 位
该图表上的 权限 都是用 二进制的 AND(|)运算的
*/
msg.type = getpid(); //得到当前 进程号,以使 消息队列 可分辨相关数据的处理对象
strcpy(msg.text,"ok"); //该数据作用不大,只是为了起到填充数据的作用
while(1)
{
nread = recv(connfd,(structmessage *)&recvmsg,sizeof(struct message),0);
if(nread < 0)
{//接受信息时出错
perror("接受信息失败");
close(fd);
exit(1);
}
else if(nread == 0)
{// IP 数据包内容为空
printf("与服务器断开链接...\n");
close(fd);
exit(1);
}
elseif(strcmp(recvmsg.flag,"all") == 0)
{//群聊信息
time(&curtime); //获取当前时间
// sprintf(str," %s %s 发给所有客户:%s\n\n",ctime(&curtime),recvmsg.name,recvmsg.msg);
printf(" %s %s 发给所有客户:%s\n\n",ctime(&curtime),recvmsg.name,recvmsg.msg);
}
elseif(strcmp(recvmsg.flag,"view") == 0)
{//查看在线客户
time(&curtime);
printf(" %s 当前在线客户:%s\n\n",ctime(&curtime),recvmsg.msg);
continue; //结束 本次 循环,继续等待新的信息
}
else if(strcmp(recvmsg.flag,"trans")== 0)
{//传送 文件 询问信息
pthread_t pid; //用于传送文件的线程id
if(strcmp(recvmsg.msg,"agree")== 0)
{//(发送端)发送方收到接收方的肯定接受文件的回复信息
strcpy(filetoname,recvmsg.name);
pthread_create(&pid,NULL,(void*)HandleSendfile,(void*)filename);
continue;
}
elseif(strcmp(recvmsg.msg,"disagree") == 0)
{//(发送端)接收方拒绝接收文件
printf("客户 %s 拒绝接收 %s文件\n",recvmsg.name,filename);
savefilefd = -1; //客户又可以重新选择在线用户传送文件了
continue;
}
else if(strcmp(recvmsg.msg,"noexist")== 0)
{//(发送端)客户不存在
printf("不存在该用户\n"); //可从服务器查看在线的客户,用 flag = view即可
savefilefd = -1; //客户又可以重新选择在线用户传送文件了
continue;
}
else
{//(接受端)如果发送端发送文件结束询问,~.msg 为 发送端要发送的文件名,故和以上的都不匹配
printf("客户 %s 传送 %s文件给你,是否接收?[agree(同意)] | [disagree(不同意)]\n",\
recvmsg.name,recvmsg.msg);//作为接受端,其接受到的的是发送端的传送文件询问信息,
//此时,recvmsg.name为发送端的名字,
//recvmsg.msg为将要发送的问文件名
strcpy(filename,recvmsg.msg); //接收方接受到的文件名
if((savefilefd =open(recvmsg.msg,O_CREAT | O_RDWR | O_APPEND,0666)) <0)
{//用 savefiledf 作为与接收到的文件的 文件描述符,但此时打开该文件不成功
printf("无法创建 %s 文件",recvmsg.msg);
}
}
/*
在传送文件的设计中,需要考虑到接收方,发送方都是客户端,都要接受收据
故在这数据的接受函数中,这个文件传送的询问信息关系到这两方
*/
continue; //继续等待信息
}//end trans
elseif(strcmp(recvmsg.flag,"transf") == 0)
{//(接收方)接受发送端发送过来的文件
/*
pthread_t pid; //接收方接收文件的线程处理函数
pthread_create(&pid,NULL,(void*)HandleRecvfile,(void*)&recvmsg);
问题:如此每次接受到一条文件信息,就要创建一个接收文件线程,不好。
如需真用接收文件线程,就需要与接收线程分开(HandleRecvmsg)
*/
if(strcmp(recvmsg.msg,"end$")== 0)
{//接受完发送端发送过来的文件
printf("接收文件完成\n");
close(savefilefd); //关闭刚接收完的文件
savefilefd= -1; //有可以接受或发送文件了
}
do
{//把数据写入文件中
nread =write(savefilefd,&recvmsg.msg,MAXLEN);
}while(nread > 0);
continue;
}
else
{//私聊,因为私聊是,flag 为 接收端的 名字,与以上不符
time(&curtime); //获取当前时间
// sprintf(str,"私聊: %s %s 发给客户:%s\n\n",ctime(&curtime),recvmsg.name,recvmsg.msg);
printf("私聊: %s %s 发给客户:%s\n\n",ctime(&curtime),recvmsg.name,recvmsg.msg);
}//群聊,私聊都没有用 continue 是因为,这些内容要写入 聊天记录 文件中,一起处理,方便而已,
//也可用continue,只要分别在 群聊,私聊 中直接写入 聊天记录 文件即可
write(fd,str,sizeof(str)); //把消息写入 聊天记录 文件中
msgsnd(qid,&msg,sizeof(structmsg),0); //发送消息给 消息队列,通知其 display进程 到 聊天记录 文件中读取数据
}//end while
return NULL;
}
/*
问题:可用 switch,可用 枚举 把 flag 打包起来,用相关数字表示
struct message
{
enum flag {all,view...}
...
}
*/