目录
前言
本篇主要总结编写小度网盘项目详细的过程,实现了TCP通讯,客户端与服务器端的交互:注册、登录、获取服务器的文件列表,下载文件、上传文件和查询用户上传下载操作历史记录等功能。
应用了宏定义、结构体、函数封装调用、sqlite数据库、多线程并发服务器和TCP/IP通信协议、客户端与服务器的交互以及文件操作命令;是C语言文件IO、进程线程、网络编程的综合知识应用。
提示:以下是本篇文章正文内容,下面案例可供参考
一、开发环境、客户端与服务器的源代码
1.1 开发环境
开发软件:VMware Workstation 16 Player 配置环境:Ubuntu虚拟 Linux系统
1.2 客户端源代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sqlite3.h>
#define R 1 //注册 register
#define L 2 //登录 login
#define LIST 3 //列表
#define GET 4 //下载
#define PUT 5 //上传
#define Q 6//退出 quit
#define H 7//查询历史
//注意 服务和客户端两端的请求类型宏定义 要一样
typedef struct
{
int type;//标志位 请求的类型
char name[30];//用户名
char passwd[256];//用户密码
char filename[50];//文件名
char filedata[100];//文件内容
int len;//每次读取文件内容实际读取的块数
}MSG;
//函数声明
void menu2();
MSG s = { 0 };//与服务器交互接收和发送的结构体
int sockfd;//全局
//客户端注册请求
void doRegister()
{
//总思想:先给结构体赋值,然后将结构体发送给服务器
//先装东西,再发送
//装东西
s.type = R;//代表的是注册请求
printf("请输入注册的用户名:\n\n");
scanf("%s",s.name);
printf("请输入要注册的密码:\n\n");
scanf("%s",s.passwd);
//发送给服务器
send(sockfd, &s, sizeof(s),0);
//等待服务器的应答
recv(sockfd, &s, sizeof(s),0);
printf("%s\n",s.passwd);//将是否注册成功的结果打印输出
}
//客户端登录请求
void doLogin()
{
s.type = L;//代表的是登录请求
printf("请输入登录的用户名:\n\n");
scanf("%s",s.name);
printf("请输入要登录的密码:\n\n");
scanf("%s",s.passwd);
//发送给服务器
send(sockfd, &s, sizeof(s),0);
//等待服务器的应答
recv(sockfd, &s, sizeof(s),0);
printf("%s\n",s.passwd);//将是否登录成功的结果打印输出
//如果登录成功,进入菜单界面2
if(s.type == 0)
{
//进入菜单界面二
menu2();
}
}
//客户端获取文件列表请求
void doGetFileList()
{
s.type = LIST;//代表获取下载列表请求
send(sockfd, &s, sizeof(s),0);
int count = 0;
while(1)
{
recv(sockfd, &s, sizeof(s), 0);
if(s.type == -3)
{
printf("\n#######获取文件列表完毕!!!#####\n");
break;
}
printf("%s ", s.filename);
count++;
if(count == 5)
printf("\n");
}
}
//客户端下载文件的请求
void doGetFile()
{
s.type = GET;//下载文件请求类型
printf("请输入要下载的文件名字:\n");
scanf("%s",s.filename);
send(sockfd, &s, sizeof(s), 0);
recv(sockfd, &s, sizeof(s), 0);//阻塞
if(s.type == -1)
{
printf("%s\n",s.filedata);
return;//提前结束函数,证明下载的文件不存在
}
//如果程序能够走到这,说明文件存在
FILE* fp = fopen(s.filename, "w");
if(fp == NULL)
{
perror("fopen failed");
exit(-1);
}
//多加一次写入文件,是因为服务器端,下载文件存在,
//直接就开始读取发送了,被上面的recv接收了一次,s.filedata是第一次发送过来的有效数据
fwrite(s.filedata,1,s.len, fp);
while(1)
{
recv(sockfd, &s, sizeof(s), 0);//接收一次,写入文件一次
if(s.type == -2)
{
printf("下载%s文件完毕!!!\n",s.filename);
break;
}
fwrite(s.filedata,1,s.len, fp);//写入的时候,用s.len每次实际读取的块数
//每次实际读取多少块,就写入多少块
}
fclose(fp);
}
//客户端上传文件请求函数
//注意上传的是客户端当前目录下的文件
void doPutFile()
{
FILE* fp = NULL;
s.type = PUT;//上传文件
printf("请输入要上传的文件名字:\n");
scanf("%s",s.filename);
//打开文件,然后循环读取,并发送
fp = fopen(s.filename,"r");
if(fp == NULL)
{
perror("fopen failed");
exit(-1);
}
while((s.len=fread(s.filedata,1,sizeof(s.filedata),fp)) > 0)
{
send(sockfd, &s, sizeof(s), 0);
}
//额外在多发送一次,告诉服务器,全部内容上传完毕
s.type = -1;
send(sockfd, &s, sizeof(s), 0);
printf("%s上传完毕!!\n",s.filename);
fclose(fp);
}
//客户端获取历史信息请求
void doGetHistory()
{
s.type = H;//获取历史信息
send(sockfd, &s, sizeof(s), 0);
while(1)
{
recv(sockfd,&s,sizeof(s),0);
if(s.type == -1)
{
break;
}
printf("%s\n",s.filedata);
}
}
void menu2()
{
int n;
while(1)
{
printf("\n/");
printf("\n*****************************************************\n");
printf("* 欢 迎 使 用 小 度 网 盘 *");
printf("\n*****************************************************\n");
printf("\n* 1.列表 2.下载文件 3.上传文件 4.历史记录 5.退出*\n");
printf("\n*****************************************************\n");
printf("/\n");
printf("\n请 输 入 您 的 选 择:\n\n");
scanf("%d",&n);
switch(n)
{
case 1:
doGetFileList();//客户端端获取文件表请求
break;
case 2:
doGetFile();//客户端下载文件请求
break;
case 3:
doPutFile();//客户端上传文件请求
break;
case 4:
doGetHistory();//客户端获取历史信息请求
break;
case 5:
printf("即将结束程序,欢迎下次使用!!\n");
sleep(2);
exit(-1);
break;
}
}
}
//界面1
void menu1()
{
int n;
while(1)
{
printf("\n****************************************\n");
printf("* 小 度 网 盘 *");
printf("\n****************************************\n");
printf("\n* 1.注册 2.登录 3.退出 *\n");
printf("\n****************************************\n");
printf("\n请 输 入 您 的 选 择:\n\n");
scanf("%d",&n);
switch(n)
{
case 1:
doRegister();//客户端发送注册请求
break;
case 2:
doLogin();//客户端发送登录请求
break;
case 3:
printf("即将结束程序,欢迎下次使用!!\n");
sleep(2);
exit(-1);
break;
}
}
}
int main(int argc, const char *argv[])
{
if(argc != 3)
{
printf("忘记传递参数了!! ./client IP Port\n");
exit(-1);
}
//1.创建一个流式套接字
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
perror("socket failed");
exit(-1);
}
//2.连接服务器前,提前知道服务器的IP地址和端口号
struct sockaddr_in serveraddr = { 0 };
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
//3.连接服务器
connect(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
//4.进入菜单界面一
menu1();
return 0;
}
1.3 服务器源代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <dirent.h>
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
#include <sys/shm.h>
#include <sys/msg.h>
#include<netinet/in.h>
#include<sys/socket.h>
#include<sqlite3.h>
#define R 1 //注册 register
#define L 2 //登录 login
#define LIST 3 //列表
#define GET 4 //下载
#define PUT 5 //上传
#define Q 6//退出 quit
#define H 7//查询历史
//注意 服务和客户端两端的请求类型宏定义 要一样
typedef struct
{
int type;//标志位 请求的类型
char name[30];//用户名
char passwd[256];//用户密码
char filename[50];//文件名
char filedata[100];//文件内容
int len;//每次读取文件内容实际读取的块数
}MSG;
sqlite3* db = NULL;//数据库句柄定义为全局,任何函数有了句柄,就可以直接操作数据库
char sql[200] = { 0 };//用来保存数据库操作语句
char dirname[100] = { 0 };//用来保存设置的下载目录
void mySqlite3Exec(char* sql)
{
int ret;
char* errmsg = NULL;
ret = sqlite3_exec(db, sql, NULL, NULL, &errmsg);
if(ret == 0)
{
printf("sqlite3_exec sucessful!!\n");
}
else
{
printf("sqlite3_exec failed:%s\n",errmsg);
}
}
int mySqlite3GetTbale(char* sql)
{
int i;
char* errmsg = NULL;
char** resultp = NULL;
int row,column;
int ret = sqlite3_get_table(db, sql, &resultp, &row, &column, &errmsg);
if(ret == 0)
{
return row;//将数据库中的查询结果返回
//row 代表查找的时候,满足条件信息的行数
//row == 0 不存在这个信息,没找到
//row != 0 存在这个信息,查找到了
}
else
{
printf("sqlite3_get_table failed:%s\n",errmsg);
exit(-1);//结束程序
}
}
//处理客户端注册请求的函数
void doRegister(MSG* ps, int newsockfd)
{
int ret;
//总思想:打开数据库,取数据库查找用户名是否已经存在,给客户端一个应答
sprintf(sql, "select * from user_info where username = '%s';",ps->name);
ret = mySqlite3GetTbale(sql);
if(ret == 0)//说明数据库中不存在该用户名,可以将用户名和秘密保存到数据库中
{
sprintf(sql,"insert into user_info values('%s','%s');",ps->name,ps->passwd);
mySqlite3Exec(sql);
//告诉客户一声,注册成功
ps->type = 0;//表达成功
sprintf(ps->passwd,"################恭喜你,注册成功!!!###############\n");
send(newsockfd, ps, sizeof(MSG), 0);
}
else//说明用户名已经被注册了
{
ps->type = -1;//表达失败
sprintf(ps->passwd,"################非常抱歉,注册失败!!!###############\n");
send(newsockfd, ps, sizeof(MSG), 0);
}
}
//处理客户端登录请求的函数
void doLogin(MSG* ps, int newsockfd)
{
//1.去数据库里面比对,用户名和密码是否正确
sprintf(sql, "select * from user_info where username = '%s' and passwd = '%s';",ps->name,ps->passwd);
int ret = mySqlite3GetTbale(sql);
if(ret == 0)//说明row的值是0,数据库中没有该用户名和密码
{
ps->type = -1;//-1表达登录失败!!
sprintf(ps->passwd,"################非常抱歉,登录失败!!!###############\n");
send(newsockfd, ps, sizeof(MSG), 0);
}
else//说明row的值不为0,数据库中有该用户名和密码,登录成功
{
ps->type = 0;//0表达登录成功
sprintf(ps->passwd,"################恭喜你,登录成功!!!###############\n");
send(newsockfd, ps, sizeof(MSG), 0);
}
}
//处理客户端获取文件列表请求函数
void doGetFileList(MSG* ps, int newsockfd)
{
//1.打开目录文件,循环读取每一个文件的名字,发送给客户端
// /home/linux/aaaa
struct dirent* ep = NULL;
DIR* dp = opendir(dirname);
if(dp == NULL)
{
perror("opendir failed");
exit(-1);
}
while(1)
{
ep = readdir(dp);
if(ep == NULL)//全部读取完了
{
break;
}
if(ep->d_name[0] == '.')//去掉隐藏文件
continue;
sprintf(ps->filename,"%s",ep->d_name);
send(newsockfd, ps, sizeof(MSG), 0);
}
//额外在多发送一次,告诉客户端,已经全部发送完毕
ps->type = -3;//-3代表全部发送完毕
send(newsockfd, ps, sizeof(MSG), 0);
closedir(dp);
}
//获取系统时间
void getSystemTime(char* temp)
{
time_t raw_time;
time(&raw_time);//得到秒数
//将秒数转换为中文时间
struct tm* tp = localtime(&raw_time);
sprintf(temp, "%04d-%02d-%02d %02d:%02d:%02d",tp->tm_year+1900,tp->tm_mon+1,tp->tm_mday,
tp->tm_hour,tp->tm_min,tp->tm_sec);
}
//处理客户端下载文件请求
void doGetFile(MSG* ps, int newsockfd)
{
char nowtime[100] = { 0 };
//用户名 系统时间 干了那些事
getSystemTime(nowtime);
char pathname[100] = { 0 };//用来保存要下载文件的绝对路径
//思想:文件IO将发送过来的文件名字打开,循环读取,并发送给客户端
//"haha.c" 当前目录没有haha.c
// "/home/linux/haha.c"
sprintf(pathname,"%s/%s",dirname,ps->filename);
FILE* fp = fopen(pathname,"r");
if(fp == NULL)
{
//提示客户端,下载的文件不存在!!!
ps->type = -1;//用来表达下载的文件不存在
sprintf(ps->filedata,"非常抱歉,您下载的文件不存在!!!");
send(newsockfd,ps, sizeof(MSG), 0);
return;//提前结束函数
}
//程序能够走到这,说明下载的文件存在
//循环读取,读取一次,给客户端发送一次,每次实际读取的块数
//保存到结构体里面的len中
while((ps->len=fread(ps->filedata,1,sizeof(ps->filedata),fp)) > 0)
{
send(newsockfd, ps, sizeof(MSG), 0);
}
//上面的while循环结束,再发送一次,告诉客户端所有的文件内容发送完毕
ps->type = -2;
send(newsockfd, ps, sizeof(MSG), 0);
//将用户下载文件的系统时间保存到数据库中
sprintf(sql,"insert into user_his values('%s','%s','get %s');",ps->name,nowtime,ps->filename);
mySqlite3Exec(sql);
//关闭文件
fclose(fp);
}
//处理客户端上传文件请求
void doPutFile(MSG* ps, int newsockfd)
{
char pathname[200] = { 0 };
char nowtime[100] = { 0 };
//用户名 系统时间 干了那些事
getSystemTime(nowtime);
//上传文件,是保存在共享目录的
sprintf(pathname,"%s/%s",dirname,ps->filename);
FILE* fp = fopen(pathname,"w");
if(fp == NULL)
{
perror("fopen failed");
exit(-1);
}
//在进入下面的while(1)循环之前,先写入文件一次,是因为
//客户端的第一次读取出来的内容,发送后,被do_client函数中的
//recv接收到了,所以先把第一次接收写入文件
fwrite(ps->filedata,1,ps->len,fp);
while(1)
{
recv(newsockfd,ps,sizeof(MSG),0);
if(ps->type == -1)
{
break;
}
fwrite(ps->filedata,1,ps->len,fp);
}
//将用户上传文件的系统时间保存到数据库中
sprintf(sql,"insert into user_his values('%s','%s','put %s');",ps->name,nowtime,ps->filename);
mySqlite3Exec(sql);
fclose(fp);
}
//处理客户端查询历史信息请求函数
void doGetHistory(MSG* ps, int newsockfd)
{
int i;
char* errmsg = NULL;
char** resultp = NULL;
int row,column;
sprintf(sql,"select * from user_his where username = '%s';",ps->name);
int ret = sqlite3_get_table(db, sql, &resultp, &row, &column, &errmsg);
printf("row is %d column is %d\n",row,column);
if(ret == 0)
{
//思想:将所有历史信息,一行一行的发送
for(i = 0; i < (row+1)*column; i+=column)
{
sprintf(ps->filedata,"%s %s %s",resultp[i],resultp[i+1],resultp[i+2]);
send(newsockfd,ps,sizeof(MSG),0);
}
//循环接收后,额外在多发一次,告诉客户端已经全部发送完毕
ps->type = -1;//用-1代表全部发送完毕
send(newsockfd,ps,sizeof(MSG),0);
}
else
{
printf("sqlite3_get_table failed:%s\n",errmsg);
exit(-1);//结束程序
}
}
//服务器器一直接收客户端请求的线程函数do_client
void* do_client(void* p)
{
MSG s = { 0 };//用来保存接收的数据
int ret;
int newsockfd = *((int*)p);
//一直接收客户端的请求
while(1)
{
ret = recv(newsockfd, &s, sizeof(s), 0);
if(ret > 0)
{
//打印就是为了调试程序
printf("type:%d %s\n",s.type,s.name);
switch(s.type)
{
case R://处理注册请求
doRegister(&s, newsockfd);
break;
case L://处理登录请求
doLogin(&s, newsockfd);
break;
case LIST://处理列表请求
doGetFileList(&s, newsockfd);
break;
case GET://处理下载请求
doGetFile(&s, newsockfd);
break;
case PUT://处理上传请求
doPutFile(&s, newsockfd);
break;
case H://处理查询历史信息
doGetHistory(&s, newsockfd);
break;
}
}
else
{
printf("客户端newsockfd:%d断开!!\n",newsockfd);
close(newsockfd);
pthread_exit(NULL);
}
}
}
int main(int argc, const char *argv[])
{
pthread_t id;
int newsockfd;
if(argc != 3)
{
printf("忘记传递参数了!! ./server port(6666) dirname(/home/linux/aaaaa)\n");
exit(-1);
}
//从命令行参数得到下载的目录
sprintf(dirname,"%s",argv[2]);
//在mian函数中打开数据库,只打开一次就可以
if(sqlite3_open("./my.db",&db) != 0)
{
perror("sqlite3 failed");
exit(-1);
}
printf("sqlite3_open sucessful!!\n");
//1.创建一个流式套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
perror("socket failed");
exit(-1);
}
//2.绑定自己的IP地址和端口号
struct sockaddr_in myaddr = { 0 };
myaddr.sin_family = AF_INET;
myaddr.sin_port = htons(atoi(argv[1]));
myaddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sockfd, (struct sockaddr*)&myaddr, sizeof(myaddr)) == -1)
{
perror("bind failed");
exit(-1);
}
printf("bind ok!!\n");
//3.设置监听
listen(sockfd, 5);
//4.并发服务器
while(1)
{
newsockfd = accept(sockfd, NULL, NULL);
printf("client connect sucessful, newsockfd is %d\n",newsockfd);
pthread_create(&id, NULL, do_client, &newsockfd);
}
//5.关闭套接字
close(sockfd);
return 0;
}
二、注册功能
2.1 客户端注册请求
1.创建一个流式套接字SOCK_STREAM 并且用sockfd 接住socket 返回值 进行判断是否成功
2.连接服务器 提前知道服务器的ip地址和端口号; 定义一个结构体 给severaddr.sin_family sin_port sin_addr.s_addr 赋上已经知道的服务器IP地址和端口号
3.connet 连接服务器
4.进入界面菜单1 ,进入菜单后根据选项进行注册、登录、退出
客户端注册请求:定义一个结构体 装上用户名、密码和请求类型 发送给服务器;等待服务器应答是否成功
#include "my.h"
#define R 1 //注册 register
#define L 2 //登录 login
#define LIST 3 //列表
#define GET 4 //下载
#define PUT 5 //上传
#define Q 6//退出 quit
#define H 7//查询历史
//注意 服务和客户端两端的请求类型宏定义 要一样
typedef struct
{
int type;//标志位 请求的类型
char name[30];//用户名
char passwd[256];//用户密码
char filename[50];//文件名
char filedata[100];//文件内容
int len;//每次读取文件内容实际读取的块数
}MSG;
MSG s = { 0 };//与服务器交互接收和发送的结构体
int sockfd;//全局
//客户端注册请求
void doRegister()
{
//总思想:先给结构体赋值,然后将结构体发送给服务器 先装东西,再发送
//装东西:更改请求类型,输入注册的用户名和密码 发东西:发送给服务器
s.type = R;
printf("请输入注册的用户名:\n\n");
scanf("%s",s.name);
printf("请输入要注册的密码:\n\n");
scanf("%s",s.passwd);
send(sockfd, &s, sizeof(s),0);
//等待服务器的应答
recv(sockfd, &s, sizeof(s),0);
printf("%s\n",s.passwd);
}
//界面1
void menu1()
{
int n;
while(1) //一直等待做出选择,switch语句跳转
{
printf("\n****************************************\n");
printf("* 小 度 网 盘 *");
printf("\n****************************************\n");
printf("\n* 1.注册 2.登录 3.退出 *\n");
printf("\n****************************************\n");
printf("\n请 输 入 您 的 选 择:\n\n");
scanf("%d",&n);
switch(n)
{
case 1:
doRegister();//客户端发送注册请求
break;
case 2:
break;
case 3:
break;
}
}
}
int main(int argc, const char *argv[])
{
//1.创建一个流式套接字,对socket返回值进行判断,创建是否成功
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
perror("socket failed");
exit(-1);
}
//2.连接服务器前,提前知道服务器的IP地址和端口号,定义一个结构体来保存
struct sockaddr_in serveraddr = { 0 };
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(6666);
serveraddr.sin_addr.s_addr = inet_addr("192.168.110.157");
//3.连接服务器
connect(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr));
//4.进入菜单界面一
menu1();
return 0;
}
2.2 服务器处理客户端注册请求功能
1.创建一个流式套接字SOCK_STREAM 并且用sockfd 接住socket 返回值 进行判断是否成功
2.绑定自己主机的IP地址、端口号 ifconfig bind()
3.设置监听 listen
4.并发服务器,阻塞等待接收,客户端来一个我就创建一个子线程去执行函数 accept pthread_create
注册功能具体实现:定义一个与客户端类型相同的结构体来存储客户端发来的信息 while一直接收客户端的消息并存储在结构体中
根据数据类型来判断客户端的请求,以此跳转到需要的函数去实现相应功能
服务器注册功能:打开数据库,查询要创建的用户名是否存在,判断可否创建用户 sqlite3_exec sqlite3_get_table
1.sprintf 格式化出数据库操作语句 因为要多次重复调用数据库,所以把数据库的插入、查询封装成函数 数据库句柄 sqlite3*db 定义一个char sql[200]数组保存操作语句
2.更改请求类型,将判断结果保存在password数组中返回给客户端 send
#include "my.h"
#define R 1 //注册 register
#define L 2 //登录 login
#define LIST 3 //列表
#define GET 4 //下载
#define PUT 5 //上传
#define Q 6//退出 quit
#define H 7//查询历史
//注意 服务和客户端两端的请求类型宏定义 要一样
typedef struct
{
int type;//标志位 请求的类型
char name[30];//用户名
char passwd[256];//用户密码
char filename[50];//文件名
char filedata[100];//文件内容
int len;//每次读取文件内容实际读取的块数
}MSG;
sqlite3* db = NULL;//数据库句柄定义为全局,任何函数有了句柄,就可以直接操作数据库
char sql[200] = { 0 };//用来保存数据库操作语句
void mySqlite3Exec(char* sql)
{
int ret;
char* errmsg = NULL;
ret = sqlite3_exec(db, sql, NULL, NULL, &errmsg);
if(ret == 0)
{
printf("sqlite3_exec sucessful!!\n");
}
else
{
printf("sqlite3_exec failed:%s\n",errmsg);
}
}
int mySqlite3GetTbale(char* sql)
{
int i;
char* errmsg = NULL;
char** resultp = NULL;
int row,column;
int ret = sqlite3_get_table(db, sql, &resultp, &row, &column, &errmsg);
if(ret == 0)
{
return row;//将数据库中的查询结果返回
}
else
{
printf("sqlite3_get_table failed:%s\n",errmsg);
exit(-1);//结束程序
}
}
//处理客户端注册请求的函数
void doRegister(MSG* ps, int newsockfd)
{
int ret;
//总思想:打开数据库,取数据库查找用户名是否已经存在,给客户端一个应答
sprintf(sql, "select * from user_info where username = '%s';",ps->name);
ret = mySqlite3GetTbale(sql);
if(ret == 0)//说明数据库中不存在该用户名,可以将用户名和秘密保存到数据库中
{
sprintf(sql,"insert into user_info values('%s','%s');",ps->name,ps->passwd);
mySqlite3Exec(sql);
//告诉客户一声,注册成功
ps->type = 0;//表达成功
sprintf(ps->passwd,"################恭喜你,注册成功!!!###############\n");
send(newsockfd, ps, sizeof(MSG), 0);
}
else//说明用户名已经被注册了
{
ps->type = -1;//表达失败
sprintf(ps->passwd,"################非常抱歉,注册失败!!!###############\n");
send(newsockfd, ps, sizeof(MSG), 0);
}
}
//服务器器一直接收客户端请求的线程函数do_client
void* do_client(void* p)
{
MSG s = { 0 };//用来保存接收的数据
int ret;
int newsockfd = *((int*)p);
//一直接收客户端的请求
while(1)
{
ret = recv(newsockfd, &s, sizeof(s), 0);
if(ret > 0)
{
//打印就是为了调试程序
printf("type:%d %s %s %s %s\n",s.type,s.name,s.passwd,s.filename,s.filedata);
switch(s.type)
{
case R://处理注册请求
doRegister(&s, newsockfd);
break;
}
}
else
{
printf("客户端newsockfd:%d断开!!\n",newsockfd);
close(newsockfd);
pthread_exit(NULL);
}
}
}
int main(int argc, const char *argv[])
{
pthread_t id;
int newsockfd;
//在mian函数中打开数据库,只打开一次就可以
if(sqlite3_open("./my.db",&db) != 0)
{
perror("sqlite3 failed");
exit(-1);
}
printf("sqlite3_open sucessful!!\n");
//1.创建一个流式套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
perror("socket failed");
exit(-1);
}
//2.绑定自己的IP地址和端口号
struct sockaddr_in myaddr = { 0 };
myaddr.sin_family = AF_INET;
myaddr.sin_port = htons(6666);
myaddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sockfd, (struct sockaddr*)&myaddr, sizeof(myaddr)) == -1)
{
perror("bind failed");
close(sockfd);
exit(-1);
}
printf("bind ok!!\n");
//3.设置监听
listen(sockfd, 5);
//4.并发服务器
while(1)
{
newsockfd = accept(sockfd, NULL, NULL);
printf("client connect sucessful, newsockfd is %d\n",newsockfd);
pthread_create(&id, NULL, do_client, &newsockfd);
}
//5.关闭套接字
close(sockfd);
return 0;
}
三、登录功能
3.1 客户端登录请求
客户端登录请求:给结构体装上用户名、密码和请求类型,发送给客户端;等待客户端应答,根据请求类型判断是否成功;成功就进入菜单页面2
//客户端登录请求
void doLogin()
{
s.type = L;//更改请求类型,代表的是登录请求
printf("请输入登录的用户名:\n\n");
scanf("%s",s.name);
printf("请输入要登录的密码:\n\n");
scanf("%s",s.passwd);
//发送给服务器
send(sockfd, &s, sizeof(s),0);
//等待服务器的应答
recv(sockfd, &s, sizeof(s),0);
printf("%s\n",s.passwd);//将是否登录成功的结果打印输出
//如果登录成功,进入菜单界面2 服务器端更改了请求类型,如果与我们条件判断的相同,则进入菜单页面2
if(s.type == 0)
{
//进入菜单界面二
menu2();
}
}
void menu2()
{
int n;
while(1)
{
printf("\n/");
printf("\n*****************************************************\n");
printf("* 欢 迎 使 用 小 度 网 盘 *");
printf("\n*****************************************************\n");
printf("\n* 1.列表 2.下载文件 3.上传文件 4.历史记录 5.退出*\n");
printf("\n*****************************************************\n");
printf("/\n");
printf("\n请 输 入 您 的 选 择:\n\n");
scanf("%d",&n);
switch(n)
{
case 1:
break;
case 2:
break;
case 3:
break;
case 4:
break;
case 5:
break;
}
}
}
3.2 服务器处理客户端登录请求功能
服务器登录功能:打开数据库,将客户端发送过来的用户名和密码与数据库中保持的进行对比,重新设置请求类型,将回复信息装进结构体发送给客户端
//处理客户端登录请求的函数
void doLogin(MSG* ps, int newsockfd)
{
//1.格式化需要的字符串保存至sql数组中,去数据库里面比对,用户名和密码是否正确
sprintf(sql, "select * from user_info where username = '%s' and passwd = '%s';",ps->name,ps->passwd);
int ret = mySqlite3GetTbale(sql);
if(ret == 0)//说明row的值是0,数据库中没有该用户名和密码
{
ps->type = -1;//-1表达登录失败!!
sprintf(ps->passwd,"################非常抱歉,登录失败!!!###############\n");
send(newsockfd, ps, sizeof(MSG), 0);
}
else//说明row的值不为0,数据库中有该用户名和密码,登录成功
{
ps->type = 0;//0表达登录成功
sprintf(ps->passwd,"################恭喜你,登录成功!!!###############\n");
send(newsockfd, ps, sizeof(MSG), 0);
}
}
//服务器器一直接收客户端请求的线程函数do_client
void* do_client(void* p)
{
MSG s = { 0 };//用来保存接收的数据
int ret;
int newsockfd = *((int*)p);
//一直接收客户端的请求
while(1)
{
ret = recv(newsockfd, &s, sizeof(s), 0);
if(ret > 0)
{
//打印就是为了调试程序
printf("type:%d %s %s %s %s\n",s.type,s.name,s.passwd,s.filename,s.filedata);
switch(s.type)
{
case R://处理注册请求
doRegister(&s, newsockfd);
break;
case L://处理登录请求
doLogin(&s, newsockfd);
break;
}
}
else
{
printf("客户端newsockfd:%d断开!!\n",newsockfd);
close(newsockfd);
pthread_exit(NULL);
}
}
}
四、获取文件列表功能
4.1 客户端获取服务器目录文件列表功能
客户端获取列表请求:设置请求类型 发送结构体; while循环recv接收服务器应答 并打印显示
//客户端下载文件列表请求
void doGetFileList()
{
s.type = LIST;//代表获取下载列表请求
send(sockfd, &s, sizeof(s),0);
int count = 0;
while(1)//循环接收并打印显示在终端
{
recv(sockfd, &s, sizeof(s), 0);
if(s.type == -3)
{
printf("\n#######获取文件列表完毕!!!#####\n");
break;
}
printf("%s ", s.filename);
count++;
if(count == 5)
printf("\n");
}
}
void menu2()
{
int n;
while(1)
{
printf("\n/");
printf("\n*****************************************************\n");
printf("* 欢 迎 使 用 小 度 网 盘 *");
printf("\n*****************************************************\n");
printf("\n* 1.列表 2.下载文件 3.上传文件 4.历史记录 5.退出*\n");
printf("\n*****************************************************\n");
printf("/\n");
printf("\n请 输 入 您 的 选 择:\n\n");
scanf("%d",&n);
switch(n)
{
case 1:
doGetFileList();//客户端端下载文件表请求
break;
case 2:
break;
case 3:
break;
case 4:
break;
case 5:
break;
}
}
}
4.2 处理客户端获取服务器列表文件功能
服务器获取列表功能:全局定义一个数组保存命令行传参的目录,打开目录文件,循环读取并发送给客户端;跳过隐藏文件 设置请求类型并额外发送一次 告诉客户端发送完毕
struct dirent*ep=NULL DIR *db=opendir(dirname) 格式化下载目录sprintf(dirname,"%s",argv[2]) 命令行传参
ep = readdir(dp) 打开对应的目录文件
sprintf(ps->filename,"%s",ep->d_name)
send(newsockfd, ps, sizeof(MSG), 0);
//处理客户端获取文件列表请求函数
void doGetFileList(MSG* ps, int newsockfd)
{
//1.打开目录文件,循环读取每一个文件的名字,发送给客户端
// /home/linux/aaaa
struct dirent* ep = NULL;
DIR* dp = opendir(dirname);//dirname里保存的是即将打开目录文件的绝对路径
if(dp == NULL)
{
perror("opendir failed");
exit(-1);
}
while(1)
{
ep = readdir(dp);
if(ep == NULL)//全部读取完了
{
break;
}
if(ep->d_name[0] == '.')//去掉隐藏文件
continue;
sprintf(ps->filename,"%s",ep->d_name);
send(newsockfd, ps, sizeof(MSG), 0);
}
//额外在多发送一次,告诉客户端,已经全部发送完毕
ps->type = -3;//-3代表全部发送完毕
send(newsockfd, ps, sizeof(MSG), 0);
closedir(dp);
}
int main(int argc, const char *argv[])
{
pthread_t id;
int newsockfd;
if(argc != 3)
{
printf("忘记传递参数了!! ./server port(6666) dirname(/home/linux/aaaaa)\n");
exit(-1);
}
//从命令行参数得到下载的目录
sprintf(dirname,"%s",argv[2]);
//在mian函数中打开数据库,只打开一次就可以
if(sqlite3_open("./my.db",&db) != 0)
{
perror("sqlite3 failed");
exit(-1);
}
printf("sqlite3_open sucessful!!\n");
//1.创建一个流式套接字
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd == -1)
{
perror("socket failed");
exit(-1);
}
//2.绑定自己的IP地址和端口号
struct sockaddr_in myaddr = { 0 };
myaddr.sin_family = AF_INET;
myaddr.sin_port = htons(atoi(argv[1]));
myaddr.sin_addr.s_addr = htonl(INADDR_ANY);
if(bind(sockfd, (struct sockaddr*)&myaddr, sizeof(myaddr)) == -1)
{
perror("bind failed");
exit(-1);
}
printf("bind ok!!\n");
//3.设置监听
listen(sockfd, 5);
//4.并发服务器
while(1)
{
newsockfd = accept(sockfd, NULL, NULL);
printf("client connect sucessful, newsockfd is %d\n",newsockfd);
pthread_create(&id, NULL, do_client, &newsockfd);
}
//5.关闭套接字
close(sockfd);
return 0;
}
五、文件下载功能
5.1 客户端下载文件请求
客户端下载文件请求:设置请求类型 标准输入获取下载文件名,发送结构体 等待服务器应答 根据类型判断是否存在
存在的话,标准库fopen 打开文件,使用文件流指针,对文件进行读写
//客户端下载文件的请求
void doGetFile()
{
s.type = GET;//下载文件请求类型
printf("请输入要下载的文件名字:\n");
scanf("%s",s.filename);
send(sockfd, &s, sizeof(s), 0);
recv(sockfd, &s, sizeof(s), 0);//阻塞
if(s.type == -1)
{
printf("%s\n",s.filedata);
return;//提前结束函数,证明下载的文件不存在
}
//如果程序能够走到这,说明文件存在
FILE* fp = fopen(s.filename, "w");
if(fp == NULL)
{
perror("fopen failed");
exit(-1);
}
//多加一次写入文件,是因为服务器端,下载文件存在,
//直接就开始读取发送了,被上面的recv接收了一次,s.filedata是第一次发送过来的有效数据
fwrite(s.filedata,1,s.len, fp);
while(1)
{
recv(sockfd, &s, sizeof(s), 0);//接收一次,写入文件一次
if(s.type == -2)
{
printf("下载%s文件完毕!!!\n",s.filename);
break;
}
fwrite(s.filedata,1,s.len, fp);//写入的时候,用s.len每次实际读取的块数
//每次实际读取多少块,就写入多少块
}
fclose(fp);
}
5.2 服务器处理客户端下载文件请求功能
服务器下载文件功能:格式化出文件的绝对路径,保存到数组pathname中 使用fopen打开文件并对返回值进行判断;
如果不存在,重新设置请求类型,将返回信息装进结构体发送给客户端
如果存在,循环读取,读一次发一次,直到文件到达末尾,也重新设置请求类型,告诉客户端读取文件完毕 关闭文件流指针
//处理客户端下载文件请求
void doGetFile(MSG* ps, int newsockfd)
{
char pathname[100] = { 0 };//用来保存要下载文件的绝对路径
//思想:文件IO将发送过来的文件名字打开,循环读取,并发送给客户端
//"haha.c" 当前目录没有haha.c
// "/home/linux/haha.c"
sprintf(pathname,"%s/%s",dirname,ps->filename);
FILE* fp = fopen(pathname,"r");
if(fp == NULL)
{
//提示客户端,下载的文件不存在!!!
ps->type = -1;//用来表达下载的文件不存在
sprintf(ps->filedata,"非常抱歉,您下载的文件不存在!!!");
send(newsockfd,ps, sizeof(MSG), 0);
return;//提前结束函数
}
//程序能够走到这,说明下载的文件存在
//循环读取,读取一次,给客户端发送一次,每次实际读取的块数
//保存到结构体里面的len中
while((ps->len=fread(ps->filedata,1,sizeof(ps->filedata),fp)) > 0)
{
send(newsockfd, ps, sizeof(MSG), 0);
}
//上面的while循环结束,再发送一次,告诉客户端所有的文件内容发送完毕
ps->type = -2;
send(newsockfd, ps, sizeof(MSG), 0);
//关闭文件
fclose(fp);
}
六、文件上传功能
6.1 客户端上传文件请求
客户端上传文件请求:设置请求类型,scanf() 获取文件名字,使用fopen打开文件,while循环读取,读取一次发一次,直到文件尾巴,在重新设置请求类型
多发一次,告诉服务器上传完毕,关闭文件流指针
//客户端上传文件请求函数
//注意上传的是客户端当前目录下的文件
void doPutFile()
{
FILE* fp = NULL;
s.type = PUT;//上传文件
printf("请输入要上传的文件名字:\n");
scanf("%s",s.filename);
//打开文件,然后循环读取,并发送
fp = fopen(s.filename,"r");
if(fp == NULL)
{
perror("fopen failed");
exit(-1);
}
while((s.len=fread(s.filedata,1,sizeof(s.filedata),fp)) > 0)
{
send(sockfd, &s, sizeof(s), 0);
}
//额外在多发送一次,告诉服务器,全部内容上传完毕
s.type = -1;
send(sockfd, &s, sizeof(s), 0);
printf("%s上传完毕!!\n",s.filename);
fclose(fp);
}
void menu2()
{
int n;
while(1)
{
printf("\n/");
printf("\n*****************************************************\n");
printf("* 欢 迎 使 用 小 度 网 盘 *");
printf("\n*****************************************************\n");
printf("\n* 1.列表 2.下载文件 3.上传文件 4.历史记录 5.退出*\n");
printf("\n*****************************************************\n");
printf("/\n");
printf("\n请 输 入 您 的 选 择:\n\n");
scanf("%d",&n);
switch(n)
{
case 1:
doGetFileList();//客户端端获取文件表请求
break;
case 2:
doGetFile();//客户端下载文件请求
break;
case 3:
doPutFile();//客户端上传文件请求
break;
case 4:
break;
case 5:
printf("即将结束程序,欢迎下次使用!!\n");
sleep(2);
exit(-1);
break;
}
}
}
6.2 服务器处理客户端上传文件请求功能
服务器上传文件功能:格式化文件绝对路径,fopen打开文件 while循环写入上传文件中 如何关闭文件流指针
注意:在进入循环读取写入之前,要先写入一次,一维客户端读一次读取的被do_client接收了,所以要先写入一次
//处理客户端上传文件请求
void doPutFile(MSG* ps, int newsockfd)
{
char pathname[200] = { 0 };
char nowtime[100] = { 0 };
//用户名 系统时间 干了那些事
getSystemTime(nowtime);
sprintf(pathname,"%s/%s",dirname,ps->filename);
FILE* fp = fopen(pathname,"w");
if(fp == NULL)
{
perror("fopen failed");
exit(-1);
}
//在进入下面的while(1)循环之前,先写入文件一次,是因为
//客户端的第一次读取出来的内容,发送后,被do_client函数中的
//recv接收到了,所以先把第一次接收写入文件
fwrite(ps->filedata,1,ps->len,fp);
while(1)
{
recv(newsockfd,ps,sizeof(MSG),0);
if(ps->type == -1)
{
break;
}
fwrite(ps->filedata,1,ps->len,fp);
}
//将用户上传文件的系统时间保存到数据库中
sprintf(sql,"insert into user_his values('%s','%s','put %s');",ps->name,nowtime,ps->filename);
mySqlite3Exec(sql);
fclose(fp);
}
//服务器器一直接收客户端请求的线程函数do_client
void* do_client(void* p)
{
MSG s = { 0 };//用来保存接收的数据
int ret;
int newsockfd = *((int*)p);
//一直接收客户端的请求
while(1)
{
ret = recv(newsockfd, &s, sizeof(s), 0);
if(ret > 0)
{
//打印就是为了调试程序
printf("type:%d %s\n",s.type,s.name);
switch(s.type)
{
case R://处理注册请求
doRegister(&s, newsockfd);
break;
case L://处理登录请求
doLogin(&s, newsockfd);
break;
case LIST://处理列表请求
doGetFileList(&s, newsockfd);
break;
case GET://处理下载请求
doGetFile(&s, newsockfd);
break;
case PUT://处理上传请求
doPutFile(&s, newsockfd);
break;
}
}
else
{
printf("客户端newsockfd:%d断开!!\n",newsockfd);
close(newsockfd);
pthread_exit(NULL);
}
}
}
七、查询文件处理历史记录功能
7.1 客户端查询文件处理历史请求
客户端浏览历史记录请求:设置请求类型,发送给服务器 等待应答 即数据库调用sql,将历史记录发给客户端
//客户端获取历史信息请求
void doGetHistory()
{
s.type = H;//获取历史信息
send(sockfd, &s, sizeof(s), 0);
while(1)
{
recv(sockfd,&s,sizeof(s),0);
if(s.type == -1)
{
break;
}
printf("%s\n",s.filedata);
}
}
void menu2()
{
int n;
while(1)
{
printf("\n/");
printf("\n*****************************************************\n");
printf("* 欢 迎 使 用 小 度 网 盘 *");
printf("\n*****************************************************\n");
printf("\n* 1.列表 2.下载文件 3.上传文件 4.历史记录 5.退出*\n");
printf("\n*****************************************************\n");
printf("/\n");
printf("\n请 输 入 您 的 选 择:\n\n");
scanf("%d",&n);
switch(n)
{
case 1:
doGetFileList();//客户端端获取文件表请求
break;
case 2:
doGetFile();//客户端下载文件请求
break;
case 3:
doPutFile();//客户端上传文件请求
break;
case 4:
doGetHistory();//客户端获取历史信息请求
break;
case 5:
printf("即将结束程序,欢迎下次使用!!\n");
sleep(2);
exit(-1);
break;
}
}
}
7.2 服务器处理客户端查询历史记录功能
服务器调用历史记录功能:将获取系统时间封装成一个函数,将1970到系统时间的秒数,转换成中文时间 time_t raw_time;time(&raw_time);struct tm* tp = localtime(&raw_time); 将年月日、时分秒格式化成字符串存储在一个数组中
在上传和下载中调用封装的时间函数,将上传和下载时的用户名 系统时间 干了啥 格式化成字符串存储到sql数据库中
//获取系统时间
void getSystemTime(char* temp)
{
time_t raw_time;
time(&raw_time);//得到秒数
//将秒数转换为中文时间
struct tm* tp = localtime(&raw_time);
sprintf(temp, "%04d-%02d-%02d %02d:%02d:%02d",tp->tm_year+1900,tp->tm_mon+1,tp->tm_mday,
tp->tm_hour,tp->tm_min,tp->tm_sec);
}
//处理客户端下载文件请求
void doGetFile(MSG* ps, int newsockfd)
{
char nowtime[100] = { 0 };
//用户名 系统时间 干了那些事
getSystemTime(nowtime);
char pathname[100] = { 0 };//用来保存要下载文件的绝对路径
//思想:文件IO将发送过来的文件名字打开,循环读取,并发送给客户端
//"haha.c" 当前目录没有haha.c
// "/home/linux/haha.c"
sprintf(pathname,"%s/%s",dirname,ps->filename);
FILE* fp = fopen(pathname,"r");
if(fp == NULL)
{
//提示客户端,下载的文件不存在!!!
ps->type = -1;//用来表达下载的文件不存在
sprintf(ps->filedata,"非常抱歉,您下载的文件不存在!!!");
send(newsockfd,ps, sizeof(MSG), 0);
return;//提前结束函数
}
//程序能够走到这,说明下载的文件存在
//循环读取,读取一次,给客户端发送一次,每次实际读取的块数
//保存到结构体里面的len中
while((ps->len=fread(ps->filedata,1,sizeof(ps->filedata),fp)) > 0)
{
send(newsockfd, ps, sizeof(MSG), 0);
}
//上面的while循环结束,再发送一次,告诉客户端所有的文件内容发送完毕
ps->type = -2;
send(newsockfd, ps, sizeof(MSG), 0);
//将用户下载文件的系统时间保存到数据库中
sprintf(sql,"insert into user_his values('%s','%s','get %s');",ps->name,nowtime,ps->filename);
mySqlite3Exec(sql);
//关闭文件
fclose(fp);
}
//处理客户端上传文件请求
void doPutFile(MSG* ps, int newsockfd)
{
char pathname[200] = { 0 };
char nowtime[100] = { 0 };
//用户名 系统时间 干了那些事
getSystemTime(nowtime);
//上传文件,是保存在共享目录的
sprintf(pathname,"%s/%s",dirname,ps->filename);
FILE* fp = fopen(pathname,"w");
if(fp == NULL)
{
perror("fopen failed");
exit(-1);
}
//在进入下面的while(1)循环之前,先写入文件一次,是因为
//客户端的第一次读取出来的内容,发送后,被do_client函数中的
//recv接收到了,所以先把第一次接收写入文件
fwrite(ps->filedata,1,ps->len,fp);
while(1)
{
recv(newsockfd,ps,sizeof(MSG),0);
if(ps->type == -1)
{
break;
}
fwrite(ps->filedata,1,ps->len,fp);
}
//将用户上传文件的系统时间保存到数据库中
sprintf(sql,"insert into user_his values('%s','%s','put %s');",ps->name,nowtime,ps->filename);
mySqlite3Exec(sql);
fclose(fp);
}
//处理客户端查询历史信息请求函数
void doGetHistory(MSG* ps, int newsockfd)
{
int i;
char* errmsg = NULL;
char** resultp = NULL;
int row,column;
sprintf(sql,"select * from user_his where username = '%s';",ps->name);
int ret = sqlite3_get_table(db, sql, &resultp, &row, &column, &errmsg);
printf("row is %d column is %d\n",row,column);
if(ret == 0)
{
//思想:将所有历史信息,一行一行的发送
for(i = 0; i < (row+1)*column; i+=column)
{
sprintf(ps->filedata,"%s %s %s",resultp[i],resultp[i+1],resultp[i+2]);
send(newsockfd,ps,sizeof(MSG),0);
}
//循环接收后,额外在多发一次,告诉客户端已经全部发送完毕
ps->type = -1;//用-1代表全部发送完毕
send(newsockfd,ps,sizeof(MSG),0);
}
else
{
printf("sqlite3_get_table failed:%s\n",errmsg);
exit(-1);//结束程序
}
}
//服务器器一直接收客户端请求的线程函数do_client
void* do_client(void* p)
{
MSG s = { 0 };//用来保存接收的数据
int ret;
int newsockfd = *((int*)p);
//一直接收客户端的请求
while(1)
{
ret = recv(newsockfd, &s, sizeof(s), 0);
if(ret > 0)
{
//打印就是为了调试程序
printf("type:%d %s\n",s.type,s.name);
switch(s.type)
{
case R://处理注册请求
doRegister(&s, newsockfd);
break;
case L://处理登录请求
doLogin(&s, newsockfd);
break;
case LIST://处理列表请求
doGetFileList(&s, newsockfd);
break;
case GET://处理下载请求
doGetFile(&s, newsockfd);
break;
case PUT://处理上传请求
doPutFile(&s, newsockfd);
break;
case H://处理查询历史信息
doGetHistory(&s, newsockfd);
break;
}
}
else
{
printf("客户端newsockfd:%d断开!!\n",newsockfd);
close(newsockfd);
pthread_exit(NULL);
}
}
}
八、退出功能
8.1 客户端退出请求
客户端退出请求:设置请求类型,发送给服务器 等待应答,然后退出结束循环
void menu2()
{
int n;
while(1)
{
printf("\n/");
printf("\n*****************************************************\n");
printf("* 欢 迎 使 用 小 度 网 盘 *");
printf("\n*****************************************************\n");
printf("\n* 1.列表 2.下载文件 3.上传文件 4.历史记录 5.退出*\n");
printf("\n*****************************************************\n");
printf("/\n");
printf("\n请 输 入 您 的 选 择:\n\n");
scanf("%d",&n);
switch(n)
{
case 1:
doGetFileList();//客户端端获取文件表请求
break;
case 2:
doGetFile();//客户端下载文件请求
break;
case 3:
doPutFile();//客户端上传文件请求
break;
case 4:
doGetHistory();//客户端获取历史信息请求
break;
case 5:
printf("即将结束程序,欢迎下次使用!!\n");
sleep(2);
exit(-1);
break;
}
}
}
8.2 服务器处理客户端退出请求功能
服务器处理客户端退出请求功能:根据客户端的请求类型,执行相应的函数,更改请求类型,将要退出的客户端的名字、密码装进结构体,根据文件描述符send给客户端。
void* do_client(void* p)
{
MSG s = { 0 };//用来保存接收的数据
int ret;
int newsockfd = *((int*)p);
//一直接收客户端的请求
while(1)
{
ret = recv(newsockfd, &s, sizeof(s), 0);
if(ret > 0)
{
//打印就是为了调试程序
printf("type:%d %s\n",s.type,s.name);
switch(s.type)
{
case R://处理注册请求
doRegister(&s, newsockfd);
break;
case L://处理登录请求
doLogin(&s, newsockfd);
break;
case LIST://处理列表请求
doGetFileList(&s, newsockfd);
break;
case GET://处理下载请求
doGetFile(&s, newsockfd);
break;
case PUT://处理上传请求
doPutFile(&s, newsockfd);
break;
case H://处理查询历史信息
doGetHistory(&s, newsockfd);
break;
}
}
else
{
printf("客户端newsockfd:%d断开!!\n",newsockfd);
close(newsockfd);
pthread_exit(NULL);
}
}
}
九、项目过程遇到的问题
9.1 问题一:sqlite3数据库的多次复用
1、数据库操作语句格式化成我们需要的字符串
2、将数据库的注册插入命令与数据库查询函数封装成两个函数
9.2 问题二:获取文件列表的路径问题
获取服务器端目录文件列表,得先格式一个字符串,来保存即将打开的目录文件的绝对路径
/home/linux/linux_c/aaaa
9.3 问题三:上传文件得先写入问题
上传文件得先写入一次,因为客户端读一次发送过来的,被do_client()函数中的recv()接收了,所以得把读一次接收的写入文件
总结
本篇总结了小度网盘项目详细实现过程,实现了TCP通讯,客户端与服务器端的交互:注册、登录、获取服务器的文件列表,下载文件、上传文件和查询用户上传下载操作历史记录等功能。
应用了宏定义、结构体、函数封装调用、sqlite数据库、多线程并发服务器和TCP/IP通信协议、客户端与服务器的交互以及文件操作命令;是C语言文件IO、进程线程、网络编程的综合知识应用。