概述
任务要求
利用系统编程和网络编程相关知识,选择TCP或者UDP,编写一个程序,实现用户客户端与客户端之间,客户端与服务器之间的交互,客户端只能通过服务器获取其他客户端数据和执行相关操作,客户端的数据修改必须保存到服务器,需要实现的基本功能如下:
1.用户注册,并保存在服务器
2.用户登入,登入成功执行下一级操作
3.找回密码,
4.修改密码,并更新至服务器
5.注销账号,删除用户的所有文件和痕迹
6.退出
下一级菜单主要功能如下:
1.群聊,群成员发送的消息所有群成员都能收到,输入quit退出群聊
2.添加好友,添加指定用户为好友,对方通过申请才算添加成功
3.好友申请,查看他人发送给你的好友申请,你可以同意或者拒绝
4.好友列表,查看你的好友和状态,离线,在线,注销
5.私聊,选择指定好友进行聊天,只能与在线好友聊天
6.朋友圈,可以查看朋友圈和发朋友圈
7.聊天纪录,可以查看和删除与指定好友的聊天记录
8.传文件,给指定好友传输文件
9.下载文件,下载指定好友发送的文件
10.返回上一级菜单
流程图如下:
核心思想
本次项目设计主要通过UDP转发和接收数据,由于UDP只能发送一大块内存内容,不区分类型,为了实现多种多样的功能,定义了不同种类的消息结构体,发送前将这次消息结构体统一打包进消息包裹里面,并贴上消息类型的“标签”俗称“打包”,接收方收到后再根据消息包裹上面的“标签”,将包裹拆分复原成指定的消息类型,俗称“拆包”,
示意图如下:
注册
用户发送自己的注册账号,密码和电话号码给服务器进行处理,如果用户的账号与以及注册的账号同名则注册失败,反之服务器将用户的注册信息保存到服务器。
客户端代码如下
void registerUser(int sd, struct sockaddr_in *addr)
{
zhuce info; // 注册信息结构体
printf("\e[33m请依次输入你要注册的账号、电话号码、密码:\e[0m\n");
scanf("%s %s %s", info.name, info.tel, info.pwd);
package pack; // 用来封装信息结构体
memcpy(pack.msgBlock, &info, sizeof(info)); // 封装成统一的包
pack.msgType = REGISTER; // 贴上消息类型标签
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)addr, sizeof(struct sockaddr_in));
recvfrom(sd, &pack, sizeof(pack), 0, NULL, NULL);
printf("%s\n", pack.msgBlock);
}
```
服务器端代码如下
```
if (pack.msgType == REGISTER)
{
// 如果注册名字已经存在就注册失败
printf("\e[33m游客注册信息:\e[0m\n");
zhuce *info1 = (zhuce *)pack.msgBlock; // 拆开数据包还原
printf("\e[34mname:%s tel:%s pwd:%s\e[0m\n", info1->name, info1->tel, info1->pwd);
int fd = open("./server_file/userInfo.txt", O_CREAT | O_RDWR | O_APPEND, 0666); // 写入文件
int flag = 0;
// 判断是否读到同名
zhuce info3;
int ret = read(fd, &info3, sizeof(info3));
while (ret != 0) // 判断是否到达文件末尾
{
if (!strcmp(info3.name, info1->name)) // 名字相同
{
strcpy(buf, "\e[31m注册名字已存在,注册失败\e[0m");
puts(buf);
memcpy(pack.msgBlock, buf, sizeof(buf));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, length);
flag = 1;
close(fd);
break;
}
ret = read(fd, &info3, sizeof(info3));
}
if (flag == 0) // 没有重名的
{
// 为注册用户在系统创建个人文件夹
int dir_mode = 0755; // 目录权限
char dirname[128];
sprintf(dirname, "./server_file/%s", info1->name);
if (mkdir(dirname, dir_mode) == -1)
{
if (errno != EEXIST) // 如果创建失败
{
perror("Failed to create directory A");
continue;
}
}
write(fd, info1, sizeof(zhuce));
close(fd); //
strcpy(buf, "\e[1;32m注册成功\e[0m");
puts(buf);
memcpy(pack.msgBlock, buf, sizeof(buf));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, length);
continue;
}
}
```
头文件
头文件定义了各种消息类型和消息结构体以及主要头文件
```
#ifndef __REGISTER_H__
#define __REGISTER_H__
#include <malloc.h>
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <error.h>
#include <string.h>
#include <wxx.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <time.h>
#include <sys/types.h>
#include <semaphore.h>
#include <sys/stat.h>
#include <sys/errno.h>
#include <dirent.h>
#define REGISTER 1
#define DENGLU 2
#define SEEKPWD 3
#define TUICHU 4
#define QUNLIAO 5
#define DANLIAO 6
#define JIAQUN 7
#define QUNMSG 8
#define APPLY 9
#define REQUEST 10
#define REFUSE 11
#define AGREE 12
#define NOBODY 13
#define HAOYOU 14
#define UEXIT 15
#define XIUGAI 16
#define ZHUXIAO 17
#define DONGTAI 18
#define JIRU 19
#define CSFILE 20
#define CHUAN 21
#define SHOU 22
void dongHua();
void registerUser(int sd, struct sockaddr_in *addr);
void seekPwd(int sd, struct sockaddr_in *addr);
void deletePwd(int sd, struct sockaddr_in *addr);
void exitSys(int sd, struct sockaddr_in *addr);
void dengluUser(int sd, struct sockaddr_in *addr);
void next_menu(int sd, struct sockaddr_in *addr);
void changePwd(int sd, struct sockaddr_in *addr);
void *work(void *p);
// 消息载体结构体
typedef struct package
{
char msgType; // 消息类型
char msgBlock[1024]; // 消息内容载体
} package;
// 注册结构体
typedef struct zhuce
{
char name[128];
char tel[128];
char pwd[128];
} zhuce;
// 登入结构体
typedef struct denglu
{
char name[128];
char pwd[128];
} denglu;
// 找回密码结构体
typedef struct mima
{
char name[128];
char tel[128];
} mima;
typedef struct newPwd
{
char name[128];
char opwd[128];
char npwd[128];
} newPwd;
// 单聊结构体
typedef struct ptalk
{
int flag;
char srcname[128]; // 自己名字
char desname[128]; // 目标名字
char text[256]; // 聊天内容
} ptalk;
// 群聊结构体
typedef struct mtalk
{
char type;
char name[128]; // 自己名字
char text[256]; // 内容
} mtalk;
// 将用户的地址信息和名字绑定起来,便用服务器通过名字转发消息
typedef struct userAddrInfo
{
char name[128];
struct sockaddr_in addr;
} userAddrInfo;
typedef struct friendApr
{
char type;
char myname[128]; // 自己名字
char hisname[128]; // 要添加好友的名字
} friendApr;
// 还可以保存其他消息
typedef struct friend
{
char name[40]; // 好友名字
char time[40];
}
friend;
typedef struct friendState
{
char name[40]; // 名字
char isLine[10]; // 受否在线
} friendState;
typedef struct friendList
{
char name[128]; // 谁的好友列表
friendState flist[15]; // 好友列表,由于数据包大小原因,目前只设置最多15个好友
int num; // 好友数量
} friendList;
typedef struct pyq
{
char name[40]; // 谁发的
char time[40]; // 什么时候发的
char text[256]; // 发了什么
int flag; // 0代表发朋友圈,1代表看朋友圈,2代表最后一条朋友圈
} pyq;
typedef struct ltjr
{
char time[30]; // 什么时候
char name[20]; // 谁说的
char text[256]; // 说了什么
} ltjr;
// 用户进程发送消息的地址结构体
typedef struct opjr
{
int flag; // 0代表清空,1代表查看
int isOK; // 0代表没有,1代表完成
char desname[20]; // 谁要看
char srcname[20]; // 看谁的
ltjr lt; // 一条聊天记录
} opjr;
typedef struct fileBlock
{
int isOK; // 1表示传完了
int bytes; // 文件块大小
char text[800]; // 文件快内容
} fileBlock;
typedef struct chuanShou
{
char type; // 传还是收文件
char filename[100];
char desname[20]; // 发给谁
char srcname[20]; // 谁发的
fileBlock data;
} chuanShou;
typedef struct workAddr
{
int fd;
struct sockaddr_in addr;
} workAddr;
#endif
```
登录
用户输入账号和密码发送给服务器判断,如果账号未注册,或者密码错误,以及账号已经登录了,则登录失败,反之登录成功,进入下一级菜单
客户端代码如下:
```
void dengluUser(int sd, struct sockaddr_in *addr)
{
denglu info;
puts("\e[33m请输入你要登录的账号和密码:\e[0m");
scanf("%s %s", info.name, info.pwd);
package pack;
memcpy(pack.msgBlock, &info, sizeof(info));
pack.msgType = DENGLU;
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)addr, sizeof(struct sockaddr_in));
recvfrom(sd, &pack, sizeof(pack), 0, NULL, NULL);
if (!strcmp(pack.msgBlock, "登录成功")) // 登入成功继续
{
strcpy(loginUserName, info.name); // 拷贝名字用于发送
next_menu(sd, addr);
}
else
{
printf("%s\n", pack.msgBlock);
}
}
```
服务器代码如下:
```
else if (pack.msgType == DENGLU)
{
printf("\e[33m游客登录信息:\e[0m\n");
denglu *info2 = (denglu *)pack.msgBlock;
printf("\e[34mname:%s pwd:%s\e[0m\n", info2->name, info2->pwd);
int fd = open("./server_file/userInfo.txt", O_RDONLY);
int flag = 0;
zhuce info3;
int ret = read(fd, &info3, sizeof(info3));
while (ret != 0)
{
if (!strcmp(info2->name, info3.name)) // 账号正确
{
if (strcmp(info2->pwd, info3.pwd)) // 密码错误
{
strcpy(buf, "\e[1;31m密码错误\e[0m");
puts(buf);
flag = 2;
memcpy(pack.msgBlock, buf, sizeof(buf));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, length);
break;
}
else
{
int i;
// 判断账号是不是已经登录了
for (i = 0; i < users; i++)
{
if (!strcmp(loginUser[i].name, info2->name))
{
strcpy(buf, "\e[1;31m账号已经被登录了,登陆失败\e[0m");
puts(buf);
flag = 3;
memcpy(pack.msgBlock, buf, sizeof(buf));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, length);
break;
}
}
if (i >= users)
{
strcpy(buf, "登录成功");
printf("\e[1;32m用户%s%s\e[0m\n", info3.name, buf);
strcpy(pack.msgBlock, buf);
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, length);
// 保存下来,用来收发消息,名字和地址绑定起来发送
flag = 1;
loginUser[users].addr = clientAddr; // 保存登录用户的地址
strcpy(loginUser[users].name, info3.name); // 保存用户名字
users++; // 用户数量加一
break;
}
}
}
ret = read(fd, &info3, sizeof(info3));
}
if (flag == 0)
{
strcpy(buf, "\e[1;31m该账号还没有注册\e[0m");
puts(buf);
memcpy(pack.msgBlock, buf, sizeof(buf));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, length);
}
close(fd);
}
```
找回密码
客户不记得账号密码时,可通过绑定的电话号码找回原账号密码。
客户端代码如下;
```
void seekPwd(int sd, struct sockaddr_in *addr)
{
mima mm;
puts("\e[33m请输入你要找回的账号:\e[0m");
scanf("%s", mm.name);
puts("\e[33m请输入你绑定的电话号码来找回密码:\e[0m");
scanf("%s", mm.tel);
package pack;
memcpy(pack.msgBlock, &mm, sizeof(mm));
pack.msgType = SEEKPWD;
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)addr, sizeof(struct sockaddr_in));
recvfrom(sd, &pack, sizeof(pack), 0, NULL, NULL);
printf("%s\n", pack.msgBlock);
}
```
服务器端代码如下:
```
else if (pack.msgType == SEEKPWD)
{
mima mm = *(mima *)pack.msgBlock;
printf("\e[33m游客找回密码消息:\e[0m\n");
printf("\e[34mname:%s tel:%s\e[0m\n", mm.name, mm.tel);
int fd = open("./server_file/userInfo.txt", O_RDONLY);
zhuce zc;
int ret = read(fd, &zc, sizeof(zc));
int flag = 0;
while (ret)
{
if (!strcmp(zc.name, mm.name)) // 账号一样
{
if (!strcmp(zc.tel, mm.tel)) // 电话号码也一样
{
flag = 1;
char buf[128];
sprintf(buf, "\e[34m账号密码是:%s\e[0m", zc.pwd);
puts(buf);
memcpy(pack.msgBlock, buf, sizeof(buf));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, length);
break;
}
else // 电话号码不一样
{
flag = 2;
char buf[128] = "\e[1;31m输入的电话号码错误\e[0m";
puts(buf);
memcpy(pack.msgBlock, buf, sizeof(buf));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, length);
break;
}
}
ret = read(fd, &zc, sizeof(zc));
}
if (flag == 0)
{
char buf[128] = "\e[1;31m输入的账号未注册\e[0m";
puts(buf);
memcpy(pack.msgBlock, buf, sizeof(buf));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, length);
}
close(fd);
}
```
修改密码
客户可以通过输入原账号及密码来修改密码
客户端代码如下:
```
void changePwd(int sd, struct sockaddr_in *addr)
{
newPwd npd;
printf("\e[33m请输入你要修改的账号:\e[0m");
scanf("%s", npd.name);
printf("\e[33m请输入账号原密码:\e[0m");
scanf("%s", npd.opwd);
printf("\e[33m请输入账号新密码:\e[0m");
scanf("%s", npd.npwd);
package pack;
pack.msgType = XIUGAI;
memcpy(pack.msgBlock, &npd, sizeof(npd));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)addr, sizeof(struct sockaddr_in));
recvfrom(sd, &pack, sizeof(pack), 0, NULL, NULL);
printf("%s\n", pack.msgBlock);
}
```
服务器端代码如下:
```
else if (pack.msgType == XIUGAI)
{
newPwd npd = *(newPwd *)pack.msgBlock;
printf("\e[33m游客的修改账号密码消息:\e[0m\n");
printf("\e[34mname:%s\toname:%s\tnname:%s\e[0m\n", npd.name, npd.opwd, npd.npwd);
int fd = open("./server_file/userInfo.txt", O_RDONLY);
zhuce zh;
int ret = read(fd, &zh, sizeof(zh));
zhuce sumInfo[100];
int num = 0;
int flag = 0;
while (ret)
{
if (!strcmp(zh.name, npd.name)) // 账号相同
{
if (strcmp(zh.pwd, npd.opwd)) // 账号密码不相等
{
flag = 1;
char buf[128] = "\e[31m账号原密码错误,修改密码失败!\e[0m";
puts(buf);
memcpy(pack.msgBlock, buf, sizeof(buf));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, length);
break;
}
else // 找到要修改的账号
{
flag = 2;
strcpy(zh.pwd, npd.npwd); // 修改原密码
sumInfo[num++] = zh;
}
}
else
{
sumInfo[num++] = zh;
}
ret = read(fd, &zh, sizeof(zh));
}
close(fd);
if (flag == 0)
{
char buf[128] = "\e[1;31m输入的账号未注册\e[0m";
puts(buf);
memcpy(pack.msgBlock, buf, sizeof(buf));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, length);
}
else if (flag == 2)
{
fd = open("./server_file/userInfo.txt", O_TRUNC | O_WRONLY);
for (int i = 0; i < num; i++)
{
write(fd, &sumInfo[i], sizeof(zhuce));
}
close(fd);
char buf[128] = "\e[31m账号密码修改成功\e[0m";
puts(buf);
memcpy(pack.msgBlock, buf, sizeof(buf));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, length);
}
}
```
注销账号
删除服务器为用户创建的所有数据和文件,将用户从服务器的用户账号信息表中删除。
客户端代码如下:
```
void deletePwd(int sd, struct sockaddr_in *addr)
{
zhuce zh;
printf("\e[33m请输入你要注销的账号:\e[0m");
scanf("%s", zh.name);
printf("\e[33m请输入你绑定的电话号码:\e[0m");
scanf("%s", zh.tel);
printf("\e[33m请输入你的密码:\e[0m");
scanf("%s", zh.pwd);
package pack;
pack.msgType = ZHUXIAO;
memcpy(pack.msgBlock, &zh, sizeof(zh));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)addr, sizeof(struct sockaddr_in));
recvfrom(sd, &pack, sizeof(pack), 0, NULL, NULL);
printf("%s\n", pack.msgBlock);
}
```
服务器端代码如下:
```
else if (pack.msgType == ZHUXIAO)
{
zhuce np = *(zhuce *)pack.msgBlock;
printf("\e[33m游客的注销账号消息:\e[0m\n");
printf("\e[34mname:%s\ttel:%s\tpwd:%s\n\e[0m", np.name, np.tel, np.pwd);
// 删除该用户在系统的所有文件和痕迹
int fd = open("./server_file/userInfo.txt", O_RDONLY);
zhuce zh;
int ret = read(fd, &zh, sizeof(zh));
zhuce sumInfo[100];
int num = 0;
int flag = 0;
while (ret)
{
if (!strcmp(zh.name, np.name)) // 本人
{
if (strcmp(zh.pwd, np.pwd)) // 密码错误
{
flag = 1;
char buf[128] = "\e[31m密码错误,注销失败\e[0m";
puts(buf);
memcpy(pack.msgBlock, buf, sizeof(buf));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, length);
break;
}
if (strcmp(zh.tel, np.tel))
{
flag = 1;
char buf[128] = "\e[31m电话号码错误,注销失败\e[0m";
puts(buf);
memcpy(pack.msgBlock, buf, sizeof(buf));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, length);
break;
}
flag = 2;
}
else
{
sumInfo[num++] = zh;
}
ret = read(fd, &zh, sizeof(zh));
}
close(fd);
if (flag == 0)
{
char buf[128] = "\e[1;31m输入的账号未注册\e[0m";
puts(buf);
memcpy(pack.msgBlock, buf, sizeof(buf));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, length);
}
else if (flag == 2)
{
// 删除服务器为注册用户创建的个人文件夹和里面的文件
char dirname[128];
sprintf(dirname, "./server_file/%s", np.name);
DIR *dp = opendir(dirname);
struct dirent *mem;
while ((mem = readdir(dp)) != NULL)
{
if (strcmp(mem->d_name, ".") == 0 || strcmp(mem->d_name, "..") == 0)
{
continue;
}
char filename[128];
sprintf(filename, "%s/%s", dirname, mem->d_name);
remove(filename);
}
closedir(dp);
rmdir(dirname);
fd = open("./server_file/userInfo.txt", O_TRUNC | O_WRONLY);
for (int i = 0; i < num; i++)
{
write(fd, &sumInfo[i], sizeof(zhuce));
}
close(fd);
char buf[128] = "\e[31m账号注销成功\e[0m";
puts(buf);
memcpy(pack.msgBlock, buf, sizeof(buf));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, length);
}
}
```
退出
客户退出功能实现比较简单,告诉服务器退出后,清屏终端界面
客户端代码如下:
```
void exitSys(int sd, struct sockaddr_in *addr)
{
package pack;
char buf[128] = "退出系统";
memcpy(pack.msgBlock, buf, sizeof(buf));
pack.msgType = TUICHU;
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)addr, sizeof(struct sockaddr_in));
recvfrom(sd, &pack, sizeof(pack), 0, NULL, NULL);
printf("\e[H\e[J"); // 清屏
printf("%s\n", pack.msgBlock);
close(sd);
exit(0);
}
```
服务器代码如下:
```
else if (pack.msgType == TUICHU)
{
printf("\e[34m游客退出消息:%s\e[0m\n", pack.msgBlock);
char buf[128] = "\e[1;32m退出成功\e[0m";
puts(buf);
memcpy(pack.msgBlock, buf, sizeof(buf));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, length);
}
```
下一级菜单
1.群聊
客户加入群聊后,可以收到群内成员发送的消息,也可以给群内所有成员发送信息,谁加入和退出群聊都可以告诉群内其他成员,客户可以输入quit主动退出群聊:
客户端代码如下:
```
case 1: // 群聊
{
// 告诉系统你要加入群聊
mtalk mta;
package pack;
mta.type = JIAQUN; // 加群申请,不输出消息内容,只保存地址信息和名字
strcpy(mta.name, loginUserName); // 告诉大家是谁群发的消息
memset(mta.text, 0, sizeof(mta.text)); // 没有内容
memcpy(pack.msgBlock, &mta, sizeof(mta)); // 数据封装
pack.msgType = QUNLIAO;
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)addr, sizeof(struct sockaddr_in));
printf("\e[33m\t华清嵌入式2403班级群\e[0m\n"); // 进入群聊
printf("\e[37msystem:输入quit可退出群聊\e[0m\n");
while (1)
{
scanf("%s", mta.text);
mta.type = QUNMSG;
memcpy(pack.msgBlock, &mta, sizeof(mta)); // 数据封装
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)addr, sizeof(struct sockaddr_in));
if (!strcmp(mta.text, "quit"))
{
printf("\32m退出群聊成功\e[0m\n");
break;
}
}
sem_post(&sem1);
}
break;
```
客户接收线程代码如下:
```
if (pack.msgType == QUNLIAO) // 群聊
{
mtalk mta = *(mtalk *)pack.msgBlock; // 拆包
printf("\e[34m%s(群发):%s\n\e[0m", mta.name, mta.text);
}
```
服务器代码如下:
```
else if (pack.msgType == QUNLIAO)
{
mtalk mta = *(mtalk *)pack.msgBlock;
if (mta.type == JIAQUN) // 用户加入群聊
{
strcpy(groupMem[groupNum].name, mta.name); // 保存群员名字
groupMem[groupNum].addr = clientAddr; // 保存群员地址
groupNum++;
// 告诉群员谁加入群聊了
char buf[128];
sprintf(buf, "\e[31m%s加入群聊\e[0m", mta.name);
printf("%s\n", buf);
strcpy(mta.text, buf);
memcpy(&pack.msgBlock, &mta, sizeof(mta));
for (int i = 0; i < groupNum; i++)
{
// 告诉其他群成员谁退出群聊了
if (strcmp(mta.name, groupMem[i].name))
{
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&groupMem[i].addr, length);
}
}
}
else // 用户发送群消息
{
if (!strcmp(mta.text, "quit")) // 用户退出群聊,删除群成员
{
char buf[128];
sprintf(buf, "\e[31m%s退出群聊\e[0m", mta.name);
printf("%s\n", buf);
strcpy(mta.text, buf);
memcpy(&pack.msgBlock, &mta, sizeof(mta));
for (int i = 0; i < groupNum; i++)
{
// 告诉其他群成员谁退出群聊了
if (strcmp(mta.name, groupMem[i].name))
{
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&groupMem[i].addr, length);
}
}
// 删除它
for (int i = 0; i < groupNum; i++)
{
if (!strcmp(mta.name, groupMem[i].name)) // 找到他
{
for (int j = i; j < groupNum - 1; j++)
{
groupMem[j] = groupMem[j + 1];
}
}
}
groupNum--; // 数量减一
}
else
{
printf("\e[34m%s的群发消息:%s\n\e[0m", mta.name, mta.text);
for (int i = 0; i < groupNum; i++)
{
// 由于名字是唯一的,因此可以通过比较名字来判断是不是自己,不是自己就转发
if (strcmp(mta.name, groupMem[i].name))
{
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&groupMem[i].addr, length);
}
}
}
}
}
```
2.添加好友
客户可通过输入其他客户名字进行好友添加,必须是在线且注册过的名字
客户端代码如下:
```
case 2: // 只能添加登录用户为好友
{
friendApr fri;
package pack;
pack.msgType = APPLY;
strcpy(fri.myname, loginUserName); // 谁申请添加好友
printf("\e[33m请输入你要添加的好友名字:\e[0m");
scanf("%s", fri.hisname);
fri.type = REQUEST;
memcpy(pack.msgBlock, &fri, sizeof(fri));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)addr, sizeof(struct sockaddr_in));
sem_post(&sem1);
}
break;
```
客户接收线程如下:
```
else if (pack.msgType == APPLY)
{
friendApr fri = *(friendApr *)pack.msgBlock;
// 只有接收方才能收到
if (fri.type == REQUEST) // 请求添加好友
{
printf("\e[31m%s申请添加你为好友\e[0m\n", fri.myname);
sem_wait(&sem2);
printf("\e[31m%s申请添加你为好友\e[0m\n", fri.myname);
printf("\e[33m请输入你的选择(同意或者拒绝):\e[0m");
char sel[20];
scanf("%s", sel);
if (!strcmp(sel, "同意")) // 同意好友申请
{
fri.type = AGREE; // 修改状态,同意好友申请
memcpy(pack.msgBlock, &fri, sizeof(fri));
sendto(waddr.fd, &pack, sizeof(pack), 0, (struct sockaddr *)&waddr.addr, sizeof(struct sockaddr_in));
}
else // 拒绝
{
fri.type = REFUSE; // 修改为拒绝
memcpy(pack.msgBlock, &fri, sizeof(fri));
sendto(waddr.fd, &pack, sizeof(pack), 0, (struct sockaddr *)&waddr.addr, sizeof(struct sockaddr_in));
}
sem_post(&sem1);
}
// 只有发送好友申请的才能收到
else if (fri.type == AGREE)
{
printf("\e[31m%s同意了你的好友申请,你们已经是好友了\e[0m\n", fri.hisname);
}
//
else if (fri.type == REFUSE)
{
printf("\e[31m%s拒绝了你的好友申请\e[0m\n", fri.hisname);
}
else if (fri.type == NOBODY)
{
printf("%s\n", fri.myname);
}
}
```
服务器代码如下:
```
else if (pack.msgType == APPLY)
{
friendApr fri = *(friendApr *)pack.msgBlock;
if (fri.type == REQUEST)
{
printf("\e[31m%s的好友申请消息:请求添加%s为好友\e[0m\n", fri.myname, fri.hisname);
int i;
for (i = 0; i < users; i++)
{
if (!strcmp(loginUser[i].name, fri.hisname)) // 找到要添加的好友
{
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&loginUser[i].addr, length); // 转发好友申请
break;
}
}
if (i >= users) // 没有找个人
{
sprintf(fri.myname, "\e[31m%s未上线或者不存在,添加失败!\e[0m", fri.hisname);
printf("%s\n", fri.myname);
fri.type = NOBODY;
memcpy(pack.msgBlock, &fri, sizeof(fri));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, length);
}
}
else if (fri.type == REFUSE)
{
printf("\e[31m%s拒绝了%s的好友申请!\e[0m\n", fri.hisname, fri.myname);
for (int i = 0; i < users; i++)
{
if (!strcmp(loginUser[i].name, fri.myname)) // 找到发好友申请的人
{
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&loginUser[i].addr, length); // 转发好友申请
break;
}
}
}
else if (fri.type == AGREE)
{
printf("\e[31m%s同意了%s的好友申请\e[0m\n", fri.hisname, fri.myname);
// 他把我加入好友列表
char fileName[128];
sprintf(fileName, "./server_file/%s/friends.dat", fri.hisname);
// 创建文件保存用户的好友
int fd = open(fileName, O_CREAT | O_WRONLY | O_APPEND, 0666);
friend friInfo;
strcpy(friInfo.name, fri.myname); // 写入好友名字
time_t t1;
time(&t1);
struct tm *now;
now = localtime(&t1);
char nowtime[128];
sprintf(nowtime, "%d年%d月%d日%d时", now->tm_year + 1900, now->tm_mon + 1, now->tm_mday, now->tm_hour);
strcpy(friInfo.time, nowtime); // 写入时间
write(fd, &friInfo, sizeof(friInfo)); // 写入文件
close(fd);
// 我把他加入好友列表
sprintf(fileName, "./server_file/%s/friends.dat", fri.myname);
fd = open(fileName, O_CREAT | O_WRONLY | O_APPEND, 0666);
strcpy(friInfo.name, fri.hisname); // 写入好友名字
strcpy(friInfo.time, nowtime); // 写入时间
write(fd, &friInfo, sizeof(friInfo)); // 写入文件
close(fd);
for (int i = 0; i < users; i++)
{
if (!strcmp(loginUser[i].name, fri.myname)) // 找到发好友申请的人
{
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&loginUser[i].addr, length); // 转发好友申请
break;
}
}
}
}
```
3.好友申请
客户给其他客户发送好友申请后会提示其他用户,其他用户可以通过好友申请,主动输入同意或者拒绝,输入同意则成为好友,输入拒绝则添加失败:
客户端线程接受代码同上;
客户端代码如下,因为需要在子线程进行输入,所以必须先抵消掉主线程的输入,然后锁住主线程,等待子线程执行完后释放主线程,所以设置了好友申请这一环节,目的在于抵消掉主线程的读取输入;
```
case 3:
sem_post(&sem2);
break;
```
服务器代码同上;
4.好友列表
客户可以查看自己添加的所有好友以及好友的状态,在线,离线,注销,我在这里设置了一个好友结构体数组来传输所有好友信息给客户端;
客户端代码如下
```
case 4:
{
friendList fli;
package pack;
pack.msgType = HAOYOU;
// 初始化
strcpy(fli.name, loginUserName);
memset(&fli.flist, 0, sizeof(fli.flist));
fli.num = 0;
memcpy(pack.msgBlock, &fli, sizeof(fli));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)addr, sizeof(struct sockaddr_in));
sem_post(&sem3);
}
```
客户接收线程代码如下:
```
else if (pack.msgType == HAOYOU)
{
sem_wait(&sem3);
friendList fli = *(friendList *)pack.msgBlock;
printf("----\e[36m%s的好友列表\e[0m----\n", fli.name);
if (fli.num == 0)
{
printf("\n[1;31m你还没有好友!\e[0m\n");
}
else
{
printf("\e[33m共有%d位好友\e[0m\n", fli.num);
printf("\e[32m好友\t状态\e[0m\n");
for (int i = 0; i < fli.num; i++)
{
if (!strcmp(fli.flist[i].isLine, "在线"))
{
printf("\e[34m%s\t%s\e[0m\n", fli.flist[i].name, fli.flist[i].isLine);
}
else
{
printf("%s\t%s\n", fli.flist[i].name, fli.flist[i].isLine);
}
}
}
sem_post(&sem1);
}
```
服务器代码如下:
```
else if (pack.msgType == HAOYOU)
{
friendList fli = *(friendList *)pack.msgBlock;
printf("用户%s查看好友列表\n", fli.name);
char filename[128];
sprintf(filename, "./server_file/%s/friends.dat", fli.name);
int fd = open(filename, O_RDONLY);
friend fri;
int ret = read(fd, &fri, sizeof(fri));
while (ret)
{
strcpy(fli.flist[fli.num].name, fri.name);
int fd2 = open("./server_file/userInfo.txt", O_RDONLY);
zhuce zh;
int flag = 0;
while (read(fd2, &zh, sizeof(zh)))
{
if (!strcmp(zh.name, fri.name))
{
flag = 1;
break;
}
}
// 判断好友是否在线
if (flag == 1)
{
int i;
for (i = 0; i < users; i++)
{
if (!strcmp(fri.name, loginUser[i].name))
{
strcpy(fli.flist[fli.num].isLine, "在线");
break;
}
}
if (i >= users)
{
strcpy(fli.flist[fli.num].isLine, "离线");
}
}
else
{
strcpy(fli.flist[fli.num].isLine, "注销");
}
fli.num++;
ret = read(fd, &fri, sizeof(fri));
}
memcpy(pack.msgBlock, &fli, sizeof(fli));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, length);
}
```
5.返回上一级菜单
退出至登入界面菜单,将客户从登入用户里面删除
客户端代码如下:
```
case 5:
{
package pack;
pack.msgType = UEXIT;
memcpy(pack.msgBlock, loginUserName, sizeof(loginUserName));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)addr, sizeof(struct sockaddr_in));
pthread_cancel(pt); // 取消子线程
printf("\e[H\e[J");
return;
}
```
服务器端代码如下:
```
else if (pack.msgType == UEXIT)
{
printf("\e[31m%s退出登录\e[0m\n", pack.msgBlock);
// 删除他
for (int i = 0; i < users; i++)
{
if (!strcmp(pack.msgBlock, loginUser[i].name))
{
for (int j = i; j < users - 1; j++)
{
loginUser[j] = loginUser[j + 1];
}
break;
}
}
users--;
}
```
6.私聊
客户可以跟他的在线好友进行一对一聊天
客户端代码如下:
```
case 6:
{
package pack;
pack.msgType = DANLIAO;
ptalk ptk;
strcpy(ptk.srcname, loginUserName);
printf("\e[33m请输入你要单聊的好友名字:\e[0m");
scanf("%s", ptk.desname);
ptk.flag = 0;
// 判断是不是好有,好友在不在线
while (1)
{
printf("\e[34m请输入你要发送的消息:\e[0m");
scanf("%s", ptk.text);
memcpy(pack.msgBlock, &ptk, sizeof(ptk));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)addr, sizeof(struct sockaddr_in));
if (!strcmp(ptk.text, "quit"))
{
printf("\e[32m退出私聊成功\e[0m\n");
break;
}
}
sem_post(&sem1);
}
break;
```
客户端接收线程代码如下:
```
else if (pack.msgType == DANLIAO) // 私聊
{
ptalk pta = *(ptalk *)pack.msgBlock;
if (pta.flag == 1)
{
printf("\e[31m服务器提示消息:%s\e[0m\n", pta.text);
printf("\e[33m请输入quit退出私聊界面\e[0m\n");
}
else if (pta.flag == 0)
{
if (!strcmp(pta.text, "quit"))
{
printf("\e[33m好友%s退出聊天了\e[0m\n", pta.srcname);
}
else
{
printf("\e[34m好友%s(私发):%s\e[0m\n", pta.srcname, pta.text);
}
}
}
```
服务器接收代码如下:
```
else if (pack.msgType == DANLIAO)
{
ptalk ptk = *(ptalk *)pack.msgBlock;
printf("\e[34m%s私发给%s的消息:%s\e[0m\n", ptk.srcname, ptk.desname, ptk.text);
// 先判断是不是好友
char fileName[128];
sprintf(fileName, "./server_file/%s/friends.dat", ptk.srcname);
int fd = open(fileName, O_RDONLY);
friend fri;
int flag = 0;
int ret = read(fd, &fri, sizeof(fri));
while (ret)
{
if (!strcmp(fri.name, ptk.desname)) // 他是你的好友
{
flag = 1;
// 判断好友是否在线
for (int i = 0; i < users; i++)
{
if (!strcmp(ptk.desname, loginUser[i].name)) // 在线
{
flag = 2;
printf("\e[33m%s是%s的好友而且在线,可以私聊\e[0m\n", ptk.desname, ptk.srcname);
// 保存聊天记录
char filename[128];
sprintf(filename, "./server_file/%s/%s.dat", ptk.srcname, ptk.desname);
int fd2 = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666); // 创建聊天记录文件夹
ltjr lt;
strcpy(lt.name, ptk.srcname); // 谁发的
strcpy(lt.text, ptk.text);
time_t t1;
time(&t1);
struct tm *now;
now = localtime(&t1);
char nowtime[128];
sprintf(nowtime, "%d月%d日%d时%d分", now->tm_mon + 1, now->tm_mday, now->tm_hour, now->tm_min);
strcpy(lt.time, nowtime); // 写入时间
write(fd2, <, sizeof(lt));
close(fd2);
sprintf(filename, "./server_file/%s/%s.dat", ptk.desname, ptk.srcname);
fd2 = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666); // 创建聊天记录文件夹
write(fd2, <, sizeof(lt));
close(fd2);
//
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&loginUser[i].addr, sizeof(struct sockaddr_in));
break;
}
}
break;
}
ret = read(fd, &fri, sizeof(fri));
}
close(fd);
if (flag != 2)
{
ptk.flag = 1;
char buf[128];
if (flag == 0)
{
sprintf(buf, "\e[31m%s不是%s的好友,不可以私聊\e[0m", ptk.desname, ptk.srcname);
}
else if (flag == 1)
{
sprintf(buf, "\e[31m%s的好友%s不在线,不可以私聊\e[0m", ptk.srcname, ptk.desname);
}
puts(buf);
strcpy(ptk.text, buf);
memcpy(pack.msgBlock, &ptk, sizeof(ptk));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, sizeof(struct sockaddr_in));
recvfrom(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, &length); // 接收quit但不处理
}
}
```
7. 朋友圈
发朋友圈
客户可以将自己写的说说发送给他的所有好友,
看朋友圈
客户可以通过选择查看朋友圈查看自己和自己所有好友发送的说说
客户端代码如下:
```
case 7:
{
printf("\e[033m1.发朋友圈------2.看朋友圈\e[0m\n");
int xz;
scanf("%d", &xz);
getchar();
if (xz == 1)
{
package pack;
pack.msgType = DONGTAI;
pyq tiez;
tiez.flag = 0; // 告诉服务器这是发朋友圈
strcpy(tiez.name, loginUserName);
time_t t1;
time(&t1);
struct tm *now;
now = localtime(&t1);
char nowtime[128];
sprintf(nowtime, "%d年%d月%d日%d时%d分", now->tm_year + 1900, now->tm_mon + 1, now->tm_mday, now->tm_hour, now->tm_min);
strcpy(tiez.time, nowtime); // 写入时间
printf("\e[33m请输入你要发表的说说:\e[0m");
scanf("%[^\n]", tiez.text);
memcpy(pack.msgBlock, &tiez, sizeof(tiez));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)addr, sizeof(struct sockaddr_in));
}
else if (xz == 2)
{
printf("----------\e[1;32m朋友圈\e[0m----------\n");
package pack;
pack.msgType = DONGTAI;
pyq tiez;
tiez.flag = 1;
strcpy(tiez.name, loginUserName);
memset(tiez.text, 0, sizeof(tiez.text));
memset(tiez.time, 0, sizeof(tiez.time));
memcpy(pack.msgBlock, &tiez, sizeof(tiez));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)addr, sizeof(struct sockaddr_in)); // 告诉服务器你要看朋友圈
}
sem_post(&sem5);
}
break;
```
客户端接收线程代码如下:
```
else if (pack.msgType == DONGTAI)
{
sem_wait(&sem5);
pyq tiez = *(pyq *)pack.msgBlock;
if (tiez.flag == 0)//发朋友圈
{
printf("\e[31m发表成功,等待好友评论和点赞\e[0m\n");
sem_post(&sem1);
}
else if (tiez.flag == 1) // ,查看朋友圈,至少有一条朋友圈
{
while (1)
{
printf("\e[36m%s\t%s\e[0m\n", tiez.time, tiez.name);
printf(" \e[34m%s\n\n\e[0m", tiez.text);
recvfrom(waddr.fd, &pack, sizeof(pack), 0, NULL, NULL);
tiez = *(pyq *)pack.msgBlock;
if (tiez.flag == 2)
{
break;
}
}
sem_post(&sem1);
}
else if (tiez.flag == 3) // 朋友圈为空
{
printf("\e[31m%s\t%s\e[0m\n", tiez.time, tiez.name);
printf(" \e[31m%s\n\n\e[0m", tiez.text);
sem_post(&sem1);
}
}
```
服务器端代码如下:
```
else if (pack.msgType == DONGTAI)
{
pyq tiez = *(pyq *)pack.msgBlock;
if (tiez.flag == 0)
{
printf("\e[033m%s\e[0m\n", tiez.time);
printf("\e[34m%s发的朋友圈:%s\e[0m\n", tiez.name, tiez.text);
char filename[128];
sprintf(filename, "./server_file/%s/pyq.dat", tiez.name);
int fd = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
write(fd, &tiez, sizeof(tiez));
close(fd);
sprintf(filename, "./server_file/%s/friends.dat", tiez.name);
fd = open(filename, O_RDONLY | O_CREAT, 0666);
friend fri;
int ret = read(fd, &fri, sizeof(fri));
// 应该判断一下好友注销没
while (ret)
{
int fd3 = open("./server_file/userInfo.txt", O_RDONLY);
zhuce zh;
int flag = 0;
while (read(fd3, &zh, sizeof(zh)))
{
if (!strcmp(fri.name, zh.name))
{
flag = 1; // 没有注销
break;
}
}
close(fd3);
if (flag == 1)
{
sprintf(filename, "./server_file/%s/pyq.dat", fri.name);
int fd2 = open(filename, O_CREAT | O_WRONLY | O_APPEND, 0666);
write(fd2, &tiez, sizeof(tiez));
close(fd2);
}
ret = read(fd, &fri, sizeof(fri));
}
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, sizeof(struct sockaddr_in)); // 告诉用户发表成功
close(fd);
}
else if (tiez.flag == 1) // 看朋友圈,把朋友圈的系统文件发送给用户
{
printf("\e[34m用户%s查看朋友圈\e[0m\n", tiez.name);
char filename[128];
sprintf(filename, "./server_file/%s/pyq.dat", tiez.name);
int fd = open(filename, O_RDONLY); // 打开保存用户朋友圈的服务器文件
if (fd < 0) // 如果没有人发过朋友圈
{
close(fd);
strcpy(tiez.text, "你的朋友圈为空");
strcpy(tiez.name, "");
strcpy(tiez.time, "");
tiez.flag = 3; // 告诉客户发完了
memcpy(pack.msgBlock, &tiez, sizeof(tiez));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, sizeof(struct sockaddr_in));
perror("open fail");
}
else
{
while (read(fd, &tiez, sizeof(tiez))) // 取出朋友圈
{
printf("\e[36m%s\t%s\e[0m\n", tiez.time, tiez.name);
printf(" \e[34m%s\n\n\e[0m", tiez.text);
tiez.flag = 1; // TMD,我忘了我朋友圈文件里面也有标准位
memcpy(pack.msgBlock, &tiez, sizeof(tiez));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, sizeof(struct sockaddr_in));
usleep(4); // 延时一下,然客户端接收
}
close(fd);
tiez.flag = 2; // 告诉客户发完了
memcpy(pack.msgBlock, &tiez, sizeof(tiez));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, sizeof(struct sockaddr_in));
}
printf("\e[32m朋友圈发送完毕\n\e[0m");
}
}
```
8. 聊天记录
查看聊天记录
客户可以通过好友名字查看和好友的聊天记录
清空聊天记录
客户可以通过好友名字清空与该好友的聊天记录
客户端代码如下:
```
case 8:
{
printf("\e[35m请输入你要操作的好友名:\e[0m");
char name[20];
scanf("%s", name);
printf("\e[033m1.查看聊天记录------2.清空聊天记录\e[0m\n");
int xz;
scanf("%d", &xz);
opjr op1;
strcpy(op1.srcname, loginUserName);
strcpy(op1.desname, name);
memset(&op1.lt, 0, sizeof(op1.lt));
op1.isOK = 0;
if (xz == 1)
{
op1.flag = 1;
}
else if (xz == 2)
{
op1.flag = 0;
}
package pack;
pack.msgType = JIRU;
memcpy(pack.msgBlock, &op1, sizeof(op1));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)addr, sizeof(struct sockaddr_in));
sem_post(&sem4);
}
break;
```
客户端接收线程代码如下:
```
else if (pack.msgType == JIRU)
{
sem_wait(&sem4);
opjr op1 = *(opjr *)pack.msgBlock;
if (op1.flag == 0) // 清空完成
{
printf("\e[31m清空聊天记录成功\n\e[0m");
sem_post(&sem1); // 回到菜单栏
}
else // 查看聊天记录
{
// 想查看那一天的聊天记录也可以,只需要对时间进行分类查看输出就行
if (op1.isOK == 0) // 还没传完
{
if (strcmp(op1.lt.time, temp.time)) // 时间不相同
{
// 时间居中对齐输出
for (int i = 0; i < (tty_size - strlen(op1.lt.time) + 4) / 2; i++)
printf(" ");
printf("%s\n", op1.lt.time);
}
if (strcmp(loginUserName, op1.lt.name)) // 不是自己发的
{
if (strcmp(op1.lt.text, "quit"))
{
printf("\e[34m%s:%s\e[0m\n", op1.lt.name, op1.lt.text); // 蓝色对方用,自古红蓝出CP
}
}
else
{
/*因为在UTF-8编码中汉字占3个字节,但在终端输出是占2个空格
因此汉字字符串要乘以2/3,但因为字符串中不全是汉字,因此算出来的
结果会偏小,输出的空格过多,以至于想要字符串输出紧靠终端行末而溢出
到下一行,在这里没有实现汉字和普通字符的区分,有待下一步提高
*/
if (strcmp(op1.lt.text, "quit")) // 不打印quit
{
char buf[256];
sprintf(buf, "%s:%s", op1.lt.text, op1.lt.name);
for (int i = 0; i < tty_size - strlen(buf) * 2 / 3 - 2; i++)
printf(" ");
printf("\e[31m%s\n\e[0m", buf);
}
}
temp = op1.lt; // temp是上一条聊天记录
sem_post(&sem4); // 让线程继续接收消息
}
else // 传输完成
{
sem_post(&sem1);
}
}
}
```
服务器端代码如下:
```
else if (pack.msgType == JIRU)
{
opjr op1 = *(opjr *)pack.msgBlock;
if (op1.flag == 1)
{
printf("\e[34m用户%s想要查看和%s的聊天记录\e[0m\n", op1.srcname, op1.desname);
char filename[128];
sprintf(filename, "./server_file/%s/%s.dat", op1.srcname, op1.desname);
int fd = open(filename, O_RDONLY);
ltjr lt;
while (read(fd, <, sizeof(lt)))
{
op1.lt = lt;
memcpy(pack.msgBlock, &op1, sizeof(op1));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, sizeof(struct sockaddr_in));
usleep(4); // 延时一下,然客户端接收
}
op1.isOK = 1; // 告诉用户发完了
memcpy(pack.msgBlock, &op1, sizeof(op1));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, sizeof(struct sockaddr_in));
}
else
{
printf("\e[34m用户%s想要清空和%s的聊天记录\e[0m\n", op1.srcname, op1.desname);
char filename[128];
sprintf(filename, "./server_file/%s/%s.dat", op1.srcname, op1.desname);
int fd = open(filename, O_TRUNC); // 清空文件
close(fd);
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, sizeof(struct sockaddr_in));
}
}
```
9.传输文件
用户输入指定好友名字和需要传输的文件名字(包括路径)上传至服务器,服务器会暂时保存该文件,上传完后服务器会通知用户的好友。
服务器代码如下:
```
case 9:
{
package pack;
pack.msgType = CSFILE;
printf("\e[33m请输入你要发送的好友名:\e[0m");
chuanShou chs;
scanf("%s", chs.desname);
strcpy(chs.srcname, loginUserName);
chs.type = CHUAN;
printf("\e[33m请输入你要发送的文件名:\e[0m");
scanf("%s", chs.filename); // 把路径也传过去了,要去掉文件路径
int fd = open(chs.filename, O_RDONLY); // 打开文件
char *ch;
for (int i = 0; i < strlen(chs.filename); i++)
{
if (chs.filename[i] == '/')
{
ch = &chs.filename[i];
}
}
ch++; // 去掉文件的路径,只保留文件名
puts(ch);
if (fd < 0)
{
perror("open fail");
close(fd);
continue;
}
else
{
struct stat info;
stat(chs.filename, &info);
int leftSize = info.st_size; // 取出传输文件大小文件
strcpy(chs.filename, ch); // 之传输文件名,不包括路径
printf("%d\n", leftSize);
int sendSize = 0;
while (leftSize)
{
if (leftSize > 800)
{
chs.data.isOK = 0;
chs.data.bytes = 800;
}
else
{
chs.data.isOK = 1; // 告诉服务器这是最后一个文件块
chs.data.bytes = leftSize;
}
read(fd, chs.data.text, chs.data.bytes);
leftSize -= chs.data.bytes;
sendSize += chs.data.bytes;
memcpy(pack.msgBlock, &chs, sizeof(chs));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)addr, sizeof(struct sockaddr_in));
printf("已上传%d字节\n", sendSize);
usleep(6);
}
close(fd);
printf("\e[32m文件传输完成\e[0m\n");
}
sem_post(&sem1);
}
break;
```
10.下载文件
客户可以输入指点好友名字和文件名来下载好友给他发送的文件,客户从服务器下载该文件后,服务器会删除该文件,并提示发送文件的客户,”你的好友已经接收了你的文件”
客户端代码如下:
```
case 10:
{
package pack;
pack.msgType = CSFILE;
printf("\e[33m请输入你要下载文件的好友名:\e[0m");
chuanShou chs;
scanf("%s", chs.srcname);
strcpy(chs.desname, loginUserName);
chs.type = SHOU;
printf("\e[33m请输入你要接收的文件名:\e[0m");
scanf("%s", chs.filename); //
memset(&chs.data, 0, sizeof(chs.data));
// 告诉服务器你要下载什么文件
memcpy(pack.msgBlock, &chs, sizeof(chs));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)addr, sizeof(struct sockaddr_in));
// 开始下载文件
sem_post(&sem6);
}
break;
default:
sem_post(&sem1);
break;
}
```
下载文件和传输文件共用一个消息类型,因此他俩的接收线程代码共同如下:
```
else if (pack.msgType == CSFILE)
{
chuanShou chs = *(chuanShou *)pack.msgBlock;
if (chs.type == CHUAN)
{
printf("\e[33m你的好友%s给你发送了一个文件:%s\e[0m\n", chs.srcname, chs.filename);
continue;
}
else if (chs.type == SHOU)
{
// 发送方
if (chs.data.isOK == 2)
{
printf("\e[33m你的好友%s接收了你的文件:%s\e[0m\n", chs.desname, chs.filename);
continue;
}
else
{
// 接收方
sem_wait(&sem6);
int dir_mode = 0755; // 目录权限
char dirname[128];
sprintf(dirname, "./customer_file/%s", loginUserName);
if (mkdir(dirname, dir_mode) == -1)
{
if (errno != EEXIST) // 如果创建失败
{
perror("Failed to create directory A");
continue;
}
}
char clientfile[128];
sprintf(clientfile, "./customer_file/%s/%s_%s", loginUserName, chs.srcname, chs.filename); // 指定下载目录,这里我直接指定了一个目录
int fd = open(clientfile, O_CREAT | O_WRONLY | O_TRUNC, 0666);
int rcvsize = 0;
while (1)
{
if (write(fd, chs.data.text, chs.data.bytes) != chs.data.bytes)
{
perror("write wrong");
close(fd);
break;
}
rcvsize += chs.data.bytes;
printf("已下载%d个字节\n", rcvsize);
if (chs.data.isOK == 1)
{
printf("下载完毕\n");
close(fd);
break;
}
recvfrom(waddr.fd, &pack, sizeof(pack), 0, NULL, NULL);
chs = *(chuanShou *)pack.msgBlock;
}
sem_post(&sem1);
}
}
}
```
服务器端代码如下:
```
else if (pack.msgType == CSFILE)
{
chuanShou chs = *(chuanShou *)pack.msgBlock;
if (chs.type == CHUAN)
{
printf("\e[34m%s给%s发送了一个文件%s\e[0m\n", chs.srcname, chs.desname, chs.filename);
// 不检测是不是好友了,没有做离线文件传输了
char sysfile[128];
sprintf(sysfile, "./server_file/%s/%s_%s", chs.srcname, chs.desname, chs.filename); // 保存到服务器指定文件夹
// 给文件添加前缀,区分谁发的,发给谁,什么文件
int fd = open(sysfile, O_CREAT | O_WRONLY | O_TRUNC, 0666); // 一定要加上0666
// 如果客户端发送的文件名包含路径信息,服务器可能会尝试在错误的目录下创建文件。
if (fd < 0)
{
perror("open fail");
continue;
}
int rcvsize = 0;
while (1)
{
if (write(fd, chs.data.text, chs.data.bytes) != chs.data.bytes)
{
perror("write wrong");
close(fd);
break;
}
rcvsize += chs.data.bytes;
printf("已下载%d个字节\n", rcvsize);
if (chs.data.isOK == 1)
{
close(fd);
break;
}
recvfrom(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, &length);
chs = *(chuanShou *)pack.msgBlock;
}
printf("下载完毕\n");
// 告诉客户有人给你发了文件
for (int i = 0; i < users; i++)
{
if (!strcmp(loginUser[i].name, chs.desname))
{
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&loginUser[i].addr, sizeof(struct sockaddr_in));
}
}
}
else if (chs.type == SHOU)
{
printf("\e[34m%s开始接收%s的文件:%s\n\e[0m", chs.desname, chs.srcname, chs.filename);
char sysfile[256];
sprintf(sysfile, "./server_file/%s/%s_%s", chs.srcname, chs.desname, chs.filename); // 服务器读取之前保存的文件
// puts(sysfile);
int fd = open(sysfile, O_RDONLY);
if (fd < 0)
{
// 可以发送给用户,报错
perror("open fail");
close(fd);
continue;
}
else
{
struct stat info;
stat(sysfile, &info);
int leftSize = info.st_size; // 取出传输文件大小文件
printf("%d\n", leftSize);
int sendSize = 0;
while (leftSize)
{
if (leftSize > 800)
{
chs.data.isOK = 0;
chs.data.bytes = 800;
}
else
{
chs.data.isOK = 1; // 告诉客户这是最后一个文件块
chs.data.bytes = leftSize;
}
read(fd, chs.data.text, chs.data.bytes);
leftSize -= chs.data.bytes;
sendSize += chs.data.bytes;
memcpy(pack.msgBlock, &chs, sizeof(chs));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&clientAddr, sizeof(struct sockaddr_in));
printf("已发送%d字节\n", sendSize);
usleep(6);
}
close(fd);
printf("\e[32m文件传输完成\e[0m\n");
// 在服务器上面删除掉这个文件
remove(sysfile); // 用户下载了就删除它
for (int i = 0; i < users; i++)
{
if (!strcmp(loginUser[i].name, chs.srcname))
{
// 改标志位,区分接收方还是发送方
chs.data.isOK = 2; // 发送给发送方,提示你的好友已经接收了文件
memcpy(pack.msgBlock, &chs, sizeof(chs));
sendto(sd, &pack, sizeof(pack), 0, (struct sockaddr *)&loginUser[i].addr, sizeof(struct sockaddr_in));
}
}
}
}
}
```
不足与改进
不足
不足如下几点:
1.没有实现离线好友申请,离线用户接收不到好友申请
2.没有注册掉”ctrl+c“,用户强制退出时,他的登录信息仍然保存在服务器,服务器默认客户还在继续登录
3.没有实现同时处理多条好友申请,只能一条一条处理好友申请
4.整体结构有待提高,由于前面构思不够完善,考虑的不周到,随着项目的推进,构思逐渐完善,但前面写的已经占了大部分,不好修改。
改进
针对以上几点不足,提出改进想法:
1.发送好友申请的同时,服务器检测对方是否在线,如果不在线,服务器创建一个子线程,专门用来监测对方用户,一旦用户上限,服务器子线程就将好友申请提示信息发送给对方并结束子线程。
2.用signal函数注册掉ctrl+c信号,为其指定相应的处理退出函数,在登录用户数组删除掉它,并结束线程,防止强制退出的用户依然霸占着这个账号
3.将用户的好友申请消息保存在服务器中,一条一条处理好友申请消息
4.在写项目的前期一定要构思好整体框架,不要”顾头不顾腚“
备注
小白第一次把自己写的项目上传,希望大家多多指教,完整源代码我放在了gitee仓库,链接如下: https://gitee.com/wxx2024/program
2024年6月4日