小度网盘项目

目录

前言

一、开发环境、客户端与服务器的源代码

1.1 开发环境

1.2 客户端源代码

1.3 服务器源代码

二、注册功能

2.1 客户端注册请求

2.2 服务器处理客户端注册请求功能

三、登录功能

3.1 客户端登录请求

3.2 服务器处理客户端登录请求功能

四、获取文件列表功能

4.1 客户端获取服务器目录文件列表功能

4.2 处理客户端获取服务器列表文件功能

五、文件下载功能

5.1 客户端下载文件请求

5.2 服务器处理客户端下载文件请求功能

六、文件上传功能

6.1 客户端上传文件请求

6.2 服务器处理客户端上传文件请求功能

七、查询文件处理历史记录功能

7.1 客户端查询文件处理历史请求

7.2 服务器处理客户端查询历史记录功能

八、退出功能

8.1 客户端退出请求

8.2 服务器处理客户端退出请求功能

九、项目过程遇到的问题

9.1 问题一:sqlite3数据库的多次复用

9.2 问题二:获取文件列表的路径问题

9.3 问题三:上传文件得先写入问题

总结


 


前言

本篇主要总结编写小度网盘项目详细的过程,实现了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、进程线程、网络编程的综合知识应用。

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值