一、项目简介:
本次项目以网盘为题目,设计一个基于C/C++语言开发的网盘系统。由Vitual Studio打造的一个网盘系统项目,后台数据库使用MySQL数据库开发而成。结合网上诸多的云网盘软件为设计为基础,自己设计的网盘系统。该系统可以注册用户,已有的用户可以直接登录进入网盘主界面,上传文件以及下载已有文件或者删除,可以对文件的获取链接请求,并且分享文件以及链接。
二、项目主体:
1、数据库设计:
我创建了网盘数据库,在该数据库中存在四个表用以存储数据:有文件表存储文件信息(file),用户表存储用户信息(user),用户文件表用于储存一个用户的文件(user_file),文件链接表用于储存文件及其文件链接(share_file)。
表1 file表示例
字段名 | 说明 | 类型 | 长度 | 可否为空 | 主键 |
f_id | 文件id | int | 4 | 否 | 主键 |
F_name | 文件名 | varchar | 20 | 否 |
|
F_uploadtime | 上传时间 | varchar | 20 | 是 |
|
F_size | 文件大小 | varchar | 100 | 是 |
|
F_path | 文件路径 | Varchar | 100 | 否 |
|
F_count | 文件应用数 | Int | 4 | 是 |
|
F_MD5 | 文件MD5值 | Int | 4 | 是 |
|
表2 user表示例
字段名 | 说明 | 类型 | 长度 | 可否为空 | 主键 |
U_id | 用户编号 | int | 4 | 否 | 主键 |
U_name | 用户名 | varchar | 20 | 否 |
|
U_password | 用户密码 | varchar | 20 | 是 |
|
表3 user_file表示例
字段名 | 说明 | 类型 | 长度 | 可否为空 | 主键 |
U_id | 用户编号 | int | 4 | 否 | 主键 |
F_id | 文件编号 | varchar | 20 | 否 | 主键 |
表4 share_file表示例
字段名 | 说明 | 类型 | 长度 | 可否为空 | 主键 |
U_id | 用户编号 | int | 4 | 否 | 主键 |
F_id | 文件编号 | varchar | 20 | 否 | 主键 |
S_link | 文件链接 | varchar | 20 | 是 |
|
2、功能实现:
(1)用户注册:
由界面提交注册信息,包括用户id,姓名,密码,向服务器端发送注册信息;服务器端接受处理,尝试向数据库插入注册信息,根据约束条件,如果成功插入,返回注册成功,创建用户对应的路径;否则注册失败。返回注册结果后,再在客户端接受并处理,弹窗显示注册结果。
void TCPKernel::RegisterRq(char *szbuf,SOCKET sock)
{
//注册请求包
STRU_REGISTER_RQ *psrr = (STRU_REGISTER_RQ*)szbuf;
//将用户信息写入到数据库里
char szsql[_DEF_SQLLEN_] = {0};
STRU_REGISTER_RS srr;
srr.m_ntype = _DEF_PRTOCOL_REGISTER_RS;
srr.m_szResult = _register_fail;
sprintf_s(szsql,"insert into user values(%lld,'%s','%s')",psrr->m_userid,
psrr->m_szName,psrr->m_szPassword);
if(m_sql.UpdateMySql(szsql))
{
srr.m_szResult = _register_success;
char szPath[MAX_PATH] = {0};
sprintf_s(szPath,MAX_PATH,"%s%lld",m_szSystemPath,psrr->m_userid);
CreateDirectoryA(szPath,NULL);
//int n = GetLastError();
}
m_pNet->SendData(sock,(char*)&srr,sizeof(srr));
}
(2)用户登录:
客户端界面提交登录用户id,密码等信息,向服务器端接收并处理,用户id查表,获取密码,如果能找到密码,并且密码一致,那么登录成功,否则登录失败。再返回客户端后,接收并处理登录结果,登录成功向窗口发送消息,否则弹窗提示登录失败。然后当用户登录后我设计了创建主界面窗口。
//登录请求
void TCPKernel::LoginRq(char *szbuf,SOCKET sock)
{
//登录请求包
STRU_LOGIN_RQ *pslr = (STRU_LOGIN_RQ*)szbuf;
char szsql[_DEF_SQLLEN_] = {0};
list<string> lststr;
STRU_LOGIN_RS slr;
slr.m_ntype = _DEF_PRTOCOL_LOGIN_RS;
slr.m_szResult = _login_fail;
sprintf_s(szsql,"select u_password from user where u_id =%lld;",pslr->m_userid);
//去数据库里面查询用户对应的密码,放入链表中
m_sql.SelectMySql(szsql,1,lststr);
if(lststr.size() >0)
{
//从链表中取出密码
string strPassword = lststr.front();
lststr.pop_front();
//比较密码是否相同
if(0 == strcmp(pslr->m_szPassword,strPassword.c_str()))
{
slr.m_szResult = _login_success;
}
}
m_pNet->SendData(sock,(char*)&slr,sizeof(slr));
}
(3)获取用户文件列表:
在客户端主界面窗口创建时,在初始化函数中向服务器提交请求获取文件列表,用户信息包括用户id,然后向服务器发送获取列表请求,服务器接收并处理,根据用户id 联表查询该用户上的所有上传过的文件,将文件的名字按照每50个一组的形式写入获取文件列表回复中,发送给客户端。客户端返回获取列表回复后发送窗口消息,消息有文件信息数组,文件个数。最后在客户端中处理窗口消息,循环向主界面窗口插入已有文件信息。
//获取用户文件信息列表
void TCPKernel::GetFileListRq(char *szbuf,SOCKET sock)
{
STRU_GETFILELIST_RQ *psgr = (STRU_GETFILELIST_RQ*)szbuf;
char szsql[_DEF_SQLLEN_] = {0};
list<string> lststr;
STRU_GETFILELIST_RS sgr;
sgr.m_ntype = _DEF_PRTOCOL_GETFILELIST_RS;
//根据userid 从数据库中取出file 信息
sprintf_s(szsql,"select f_name,f_uploadtime,f_size from user \
inner join user_file on user.u_id = user_file.u_id \
inner join file on user_file.f_id = file.f_id \
where user.u_id = %lld;",psgr->m_userid);
m_sql.SelectMySql(szsql,3,lststr);
int i = 0;
//遍历链表
while(lststr.size() >0)
{
string strFileName =lststr.front();
lststr.pop_front();
string strFileUpLoadTime =lststr.front();
lststr.pop_front();
string strFileSize =lststr.front();
lststr.pop_front();
sgr.m_aryFile[i].m_FileSize = _atoi64(strFileSize.c_str());
strcpy_s(sgr.m_aryFile[i].m_szFileName,_DEF_SIZE,strFileName.c_str());
strcpy_s(sgr.m_aryFile[i].m_szUpLoadTime,_DEF_SIZE,strFileUpLoadTime.c_str());
i++;
if(lststr.size() ==0 || i == _DEF_FILENUM)
{
//发送回复
sgr.m_nFileNum = i;
m_pNet->SendData(sock,(char*)&sgr,sizeof(sgr));
i = 0;
ZeroMemory(sgr.m_aryFile,sizeof(sgr.m_aryFile));
}
}
}
(4)上传文件:
这是实现网盘的最重要的一步,步骤如下:首先,在客户端上点击上传按钮准备上传文件,弹窗上选择要上传的文件,确认后,获取文件的文件名和路径,根据文件生成MD5。然后,向服务器发送上传文件头请求,请求内容包括文件的大小,文件MD5,上传时间,上传位置,同时记录上传文件信息,我用了一个映射存储文件信息和文件,用于后面的查找。然后,向服务器端发送文件头请求,服务器根据文件名和文件MD5,查看该文件是否存在,情况1:如果文件已经存在,那么查看用户id是否对应这个人,如果是的话,则提示文件已经传过,如果不是则秒传文件,(),情况2:如果文件不存在的话就可以正常传文件,根据用户id和文件名,创建文件,更新文件信息表,即数据库插入数据,向文件表中添加映射。然后,返回文件头请求,客户端接收并处理回复,进行,弹窗提示,向窗口插入消息提示文件传送成功。在上传新文件时开启一个线程完成内容的全部发送,线程的目的是为了打开文件读取文件,发送文件块,文件id到服务器,并将当前发送的大小粘贴到表格控件上面。最后,服务器接收文件拿到对应的文件信息结构体,从而拿到文件指针,向文件里面写传说内容。(循环进行),当文件大小与传送字节数一直时关闭文件,完成上传。同时在map中将文件信息节点删除。
//上传文件头请求(文件名,id,路径,md5)
void TCPKernel::UpLoadFileHeaderRq(char *szbuf,SOCKET sock)
{
//上传文件头
STRU_UPLOADFILEHEADER_RQ *psur = (STRU_UPLOADFILEHEADER_RQ*)szbuf;
char szsql[_DEF_SQLLEN_] = {0};
list<string> lststr;
STRU_UPLOADFILEHEADER_RS sur;
sur.m_ntype = _DEF_PRTOCOL_UPLOAD_FILEHEADER_RS;
strcpy_s(sur.m_szMD5,_DEF_SIZE,psur->m_szMD5);
sur.m_fileid = 0;
sprintf_s(szsql,"select file.f_id,u_id,f_count from user_file \
inner join file on user_file.f_id = file.f_id \
where f_name = '%s' and f_MD5 = '%s' ;",psur->m_szFileName,psur->m_szMD5);
m_sql.SelectMySql(szsql,3,lststr);
if(lststr.size() >0)
{
string strFileId = lststr.front();
lststr.pop_front();
string strUserId = lststr.front();
lststr.pop_front();
string strFileCount = lststr.front();
lststr.pop_front();
long long userid = _atoi64(strUserId.c_str());
if(psur->m_userid == userid)
{
//1.查看自己是否传过,
sur.m_szResult = _fileheader_uploaded;
//1.1如果自己传过,回复 已经上传过了
}
else
{
sur.m_szResult = _fileheader_uploadsuccess;
sur.m_fileid = _atoi64(strFileId.c_str());
//2.查看别人传没传过 如果别人传过,秒传成功
long long filecount = _atoi64(strFileCount.c_str());
//引用计数+1
sprintf_s(szsql,"update file set f_count = %lld where f_MD5 = '%s' and f_name = '%s'"
,++filecount,psur->m_szMD5,psur->m_szFileName);
m_sql.UpdateMySql(szsql);
//将文件与用户做映射
sprintf_s(szsql,"insert into user_file values(%lld,%lld)"
,psur->m_userid,_atoi64(strFileId.c_str()));
m_sql.UpdateMySql(szsql);
}
}
else
{
sur.m_szResult = _fileheader_continueupload;
FILE *pFile = NULL;
//3.否则 创建文件 回复 可以正常传
char szPath[MAX_PATH] = {0};
sprintf_s(szPath,MAX_PATH,"%s%lld/%s",m_szSystemPath,psur->m_userid,psur->m_szFileName);
fopen_s(&pFile,szPath,"wb");
//更新文件信息表
sprintf_s(szsql,"insert into file(f_name,f_uploadtime,f_size,f_path,f_count,f_MD5) values('%s','%s',%lld,'%s',1,'%s')"
,psur->m_szFileName,psur->m_szUpLoadTime,psur->m_FileSize,szPath,psur->m_szMD5);
m_sql.UpdateMySql(szsql);
//获取文件ID
sprintf_s(szsql,"select f_id from file where f_MD5 = '%s'",psur->m_szMD5);
m_sql.SelectMySql(szsql,1,lststr);
if(lststr.size() >0)
{
string strFileid = lststr.front();
lststr.pop_front();
sur.m_fileid = _atoi64(strFileid.c_str());
//更新用户映射文件表
sprintf_s(szsql,"insert into user_file values(%lld,%lld)"
,psur->m_userid,_atoi64(strFileid.c_str()));
m_sql.UpdateMySql(szsql);
// 将文件信息保存 (fileid pFile filesize userid)
STRU_FILEINFO* pInfo = new STRU_FILEINFO;
pInfo->m_userid = psur->m_userid;
pInfo->m_filesize = psur->m_FileSize;
pInfo->m_pFile = pFile;
pInfo->m_uploadFilePos = 0;
pInfo->m_fileid = sur.m_fileid;
strcpy_s(pInfo->m_szMD5,_DEF_SIZE,psur->m_szMD5);
m_mapFileidToFileInfo[sur.m_fileid] = pInfo;
}
}
m_pNet->SendData(sock,(char*)&sur,sizeof(sur));
}
//上传文件内容请求
void TCPKernel::UpLoadFileContentRq(char* szbuf , SOCKET sock)
{
STRU_UPLOADFILECONTENT_RQ* psur = (STRU_UPLOADFILECONTENT_RQ*)szbuf;
STRU_FILEINFO *pInfo = m_mapFileidToFileInfo[psur->m_fileid];
if(pInfo == NULL)
{
return;
}
//写入文件内容
int nRealWriteNum = fwrite(psur->m_szContent,sizeof(char),psur->m_nLen,pInfo->m_pFile);
if(nRealWriteNum > 0)
{
pInfo->m_uploadFilePos += nRealWriteNum;
if(pInfo->m_uploadFilePos == pInfo->m_filesize)
{
fclose(pInfo->m_pFile);
STRU_UPLOADFILECONTENT_RS sur;
sur.m_ntype = _DEF_PRTOCOL_FILECONTENT_RS;
sur.m_fileid = psur->m_fileid;
sur.m_szResult = 1;
m_pNet->SendData(sock,(char*)&sur,sizeof(sur));
auto ite = m_mapFileidToFileInfo.begin();
while(ite != m_mapFileidToFileInfo.end())
{
if(ite->first == psur->m_fileid)
{
delete pInfo;
pInfo = NULL;
ite = m_mapFileidToFileInfo.erase(ite);
break;
}
++ite;
}
}
}
}
(5)下载文件:
客户端在界面里面点击下载按钮,下载选中项。从表格里面获取文件信息,然后弹窗,选择保存路径,保存map,发送下载请求,服务器端接收处理下载请求,根据文件名和用户id,查表得到文件信息,查不到,返回下载失败,查到文件信息,存储在结构体FileInfo,存储到map。开启发送文件块线程,循环读取文件内容,发送文件块(ileid , 文件内容)向服务器发送下载请求,内容userid ,文件名客户端接收处理处理请求回复,创建map<fleid , Filelnfo>,打开文件指针处理下载文件块根据ileid ,在map中找到文件信息,然后拿到文件指针,向文件中写入当文件大小和文件下载字节数相等时,下载完毕,删除map节点提示下载完成,发送文件回复。
void TCPKernel::DownloadRq(char* szbuf , SOCKET sock)
{
//拆包
STRU_DOWNLOAD_RQ * psdr = (STRU_DOWNLOAD_RQ*)szbuf;
//定义RS结构体
STRU_DOWNLOAD_RS sdr;
sdr.m_szResult = _file_downloadrq_failed;
sdr.m_ntype = _DEF_PRTOCOL_DOWNLOAD_CONTINUEFILEHEADER_RS;
//查表
char szsql[_DEF_SQLLEN_] = {0};
list<string> lststr;
sprintf_s(szsql,"select file.f_id ,file.f_MD5,file.f_path,file.f_size from file\
inner join user_file on file.f_id = user_file.f_id\
and user_file.u_id = %lld and file.f_name = '%s';",psdr->m_userid,psdr->m_szFileName);
DownloadFileInfo * pInfo = 0;
if(m_sql.SelectMySql(szsql , 4 ,lststr))
{
if(lststr.size() > 0)
{
//查到了
sdr.m_szResult = _file_downloadrq_success;
string strField , strMD5 , strPath , strSize;
strField = lststr.front();
lststr.pop_front();
strMD5 = lststr.front();
lststr.pop_front();
strPath = lststr.front();
lststr.pop_front();
strSize = lststr.front();
lststr.pop_front();
strcpy_s(sdr.m_szMD5 , _DEF_SIZE , strMD5.c_str());
sdr.m_fileid = _atoi64(strField.c_str());
strcpy_s(sdr.m_szFileName , _DEF_SIZE , psdr->m_szFileName);
//存储到mapstrFile = lststr.front();
//存储到map
pInfo = new DownloadFileInfo;
pInfo->m_fileid = sdr.m_fileid;
pInfo->m_filesize = _atoi64(strSize.c_str());
//打开文件:
FILE * pFile = NULL;
fopen_s(&pFile, strPath.c_str() , "rb");
pInfo->m_pFile = pFile;
pInfo->m_sock = sock;
pInfo->m_FilePos = 0;
pInfo->m_userid = psdr->m_userid;
m_mapFileIDToDownLoadFileInfo[sdr.m_fileid] = pInfo;
// todo FileInfo map
}
}
m_pNet->SendData(sock , (char*)&sdr , sizeof(sdr));
//开启发送文件块线程
if(sdr.m_szResult = _file_downloadrq_success)
{
//todo
_beginthreadex(0,0,&TCPKernel::ThreadPro , (void*)pInfo,0,0);
}
}
(6)删除文件:
删除较为两端删除 : 客户端从列表删除信息 , 服务器用户文件删除列表里的层次: 第一, 引用计数不到0 , 删除映射关系 第二, 引用计数为0 , 删除表中的文件信息. 同时, 你要考虑要不要删除磁盘里面的文件客户端发起, 文件删除, 删除控件上选中的文件内容:用户id , 文件名发送删除请求 服务器服务器处理 ,根据用户id , 文件名, 查表找文件找不到 删除失败找到了, 先删除映射关系 , 然后将文件引用计数-1引用计数-1之后, 如果为0, 删除信息从文件表里面删除成功写删除回复, 内容 结果, 文件名返回回复,客户端处理根据结果, 弹窗提示删除成功, 根据文件名, 从表格里面删除对应项。
void TCPKernel::DeleteFileRq(char *szbuf,SOCKET sock)
{
STRU_DELETEFILE_RQ *psdr = (STRU_DELETEFILE_RQ *)szbuf;
STRU_DELETEFILE_RS sdr;
sdr.m_ntype = _DEF_PROTOCOL_DELETEFILE_RS;
sdr.m_szResult = deletefile_failed;
strcpy_s(sdr.m_szFileName,_DEF_SIZE,psdr->m_szFileName);
//查找 找到userid 的对应信息
char szsql[_DEF_SQLLEN_] = {0};
list<string> lststr;
sprintf_s(szsql,"select file.f_id from file inner join user_file on user_file.f_id = file.f_id and file_file.u_id = %lld and file.f_name = '%s'",psdr->m_userid,psdr->m_szFileName);
if(m_sql.SelectMySql(szsql,1,lststr))
{
if(lststr.size() > 0)
{
sdr.m_szResult = deletefile_success;
//删除文件映射
string strFileID = lststr.front();
lststr.pop_front();
long long IFileID = _atoi64(strFileID.c_str());
ZeroMemory(szsql,sizeof(szsql));
sprintf_s(szsql,"delete from user_file where u_id = %lld and f_id = %lld;",psdr->m_userid,IFileID);
m_sql.UpdateMySql(szsql);
//引用计数-1
ZeroMemory(szsql,sizeof(szsql));
list<string>lstFile;
sprintf_s(szsql,"select f_count from file where f_id = %lld;",IFileID);
m_sql.UpdateMySql(szsql);
//是否归0,删除文件信息
long long nCount = _atoi64(lstFile.front().c_str());
lstFile.pop_front();
if(nCount == 0)
{
ZeroMemory(szsql,sizeof(szsql));
sprintf_s(szsql,"delete from file where f_id = %lld;",IFileID);
m_sql.UpdateMySql(szsql);
}
}
}
}
(7)续传文件:
在发生异常的情况下, 客户端退出, 重新进入.。从配置文件里面读取正在上传的文件信息( 文件名, 路径, 大小, 上传时间) , 存在map<MD5, FileInfo>。 开始上传把信息写到配置文件里, 当下载完成, 就把这一条信息从配置里删除。用户点击续传, 发送续传请求(文件名,md5 , userid).发送续传请求,服务器端收到续传请求, 根据文件名,md5 ,userid , 找到pFile ,关闭文件指针map<fileid , FileInfo>续传是文件传到自己的路径下.找到对应没传完的文件, 读取当前文件的字节数, 返回给客户端( MD5 , 文件名 , fileid , 已接受字节数)返回续传回复根据MD5 , map中, 找FileInfo , 写入文件位置, 然后开启线程打开文件, 跳转到已接受字节位置, 开始发送文件块( fileid , 文件内容)发送文件块 后面同上传处理.
void TCPKernel::UploadContinueRq(char *szbuf,SOCKET sock)
{
STRU_UPLOAD_CONTINUE_RQ *psur = (STRU_UPLOAD_CONTINUE_RQ *)szbuf;
psur->m_szFileName;
psur->m_szMD5;
psur->m_userid;
//根据成员,拿到文件上传字节数,文件id
//如果服务器异常,查表:
auto ite = m_mapFileidToFileInfo.begin();
while( ite != m_mapFileidToFileInfo.end() )
{
if(ite->second->m_userid == psur->m_userid && strcmp(psur->m_szMD5,ite->second->m_szMD5) == 0)
{
fclose( ite->second->m_pFile);
break;
}
ite++;
}
if(ite == m_mapFileidToFileInfo.end() ) return;//没找到
//如果有,根据用户路径,拿文件,得size返回结果
//续传一定是在用户得路径下面传
char szPath[MAX_PATH] = {0};
sprintf_s(szPath,MAX_PATH,"%s%lld%s",m_szSystemPath,psur->m_userid,psur->m_szFileName);
FILE * pFile = 0;
fopen_s(&pFile,szPath,"rb");
_fseeki64(pFile,0,SEEK_END);
long long length = _ftelli64(pFile); //读取文件字节数
fclose(pFile);
fopen_s(&pFile,szPath,"ab");
_fseeki64(pFile,length,SEEK_SET); //到文件尾
ite->second->m_pFile = pFile;
ite->second->m_filesize = length;
//回复:
STRU_UPLOAD_CONTINUE_RS sur;
sur.m_ntype = _DEF_PRTOCOL_UPLOAD_CONTINUEFILEHEADER_RS;
sur.m_fileid = ite->first;
sur.m_nPos = length;
strcpy_s(sur.m_szFileName,_DEF_SIZE,psur->m_szFileName);
strcpy_s(sur.m_szMD5,_DEF_SIZE,psur->m_szMD5);
m_pNet->SendData(sock , (char *)&sur,sizeof(sur));
}