项目描述

  • 基于Qt的桌面网盘系统,实现了网盘的基础功能,分为好友操作和文件操作两部分,包括注册登录、好友系统、私聊群聊、文件操作、分享文件等功能

  • 分为客户端和服务器端,两个大的功能模块是好友操作和文件操作,用户的个人信息和好友信息是存储在MySQL数据库中,文件操作的文件是存储在服务器端

  • 设计通信协议,用于接收客户端不同的请求,比如添加好友、删除好友、上传文件、下载文件等,通信协议中使用弹性结构体,不同类型的消息申请不同的空间大小,节省内存,提高效率

  • 服务器端使用面向对象编程自定义mytcpsocket类用于和客户端的网络通信,以及与数据库操作、文件操作。

  • 客户端在用户注册时对用户的注册密码进行哈希加密,提高用户隐私安全性

采用C/S架构,数据库存储用户信息,磁盘存储用户文件

学习参考视频

技术栈

主要编程语言:C++

开发平台:Qt Windows 6.7.2

设计特性:单例设计模式、网络通信

基础知识

多线程

TCP Socket网络编程

MySQL数据库

面相对象编程

数据库搭建

通过MySQL实现了数据库的搭建工作

用户信息表:

用户好友表:

项目目标和实现

配置文件

资源文件

将服务器IP和PORT信息填入配置文件中

将配置文件作为资源文件添加到资源文件中

程序运行时加载配置文件中的数据

服务器实现

加载配置文件

连接数据库

接收客户端的连接

客户端实现

客户端连接服务器

通信协议的设计

弹性结构体

这种设计方法可以根据传输的不同的数据块大小来分配不同大小的空间

设计原理:结构体最后一个成员为 int caData[];

通讯协议设计

数据收发

客户端连接服务器

接收客户端的连接

服务器通过一个TcpServer监听及接收客户端的连接,然后与每一个客户端都会形成一个新的QTcpSocket来进行数据交互

服务器中,始终存在一个MyTcpServer监听着端口,接收客户端的连接建立,然后对每个客户端都会创建一个MyTcpSocket实现数据传输(也需要按照协议格式,所以protocol代码需要拷贝过来一份)

MyTcpServer通过一个List来存储所有连接的客户端的Socket

登录注册注销退出

数据库操作

登录注册注销请求

登录注册注销回复

数据库操作

  1. 定义数据库操作类

  2. 将数据库操作类定义为单例

  3. 数据库相应操作

数据库连接:

// 数据库连接
void DBOperate::init()
{
m_db.setHostName("localhost"); // 数据库服务器IP
m_db.setUserName("root"); // 数据库用户名
m_db.setPassword("123456"); // 数据库密码
m_db.setDatabaseName("networkdiskdb"); // 数据库名
if(m_db.open()) // 数据库是否打开成功
{
QSqlQuery query;
query.exec("select * from userInfo");
while(query.next())
{
QString data = QString("%1, %2, %3, %4").arg(query.value(0).toString()).arg(query.value(1).toString())
.arg(query.value(2).toString()).arg(query.value(3).toString());
qDebug() << data;
}
}
else
{
QMessageBox::critical(NULL, "数据库打开", "数据库打开失败");
}
}

登录注册注销请求

规定消息类型

在protocol.h中通过枚举方式定义消息格式

// 枚举方式,定义消息类型
enum ENUM_MSG_TYPE
{
ENUM_MSG_TYPE_MIN = 0,

ENUM_MSG_TYPE_REGIST_REQUEST, // 注册请求
ENUM_MSG_TYPE_REGIST_RESPOND, // 注册回复

ENUM_MSG_TYPE_LOGIN_REQUEST, // 登录请求
ENUM_MSG_TYPE_LOGIN_RESPOND, // 登录回复

// 好友操作
ENUM_MSG_TYPE_ONLINE_USERS_REQUEST, // 所有在线用户请求
ENUM_MSG_TYPE_ONLINE_USERS_RESPOND, // 所有在线用户响应

ENUM_MSG_TYPE_SEARCH_USER_REQUEST, // 查找用户请求
ENUM_MSG_TYPE_SEARCH_USER_RESPOND, // 查找用户响应

ENUM_MSG_TYPE_ADD_FRIEND_REQUEST, // 添加好友请求
ENUM_MSG_TYPE_ADD_FRIEND_RESPOND, // 添加好友响应

ENUM_MSG_TYPE_ADD_FRIEND_AGREE, // 被添加好友消息回复同意
ENUM_MSG_TYPE_ADD_FRIEND_REJECT, // 被添加好友消息回复拒绝

ENUM_MSG_TYPE_FLSUH_FRIEND_REQUEST, // 刷新好友请求
ENUM_MSG_TYPE_FLUSH_FRIEND_RESPOND, // 刷新好友响应

ENUM_MSG_TYPE_DELETE_FRIEND_REQUEST, // 删除好友请求
ENUM_MSG_TYPE_DELETE_FRIEND_RESPOND, // 删除好友响应

ENUM_MSG_TYPE_PRIVATE_CHAT_REQUEST, // 私聊请求
ENUM_MSG_TYPE_PRIVATE_CHAT_RESPOND, // 私聊回复

ENUM_MSG_TYPE_GROUP_CHAT_REQUEST, // 群聊请求
ENUM_MSG_TYPE_GROUP_CHAT_RESPOND, // 群聊回复

// 文件操作
ENUM_MSG_TYPE_CREATE_DIR_REQUEST, // 新建文件夹请求
ENUM_MSG_TYPE_CREATE_DIR_RESPOND, // 新建文件夹回复

ENUM_MSG_TYPE_FLUSH_DIR_REQUEST, // 刷新文件夹请求
ENUM_MSG_TYPE_FLUSH_DIR_RESPOND, // 刷新文件夹回复

ENUM_MSG_TYPE_DELETE_FILE_REQUEST, // 刷新文件夹请求
ENUM_MSG_TYPE_DELETE_FILE_RESPOND, // 刷新文件夹回复

ENUM_MSG_TYPE_RENAME_FILE_REQUEST, // 重命名文件夹请求
ENUM_MSG_TYPE_RENAME_FILE_RESPOND, // 重命名文件夹回复

ENUM_MSG_TYPE_ENTRY_DIR_REQUEST, // 进入文件夹请求
ENUM_MSG_TYPE_ENTRY_DIR_RESPOND, // 进入文件夹回复

ENUM_MSG_TYPE_PRE_DIR_REQUEST, // 上一文件夹请求
ENUM_MSG_TYPE_PRE_DIR_RESPOND, // 上一文件夹回复

ENUM_MSG_TYPE_UPLOAD_FILE_REQUEST, // 上传文件请求
ENUM_MSG_TYPE_UPLOAD_FILE_RESPOND, // 上传文件回复

ENUM_MSG_TYPE_DOWNLOAD_FILE_REQUEST, // 下载文件请求
ENUM_MSG_TYPE_DOWNLOAD_FILE_RESPOND, // 下载文件响应

ENUM_MSG_TYPE_MOVE_FILE_REQUEST, // 移动文件请求
ENUM_MSG_TYPE_MOVE_FILE_RESPOND, // 移动文件响应

ENUM_MSG_TYPE_SHARE_FILE_REQUEST, // 移动文件请求
ENUM_MSG_TYPE_SHARE_FILE_RESPOND, // 移动文件响应
ENUM_MSG_TYPE_SHARE_FILE_NOTE, // 移动文件提示
ENUM_MSG_TYPE_SHARE_FILE_NOTE_RESPOND, // 移动文件提示响应

ENUM_MSG_TYPE_MAX = 0x00ffffff, // uint最大值 0xffffffff
};

界面设计

在客户端设计登录页面,并对所有按钮添加点击事件的转到槽

注册,用户名唯一,防止重复注册

服务器端需要实现接收注册信息,然后判断数据库中该用户名是否已经存在(这个判断由于数据库中name设置的是unique,所以会自动判断),存在则返回注册失败,不存在则返回注册成功。

登录,防止重复登录

登录实现逻辑基本与注册相同,在客户端实现登录按钮的转到槽函数的逻辑,然后添加接收登录响应PDU的逻辑

退出,修改登录状态

用户退出之后,需要将数据库中用户登录状态修改为非在线状态,同时要删除掉服务器中维护的Socket的List中该用户对应的Socket(如果不进行删除,每次用户登录都会新建Socket,之前Socket没有用处,只会空占资源)

服务器需要通过槽函数handleClientOffline()接收Socket建立断开的信号disconnected(),然后进行处理

服务器通过调用数据库函数handleOffline现handleClientOffline()的修改用户登录状态的功能。但是还需要删除mytcpserver的QList中对应的socket,通过让该socket发送一个删除信号offline(),然后server接收到信号以后实现删除功能(实现槽函数deleteSocket来捕获下线信号offline并删除socket和去除QList中指针变量,incomingConnection中建立客户端对应Socket之后,需要绑定该socket的offline信号给对应槽函数)

注意

合理的利用delete可以有效减少应用对内存的消耗,但是delete的不合理使用常常导致应用crash,而deleteLater()可以更好的规避风险, 降低崩溃。

  • deleteLater()是QObject对象的一个函数, 要想使用此方法, 必须是一个QObject对象
  • deleteLater()依赖于Qt的event loop机制

如果在event loop启用前被调用, 那么event loop启用后对象才会被销毁

如果在event loop结束后被调用, 那么对象不会被销毁

如果在没有event loop的thread使用, 那么thread结束后销毁对象

  • 可以多次调用此函数
  • 线程安全

注销,删除好友信息,删除个人信息,删除网盘文件

登录注册注销回复

服务器处理客户端请求

注册,用户名唯一,防止重复登录。查询数据是否存在用户注册的用户名,存在则回复失败,不存在则回复成功。在服务器新建文件夹用作该用户的网盘区域

登录,防止重复登录,在数据库中添加字段用于记录该用户是否在线,在线则不允许登录

注销,删除好友信息,删除个人信息,删除网盘文件

好友功能

界面

该页面主要是展示用户所有好友,并对其进行实现操作、刷新、聊天等功能

因为好友页面和文件页面只会有一个显示出来,所以通过QStackedWidget控件实现。

QStackedWidget控件相当于一个容器,提供一个空间来存放一系列的控件,并且每次只能有一个控件是可见的,即被设置为当前的控件

将QListWidget的行号变化信号currentRowChanged()与QStackedWidget窗口的设置当前页面槽函数setCurrentIndex()关联,实现切换页面槽函数

将operateWidget(主界面显示与操作类)类设计为单例模式,然后在tcpclient.cpp中的登录响应LOGIN_OK中添加登录跳转功能

查看在线用户

客户端发送查看请求

服务器将数据库中在线用户查询出来并发送给客户端

客户端接收用户信息并显示

因为要在friend中发送socket请求,所以可以将TcpClient设置为单例模式,然后获取TcpSocket然后发送请求消息

查找用户

客户端发送查找请求

服务器将数据库中用户查询出来并发送给客户端

客户端接收用户信息并显示

添加好友

服务器端需要处理客户端的添加好友请求,先查询数据库中该用户是否在线,在线则转发请求信息

数据库查询好友关系,这里注意一个关键点:好友关系是双向的,而我们数据库中只存了一个方向,所以查询时要将被加好友用户名和发起请求用户名分别作为查询条件(or),任意一个查到即可

刷新好友列表

获取最新的在线好友,更新好友列表,同时更新好友在线状态

memcpy不看是不是字符串,也不看字符串是否有’\0’,直接复制n个字节

strncpy是复制字符串,如果碰到’\0’就停止拷贝,否则最多复制到n个字节停止拷贝

删除好友

私聊

对于每个一个私聊窗口,都需要维护所属客户端登录的用户的用户名,以及私聊对象的用户名

当用户输入信息,然后点击发送按钮之后,客户端需要将消息传递给服务器,由服务器发给目标好友

由于每个用户可以与多个好友进行私聊,所以客户端friend中需要维护一个私聊窗口的List,其中存储已经建立的私聊窗口

QList<PrivateChatWid*> m_priChatWidList; // 所有私聊的窗口

群发

客户端实现friend中群聊发送消息按钮点击信号的槽函数绑定

服务器收到群聊消息之后,对其进行处理

客户端收到服务器转发的消息后处理

文件

界面设计

文件夹操作

创建文件夹:

刷新文件夹、查看文件

服务器不仅仅返回文件的名字,还返回文件的类型、文件修改时间、文件大小等数据,便于用户操作

删除文件或文件夹

常规文件操作

文件重命名
进入文件夹
返回上一级
上传文件
  1. 客户端发送”当前路径和上传文件名“,服务器接收数据然后创建文件,再响应客户端

  2. 客户端收到服务器响应无误之后,再上传文件内容

首先,客户端fileSystem实现上传文件按钮和对应槽函数

服务器通过解析客户端发送的上传请求,将文件的名字、大小、路径等信息进行保存,然后设置状态为接收上传文件,便于之后接收文件,准备文件写入环境并返回响应消息给客户端。这确保了在客户端发起文件上传请求时,服务器能够正确处理请求并准备接收文件数据

然后客户端如果接收到UPLOAD_FILE_START的PDU,那么就开始一个定时器(1000ms),定时器时间到的信号timeout绑定了传输文件数据的函数,实际进行文件数据传输

服务器的myTcpSocket进行改变,当处于上传文件状态时,采用readAll()来接收数据

粘包

粘包问题通常发生在网络数据传输过程中,尤其是在使用TCP协议时,因为TCP是一个流协议,它会尽量高效地传输数据,但这有时会导致多个数据包在接收方看来粘在一起,没有明显的边界。因此,接收方需要知道如何正确地解析这些数据包

定时器的作用

通过使用定时器,控制发送数据的节奏来减小粘包问题发生的概率

  1. 定时发送数据

    • 使用定时器控制数据发送的间隔时间,可以减少一次性发送大量数据的情况,从而减小粘包发生的概率。
  2. 间隔发送

    • 每次定时器触发时,读取固定大小(如4096字节)的数据,并发送给服务器。这样可以避免短时间内大量数据的连续发送,减小粘包的可能性。

处理粘包的其他方法

尽管定时发送可以在一定程度上缓解粘包问题,但通常还需要以下机制来彻底解决粘包问题:

  1. 消息边界

    • 每个数据包中包含消息长度或特定的分隔符,以便接收方知道消息的边界。例如,前面提到的协议数据单元(PDU)中可以包含消息长度字段,接收方根据长度字段来解析每个消息
  2. 协议设计

    • 设计一个应用层协议,在发送数据时,将每个消息的长度和数据一起发送。接收方先读取长度字段,然后根据长度字段读取完整消息。例如,常见的做法是在消息前加上4个字节的长度字段
  3. 缓冲区处理

    • 接收方维护一个接收缓冲区,将收到的数据放入缓冲区中,然后从缓冲区中按照消息长度解析出完整的消息

定时器可以在一定程度上减小粘包问题发生的概率,但通常需要结合消息边界、协议设计和缓冲区处理等技术来彻底解决粘包问题。通过在数据包前添加长度字段,可以明确每个消息的边界,从而确保接收方能够正确解析每个消息。

下载文件

客户端发送请求,传输当前路径,下载文件名,选择下载位置以及保存所用名,然后发送给服务器

服务器获得客户端请求,判断其合理性,然后打开所要下载的文件,同时设置计时器(1s后开始传输),然后通知客户端

客户端收到服务器响应,设置自己状态为下载文件bDownloadFile,同时实现接收文件逻辑

服务器实现传输文件逻辑槽函数,计时器结束之后触发槽函数

移动文件

客户端提供两个按钮:

移动文件:选中需要移动的文件之后,点击该按钮,可以设置需要移动的文件

目标目录:(默认状态不可点击,需要点击过“移动文件”按钮之后才可以点击),在选中需要移动的文件之后,再跳转到想要移动的目标目录,进入目标目录之后再点击该按钮,即可设置移动的目标目录,然后向服务器发送请求

服务器接收到请求之后,对文件进行移动,并返回移动结果

客户端接收响应并提示用户

分享文件

界面设计

用户点击确认键之后发送请求分享消息:

  1. 要分享的文件的文件名、文件路径

  2. 要分享的好友名,好友数量

逻辑实现
  1. 分享文件的源客户端,实现点击”分享文件“按钮之后更新”shareFileFriendList”页面的好友列表并弹出该页面,同时设置分享文件的文件名和路径

    当客户利用shareFileFriendList页面中的确认分享按钮之后,客户端向服务器发送包含了分享文件名、分享文件路径、分享好友、分享好友数等信息的PDU

  2. 服务端接收到消息之后,需要解析出其中分享的文件名和文件路径,以及分享好友,将要分享的文件名和路径发送给对应的好友

  3. 发送分享文件的源客户会收到服务器的响应

  4. 被分享文件的所有目的客户都会收到服务器发送的NOTE的PDU,询问是否接收文件,如果选择接收文件,则会向服务器发送确认报文

  5. 服务器接收到被分享方的响应之后,开始实际拷贝文件,对文件与文件夹有不同拷贝策略。并返回被分享方拷贝结果

  6. 被分享方接收到服务器的响应之后显示结果