C++网盘项目(QT)
2025-08-07
StudyProjects
00

目录

C++网盘项目
数据库搭建
数据库设计
搭建数据库
通信协议设计
弹性结构体
通信协议设计
数据收发测试
协议格式定义
测试收发数据
登录注册注销退出
数据库操作
数据库连接
常见报错:QSqlDatabase: QMYSQL driver not loaded
登录注册注销请求
1. 规定消息类型
2. 界面设计
3. 注册,用户名唯一,防止重复注册
常见错误:插入数据库失败
4. 登录,防止重复登录
5. 退出,修改登录状态
Qt小技巧:同时运行多个项目实例
常见错误:在释放QList中Socket空间时发生了程序错误退出
6. 注销,删除好友信息,删除个人信息,删除网盘文件
好友功能-查搜添
页面设计
主操作页面
全网所有在线用户查询页面
好友操作功能页面
添加所有在线用户按钮事件
将好友与文件页面关联到主操作页面
登录跳转
查看在线用户
delete和free区别
查找用户
常见问题:用户名为中文时,显示乱码并且查找失败
添加好友
对于登录操作补充上TcpClient已经登录用户名存储
补充所有在线用户列表中不显示本人信息
好友功能-刷新删除
刷新好友列表
在用户登录之后跳转到好友页面应自动刷新一次好友列表
知识点:memcpy和strncpy区别
删除好友
好友功能-私聊群聊
私聊
群聊
文件功能-文件夹操作
页面设计
创建文件夹
注册时按用户名分配文件夹
登录时获取属于用户的根目录
创建文件夹按钮实现
刷新文件夹查看文件
删除文件或文件夹
文件功能-文件操作
文件重命名
进入文件夹
返回上一级
上传文件
下载文件
移动文件
分享文件
界面设计
逻辑实现
面试准备
项目知识点
项目不足之处

C++网盘项目

主要参考教程:c++项目实战,手把手教你搭建属于自己的网盘系统~_哔哩哔哩_bilibili

数据库搭建

主要通过MySQL实现了数据库的搭建工作,注:原教程采用的是SQLite

数据库设计

用户信息表 userInfo

字段类型约束条件其他
idint主键自动增长
namevarchar(32)not null
pwdvarchar(32)not null

用户好友表 friendInfo

字段类型约束条件其他
idint主键外键
friendIdint主键外键

搭建数据库

新建数据库

然后按照以上设计的数据库表进行创建表

通信协议设计

弹性结构体

大小是变化的结构体,实质是通过数组实现。

cpp
展开代码
#include <iostream> using namespace std; struct PDU { int a; int b; int c; int d[]; // 数组没有设定大小,所以没有占用空间,输出大小为12 // int* d; // 输出大小为24,指针本身占地方 8 Byte } pdu; int main() { cout << sizeof pdu << endl; PDU * ptr = (PDU*)malloc(sizeof(PDU) + 100 * sizeof(int)); cout << sizeof ptr << endl; // 输出的是指针的大小 8 Byte ptr -> a = 1; ptr -> b = 2; ptr -> c = 3; // (ptr -> d)[0] = 4; // cout << ptr -> c << ' ' << (ptr -> d)[0] << endl; // 输出:3 4 memcpy(ptr -> d, "Hello World!", 20); cout << (char*)(ptr -> d) << endl; // 输出:Hello World! // 释放空间 free(ptr); ptr = NULL; return 0; }

这种分配空间的方法会使得:

ptr → a 指向分配的空间的前四个字节(int大小空间)的首地址;

ptr → b 指向分配的空间的第二个四个字节(int大小空间)的首地址;

ptr → c 指向分配的空间的第三个四个字节(int大小空间)的首地址;

ptr → d 指向剩余所有空间(100个int大小)的首地址。

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

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

通信协议设计

协议结构体设计:

总的消息大小 uintuiPDULen
消息类型uintuiMsgType
文件其他信息(文件名等)char*caFileData[]
实际消息大小unituiMsgLen
实际消息int

数据收发测试

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

协议格式定义

在客户端创建一个protocol.h头文件,然后定义协议数据单元格式。

cpp
展开代码
#ifndef PROTOCOL_H #define PROTOCOL_H #include <stdlib.h> #include <unistd.h> // Unix库函数,包含了read等系统服务函数 #include <string.h> typedef unsigned int uint; // 设计协议数据单元格式 struct PDU { uint uiPDULen; // 总的协议数据单元大小 uint uiMsgType; // 消息类型 char caFileData[64]; // uint uiMsgLen; // 实际消息长度 int iMsg[]; // 实际消息,主要通过iMsg访问消息数据 }; PDU *mkPDU(uint uiMsgLen); // 创建PDU,uiMsglen是可变的,总大小可有其计算得到 #endif // PROTOCOL_H

然后在对应cpp文件中实现mkPDU函数

cpp
展开代码
#include "protocol.h" PDU *mkPDU(uint uiMsgLen) { uint uiPDULen = sizeof (PDU) + uiMsgLen; PDU* pdu = (PDU*)malloc(uiPDULen); if(NULL == pdu) { exit(EXIT_FAILURE); // 错误退出程序 } memset (pdu, 0, uiPDULen); // 数据初始化为0 pdu -> uiPDULen = uiPDULen; // 数据参数初始化 pdu -> uiMsgLen = uiMsgLen; return pdu; }

测试收发数据

在客户端UI中加入一个文本编辑行,输入按钮以及文本编辑区分别作为客户端输入数据,显示接收数据使用。然后给输入按钮添加一个转到槽。

然后在tcpclient.cpp中实现相应发送按钮点击事件

cpp
展开代码
// 客户端点击发送按钮事件 void TcpClient::on_send_pb_clicked() { QString strMsg = ui->send_le->text(); if(!strMsg.isEmpty()) // 消息非空才发送 { PDU *pdu = mkPDU(strMsg.size()); pdu -> uiMsgType = 0; // 消息类型 memcpy(pdu -> caMsg, strMsg.toStdString().c_str(), strMsg.size()); // 将需要传递的信息拷贝到协议数据单元中 m_tcpSocket.write((char*)pdu, pdu -> uiPDULen); // 通过socket发送信息 // 释放空间 free(pdu); pdu = NULL; } else // 消息为空警告 { QMessageBox::warning(this, "信息发送", "发送的信息不能为空"); } }

在服务器端,由于每个客户端都会与服务器建立一个socket进行数据通信,incomingConnection(qintptr handle)中参数是新建立的socket的描述符。为了能够区分开不同的socket,我们需要通过派生QTcpSocket来封装Socket。

MyTcpSocket.h

cpp
展开代码
class MyTcpSocket : public QTcpSocket { Q_OBJECT public: MyTcpSocket(); public slots: void receiveMsg(); // 槽函数,按照协议形式处理传输过来的数据 };

MyTcpSocket.cpp

cpp
展开代码
#include "mytcpsocket.h" MyTcpSocket::MyTcpSocket() { connect(this, SIGNAL(readyRead()), // 当接收到客户端的数据时,服务器会发送readyRead()信号 this, SLOT(receiveMsg())); // 需要由服务器的相应receiveMsg槽函数进行处理 } void MyTcpSocket::receiveMsg() { qDebug() << this -> bytesAvailable(); // 获取接收到的数据大小 uint uiPDULen = 0; this->read((char*)&uiPDULen, sizeof(uint)); // 先读取uint大小的数据,首个uint正是总数据大小 uint uiMsgLen = uiPDULen - sizeof(PDU); // 实际消息大小,sizeof(PDU)只会计算结构体大小,而不是分配的大小 PDU *pdu = mkPDU(uiMsgLen); this -> read((char*)pdu + sizeof(uint), uiPDULen - sizeof(uint)); // 接收剩余部分数据(第一个uint已读取) qDebug() << pdu -> uiMsgType << ' ' << (char*)pdu -> caMsg; // 输出 }

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

QList<MyTcpSocket*> m_tcpSocketList; // 存储服务器所有已经建立的Socket连接

MyTcpServer.h

cpp
展开代码
class MyTcpServer : public QTcpServer { Q_OBJECT // 类既要继承QObject又要写上宏Q_OBJECT,才能支持信号槽 private: MyTcpServer(); public: static MyTcpServer& getInstance(); // 实现单例模式获取静态对象的引用 void incomingConnection(qintptr handle) override; // 判断何时有客户端接入并处理 private: QList<MyTcpSocket*> m_tcpSocketList; // 存储服务器所有已经建立的Socket连接 };

MyTcpServer.cpp

cpp
展开代码
MyTcpServer::MyTcpServer() { } MyTcpServer &MyTcpServer::getInstance() { static MyTcpServer instance; // 由于是静态的,所以这个函数调用多次也只是创建一次 return instance; } void MyTcpServer::incomingConnection(qintptr handle) { // 派生QTcpSocket,然后对Socket进行绑定相应的槽函数,这样就可以不同客户端由不同MyTcpSocket进行处理 // 从而可以实现客户端连接和对应数据收发的socket的关联 qDebug() << "new client connected"; MyTcpSocket *pTcpSocket = new MyTcpSocket; // 建立新的socket连接 pTcpSocket -> setSocketDescriptor(handle); // 设置其Socket描述符,不同描述符指示不同客户端 m_tcpSocketList.append(pTcpSocket); }

疑问: 为什么服务器只是将所有Socket都存到一个QList中就可以实现和多个客户端传输数据?Socket描述符是如何起作用的?服务器端是如何维护List中的Socket的,代码呢?

登录注册注销退出

数据库操作

  1. 定义数据库操作类
  2. 将数据库操作类定义为单例
  3. 数据库相应操作

数据库连接

在Server端创建C++类DBOperate,注意要继承于QObject为了能够支持信号槽。

dboperate.h

cpp
展开代码
#ifndef DBOPERATE_H #define DBOPERATE_H #include <QObject> #include <QSqlDatabase> // 连接数据库 #include <QSqlQuery> // 数据库操作 class DBOperate : public QObject { Q_OBJECT public: explicit DBOperate(QObject *parent = nullptr); static DBOperate& getInstance(); // 公用获取引用,实现单例模式 void init(); // 初始化函数,数据库连接 ~DBOperate(); // 析构函数,关闭数据库连接 signals: public slots: private: QSqlDatabase m_db; // 连接数据库 }; #endif // DBOPERATE_H

dboperate.cpp

cpp
展开代码
#include "dboperate.h" #include <QMessageBox> #include <QDebug> DBOperate::DBOperate(QObject *parent) : QObject(parent) { // 连接数据库 m_db = QSqlDatabase::addDatabase("QMYSQL"); // 连接的数据库类型 } DBOperate &DBOperate::getInstance() { static DBOperate instance; return instance; } // 数据库连接 void DBOperate::init() { m_db.setHostName("localhost"); // 数据库服务器IP m_db.setUserName("root"); // 数据库用户名 m_db.setPassword("root"); // 数据库密码 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, "数据库打开", "数据库打开失败"); } } DBOperate::~DBOperate() { m_db.close(); // 关闭数据库连接 }

常见报错:QSqlDatabase: QMYSQL driver not loaded

参考解决方案:

QT连接mysql问题解决:QSqlDatabase: QMYSQL driver not loaded_程序白痴的博客-CSDN博客_qt连接mysql数据库时提示无驱动怎么解决

QSqlDatabase QMYSQL driver not loaded_白菜侠的博客-CSDN博客

登录注册注销请求

  1. 规定消息类型
  2. 界面设计
  3. 注册,用户名唯一,防止重复注册
  4. 登录,防止重复登录
  5. 退出,修改登录状态
  6. 注销,删除好友信息,删除个人信息,删除网盘文件

1. 规定消息类型

在protocol.h中通过枚举方式定义消息格式,以注册为例:

2. 界面设计

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

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

客户端实现注册按钮转到槽的具体内容

cpp
展开代码
void TcpClient::on_regist_pb_clicked() { QString strName = ui -> name_le -> text(); // 获取用户名和密码 QString strPwd = ui -> pwd_le -> text(); // 合理性判断 if(!strName.isEmpty() && !strPwd.isEmpty()) { // 注册信息用户名和密码将通过caData[64]传输 PDU *pdu = mkPDU(0); // 实际消息体积为0 pdu -> uiMsgType = ENUM_MSG_TYPE_REGIST_REQUEST; // 设置为注册请求消息类型 // 拷贝用户名和密码信息到caData memcpy(pdu -> caData, strName.toStdString().c_str(), 32); // 由于数据库设定的32位,所以最多只拷贝前32位 memcpy(pdu -> caData + 32, strPwd.toStdString().c_str(), 32); m_tcpSocket.write((char*)pdu, pdu -> uiPDULen); // 发送消息 // 释放空间 free(pdu); pdu = NULL; } else { QMessageBox::critical(this, "注册", "注册失败:用户名或密码为空!"); } }

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

mytcpsocket.cpp中实现receiveMsg的按不同消息类型处理不同请求:

cpp
展开代码
void MyTcpSocket::receiveMsg() { qDebug() << this -> bytesAvailable(); // 输出接收到的数据大小 uint uiPDULen = 0; this -> read((char*)&uiPDULen, sizeof(uint)); // 先读取uint大小的数据,首个uint正是总数据大小 uint uiMsgLen = uiPDULen - sizeof(PDU); // 实际消息大小,sizeof(PDU)只会计算结构体大小,而不是分配的大小 PDU *pdu = mkPDU(uiMsgLen); this -> read((char*)pdu + sizeof(uint), uiPDULen - sizeof(uint)); // 接收剩余部分数据(第一个uint已读取) // qDebug() << pdu -> uiMsgType << ' ' << (char*)pdu -> caMsg; // 输出 // 根据不同消息类型,执行不同操作 PDU* resPdu = NULL; // 响应报文 switch(pdu -> uiMsgType) { case ENUM_MSG_TYPE_REGIST_REQUEST: // 注册请求 { resPdu = handleRegistRequest(pdu); // 请求处理 break; } default: break; } // 响应客户端 if(NULL != resPdu) { qDebug() << resPdu -> uiMsgType << " " << resPdu ->caData; this -> write((char*)resPdu, resPdu -> uiPDULen); // 释放空间 free(resPdu); resPdu = NULL; } // 释放空间 free(pdu); pdu = NULL; } // 处理注册请求并返回响应PDU PDU* handleRegistRequest(PDU* pdu) { char caName[32] = {'\0'}; char caPwd[32] = {'\0'}; // 拷贝读取的信息 strncpy(caName, pdu -> caData, 32); strncpy(caPwd, pdu -> caData + 32, 32); qDebug() << pdu -> uiMsgType << " " << caName << " " << caPwd; bool ret = DBOperate::getInstance().handleRegist(caName, caPwd); // 处理请求,插入数据库 // 响应客户端 PDU *resPdu = mkPDU(0); // 响应消息 resPdu -> uiMsgType = ENUM_MSG_TYPE_REGIST_RESPOND; if(ret) { strcpy(resPdu -> caData, REGIST_OK); } else { strcpy(resPdu -> caData, REGIST_FAILED); } // qDebug() << resPdu -> uiMsgType << " " << resPdu ->caData; return resPdu; }

注:为了便于处理,所以将响应消息内容设定为宏。在protocol.h中设定:

cpp
展开代码
// 定义响应消息 #define REGIST_OK "regist ok" #define REGIST_FAILED "regist failed"

数据库操作dboperate.cpp中添加处理注册的代码:

cpp
展开代码
bool DBOperate::handleRegist(const char *name, const char *pwd) { // 考虑极端情况 if(NULL == name || NULL == pwd) { return false; } // 数据插入数据库 QString strQuery = QString("insert into userInfo(name, pwd) values('%1', '%2')").arg(name).arg(pwd); QSqlQuery query; // qDebug() << strQuery; return query.exec(strQuery); // 数据库中name索引是unique,所以如果name重复会返回false,插入成功返回true }

这时,已经实现了客户端请求注册,服务器响应注册请求发回响应PDU,还需要实现客户端的接收报文并处理。

在客户端的TcpClient类中添加槽函数并绑定readyRead()信号。

cpp
展开代码
void TcpClient::receiveMsg() { qDebug() << m_tcpSocket.bytesAvailable(); // 输出接收到的数据大小 uint uiPDULen = 0; m_tcpSocket.read((char*)&uiPDULen, sizeof(uint)); // 先读取uint大小的数据,首个uint正是总数据大小 uint uiMsgLen = uiPDULen - sizeof(PDU); // 实际消息大小,sizeof(PDU)只会计算结构体大小,而不是分配的大小 PDU *pdu = mkPDU(uiMsgLen); m_tcpSocket.read((char*)pdu + sizeof(uint), uiPDULen - sizeof(uint)); // 接收剩余部分数据(第一个uint已读取) // qDebug() << pdu -> uiMsgType << ' ' << (char*)pdu -> caMsg; // 输出 // 根据不同消息类型,执行不同操作 switch(pdu -> uiMsgType) { case ENUM_MSG_TYPE_REGIST_RESPOND: // 注册请求 { if(0 == strcmp(pdu -> caData, REGIST_OK)) { QMessageBox::information(this, "注册", REGIST_OK); } else if(0 == strcmp(pdu -> caData, REGIST_FAILED)) { QMessageBox::warning(this, "注册", REGIST_FAILED); } break; } default: break; } // 释放空间 free(pdu); pdu = NULL; }

TcpClient::**TcpClient**(QWidget *parent) 构造函数添加:

cpp
展开代码
// 绑定处理服务器响应消息的槽函数 connect(&m_tcpSocket, SIGNAL(readyRead()), // 信号发送方(Socket变量),发送信号类型 this, SLOT(receiveMsg())); // 信号处理方,用以处理的槽函数

TODO 之后实现在服务器端新建文件夹作为该用户的网盘区域

常见错误:插入数据库失败

cpp
展开代码
// 正确写法 QString strQuery = QString("insert into userInfo(name, pwd) values('%1', '%2')").arg(name).arg(pwd); // 错误写法 QString strQuery = QString("insert into userInfo values('%1', '%2')").arg(name).arg(pwd); QString strQuery = QString("insert into userInfo(name, pwd) values(%1, %2)").arg(name).arg(pwd);

4. 登录,防止重复登录

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

tcpclient.cpp中登录按钮转到槽函数

cpp
展开代码
void TcpClient::on_login_pb_clicked() { QString strName = ui -> name_le -> text(); QString strPwd = ui -> pwd_le -> text(); // 合理性判断 if(!strName.isEmpty() && !strPwd.isEmpty()) { PDU *pdu = mkPDU(0); // 实际消息体积为0 pdu -> uiMsgType = ENUM_MSG_TYPE_LOGIN_REQUEST; // 设置为登录请求消息类型 // 拷贝用户名和密码信息到caData memcpy(pdu -> caData, strName.toStdString().c_str(), 32); // 由于数据库设定的32位,所以最多只拷贝前32位 memcpy(pdu -> caData + 32, strPwd.toStdString().c_str(), 32); qDebug() << pdu -> uiMsgType << " " << pdu -> caData << " " << pdu -> caData + 32; m_tcpSocket.write((char*)pdu, pdu -> uiPDULen); // 发送消息 // 释放空间 free(pdu); pdu = NULL; } else { QMessageBox::critical(this, "登录", "登录失败:用户名或密码为空!"); } }

tcpclient.cpp中接收登陆响应代码

TODO 实现登录后跳转页面逻辑

cpp
展开代码
case ENUM_MSG_TYPE_LOGIN_RESPOND: // 登录请求 { if(0 == strcmp(pdu -> caData, LOGIN_OK)) { QMessageBox::information(this, "登录", LOGIN_OK); } else if(0 == strcmp(pdu -> caData, LOGIN_FAILED)) { QMessageBox::warning(this, "登录", LOGIN_FAILED); } break; }

而服务器端要实现接收登录请求的处理代码,需要查询数据库(注意实现防止重复登陆)并进行修改登录状态。

mytcpsocket.cpp中接收登录请求的代码

cpp
展开代码
case ENUM_MSG_TYPE_LOGIN_REQUEST: // 登录请求 { resPdu = handleLoginRequest(pdu); break; }
cpp
展开代码
// 处理登录请求并返回响应PDU PDU* handleLoginRequest(PDU* pdu) { char caName[32] = {'\0'}; char caPwd[32] = {'\0'}; // 拷贝读取的信息 strncpy(caName, pdu -> caData, 32); strncpy(caPwd, pdu -> caData + 32, 32); qDebug() << pdu -> uiMsgType << " " << caName << " " << caPwd; bool ret = DBOperate::getInstance().handleLogin(caName, caPwd); // 处理请求,插入数据库 // 响应客户端 PDU *resPdu = mkPDU(0); // 响应消息 resPdu -> uiMsgType = ENUM_MSG_TYPE_LOGIN_RESPOND; if(ret) { strcpy(resPdu -> caData, LOGIN_OK); } else { strcpy(resPdu -> caData, LOGIN_FAILED); } // qDebug() << resPdu -> uiMsgType << " " << resPdu ->caData; return resPdu; }

dboperate.cpp中处理登录请求代码

cpp
展开代码
bool DBOperate::handleLogin(const char *name, const char *pwd) { // 考虑极端情况 if(NULL == name || NULL == pwd) { return false; } // 数据库查询 QString strQuery = QString("select * from userInfo where name = \'%1\' and pwd = \'%2\' " "and online = 0").arg(name).arg(pwd); // online = 0 可以判定用户是否未登录,不允许重复登陆 QSqlQuery query; qDebug() << strQuery; query.exec(strQuery); if(query.next()) // 每次调用next都会读取一条数据,并将结果放入query中,返回值为true,无数据则返回false { // 如果登录成功,需要设置online = 1,并返回true strQuery = QString("update userInfo set online = 1 where name = \'%1\' and pwd = \'%2\' ").arg(name).arg(pwd); return query.exec(strQuery); } else { return false; } }

注意:

  1. 可以直接在数据库查询语句中通过 online = 0 来实现判断是否重复登陆,而不需要代码判断;
  2. 不要忘记查询成功之后修改用户登录状态 online = 1

5. 退出,修改登录状态

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

为了能够找到特定用户对应的Socket,我们需要在mytcpsocket中添加用户名name(这是主键)属性。

服务器需要在客户端登录成功时将用户名记录下来,mytcpserver.cpp中:

cpp
展开代码
// 处理登录请求并返回响应PDU PDU* handleLoginRequest(PDU* pdu, QString& m_strName) { char caName[32] = {'\0'}; ... if(ret) { strcpy(resPdu -> caData, LOGIN_OK); // 在登陆成功时,记录Socket对应的用户名 m_strName = caName; } ... }

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

cpp
展开代码
connect(this, SIGNAL(disconnected()), this, SLOT(handleClientOffline())); // 关联Socket连接断开与客户端下线处理槽函数
cpp
展开代码
void MyTcpSocket::handleClientOffline() { // 下文介绍逻辑实现 }

数据库dboperate.cpp实现online状态更新的函数:

cpp
展开代码
bool DBOperate::handleOffline(const char *name) { if(NULL == name) { qDebug() << "name is NULL"; return false; } // 更新online状态为0 QString strQuery = QString("update userInfo set online = 0 where name = \'%1\'").arg(name); QSqlQuery query; return query.exec(strQuery); }

现在,服务器通过调用数据库函数handleOffline()实现handleClientOffline()的修改用户登录状态的功能。但是我们还需要删除mytcpserver的QList中对应的socket。我们可以通过让该socket发送一个删除信号offline(),然后server接收到信号以后实现删除功能。

mytcpsocket.h

cpp
展开代码
class MyTcpSocket : public QTcpSocket { Q_OBJECT public: MyTcpSocket(); signals: void offline(MyTcpSocket *socket); // 通过信号传送给mytcpserver用户下线通知,然后附带参数socket地址方便删除 ... }

mytcpsocket.cpp中socket连接关闭信号disconnected()的处理函数handleClientOffline()实现逻辑:

cpp
展开代码
void MyTcpSocket::handleClientOffline() { DBOperate::getInstance().handleOffline(m_strName.toStdString().c_str()); emit offline(this); // 发送给mytcpserver该socket删除信号 }

在mytcpserver需要实现槽函数deleteSocket来捕获下线信号offline并删除socket和去除QList中指针变量。

首先,在incomingConnection中建立客户端对应Socket之后,需要绑定该socket的offline信号给对应槽函数;

cpp
展开代码
void MyTcpServer::incomingConnection(qintptr handle) { // 派生QTcpSocket,然后对Socket进行绑定相应的槽函数,这样就可以不同客户端由不同MyTcpSocket进行处理 // 从而可以实现客户端连接和对应数据收发的socket的关联 qDebug() << "new client connected"; MyTcpSocket *pTcpSocket = new MyTcpSocket; // 建立新的socket连接 pTcpSocket -> setSocketDescriptor(handle); // 设置其Socket描述符,不同描述符指示不同客户端 m_tcpSocketList.append(pTcpSocket); // 关联socket用户下线信号与删除socket的槽函数 connect(pTcpSocket, SIGNAL(offline(MyTcpSocket *)), this, SLOT(deleteSocket(MyTcpSocket *))); }

然后,处理offline信号的槽函数实现机制如下:

cpp
展开代码
void MyTcpServer::deleteSocket(MyTcpSocket *mySocket) { // 遍历m_tcpSocketList并删除socket QList<MyTcpSocket*>::iterator iter = m_tcpSocketList.begin(); for(; iter != m_tcpSocketList.end(); iter ++) { if(mySocket == *iter) { (*iter) -> deleteLater(); // 延迟释放空间,使用delete会报错!!! *iter = NULL; m_tcpSocketList.erase(iter); // 删除列表中指针 break; } } // 输出一下所有socket,看看是否删除成功 --- 测试 for(int i = 0; i < m_tcpSocketList.size(); ++ i) { QString tmp = m_tcpSocketList.at(i) -> getStrName(); qDebug() << tmp; } }

注意,这里有个严重的BUG,释放Socket空间时使用delete释放会使得程序异常退出,解决方案见下文 常见错误:在释放QList中Socket空间时发生了程序错误退出

Qt小技巧:同时运行多个项目实例

“工具”->“选项”->“构建和运行”->"Stop application before building"设置为None即可。

常见错误:在释放QList中Socket空间时发生了程序错误退出

这个错误卡了一个多小时

分析原因应该是MyTcpServer中deleteSocket()函数的释放空间发生的错误。其他代码已经通过注释debug测试过了,不会异常退出。错误代码如下:

cpp
展开代码
void MyTcpServer::deleteSocket(MyTcpSocket *mySocket) { // 遍历m_tcpSocketList并删除socket QList<MyTcpSocket*>::iterator iter = m_tcpSocketList.begin(); for(; iter != m_tcpSocketList.end(); iter ++) { if(mySocket == *iter) // 异常原因发生在这里!!! { // error delete *iter; // 释放空间 *iter = NULL; // error end m_tcpSocketList.erase(iter); // 删除列表中指针 break; } } // 输出一下所有socket,看看是否删除成功 --- 测试 for(int i = 0; i < m_tcpSocketList.size(); ++ i) { QString tmp = m_tcpSocketList.at(i) -> getStrName(); qDebug() << tmp; } }

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

  • 首先”deleteLater()“是QObject对象的一个函数, 要想使用此方法, 必须是一个QObject对象。
  • ”deleteLater()“依赖于Qt的event loop机制。
    • 如果在event loop启用前被调用, 那么event loop启用后对象才会被销毁;
    • 如果在event loop结束后被调用, 那么对象不会被销毁;
    • 如果在没有event loop的thread使用, 那么thread结束后销毁对象。
  • 可以多次调用此函数。
  • 线程安全。

Qt5 -- 超好用的"deleteLater()" - 知乎 (zhihu.com)

解决方案:将delete改为deleteLater(),延迟释放空间

cpp
展开代码
void MyTcpServer::deleteSocket(MyTcpSocket *mySocket) { ... if(mySocket == *iter) { (*iter) -> deleteLater(); // 延迟释放空间 ... }

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

TODO 由于涉及到好友信息,文件等操作,所以留到后面实现该功能

好友功能-查搜添

页面设计

主操作页面

首先,客户端创建一个C++类QWidget作为基类的OperateWidget类,主要作为主页面实现客户端各种操作给用户使用。

我们采用QListWidget来组织所有用户可用的操作。

OperateWidget.h

cpp
展开代码
class OperateWidget : public QWidget { Q_OBJECT private: QListWidget *m_pListWidget; // 组织主页面左侧常用功能(好友、文件按钮等)

OperateWidget.cpp

cpp
展开代码
OperateWidget::OperateWidget(QWidget *parent) : QWidget(parent) { m_pListWidget = new QListWidget(this); // 参数指示QWidget *parent m_pListWidget -> addItem("好友"); m_pListWidget -> addItem("文件"); }

Qt提供QListWidget类列表框控件用来加载并显示多个列表项。QListWidgetItem类就是列表项类。一般列表框控件中的列表项有两种加载方式:

一种是由用户手动添加的列表项,比如音乐播放器中加载音乐文件的文件列表,每一个音乐文件都是一个列表项。

对于这种列表项,用户可以进行增加、删除、单击以及双击等操作。

一种是由程序员事先编写好,写在程序中供用户选择的列表项,比如餐厅的电子菜单,每一道菜对应一个列表项。

对于这种列表项,用户可以进行单机和双击操作(增加和删除操作也是可以进行的,但是一般的点菜系统会屏蔽掉这种功能)。

QListWidget类列表框控件支持两种列表项显示方式,即QListView::IconMode和QListView::ListMode。

总结一下列表框常用的增加、删除、单击、双击操作以及列表项显示方式设置,先给出全部代码,再解释。

QListWidget介绍原文链接:https://blog.csdn.net/weixin_38739598/article/details/110127431

全网所有在线用户查询页面

该页面可以通过Qt UI页面实现。直接选择空白的Widget的设计师界面类就可。

然后通过设计页面添加响应组件等,绘制出在线用户界面:

好友操作功能页面

该页面主要是展示用户所有好友,并对其进行实现操作、刷新、聊天等功能。这一页面涉及到功能较多,所以我们采用C++类代码形式实现该页面。

Friend.h中添加页面所用到部件

Friend.cpp在类的构建函数中构建该页面的布局:

cpp
展开代码
Friend::Friend(QWidget *parent) : QWidget(parent) { m_pFriendLW = new QListWidget; m_pInputMsgLE = new QLineEdit; m_pShowMsgTE = new QTextEdit; m_pDelFriendPB = new QPushButton("删除好友"); m_pFlushFriendPB = new QPushButton("刷新好友"); m_pShowOnlineUserPB = new QPushButton("显示在线用户"); m_pSearchUserPB = new QPushButton("查找用户"); m_pSendMsgPB = new QPushButton("发送"); m_pPrivateChatPB = new QPushButton("私聊"); QVBoxLayout *pLeftRightVBL = new QVBoxLayout; // 左侧右部分好友操作按钮布局 pLeftRightVBL -> addWidget(m_pPrivateChatPB); pLeftRightVBL -> addWidget(m_pDelFriendPB); pLeftRightVBL -> addWidget(m_pFlushFriendPB); pLeftRightVBL -> addWidget(m_pShowOnlineUserPB); pLeftRightVBL -> addWidget(m_pSearchUserPB); QHBoxLayout *pRightDownHBL = new QHBoxLayout; // 右侧下方发送消息布局 pRightDownHBL -> addWidget(m_pInputMsgLE); pRightDownHBL -> addWidget(m_pSendMsgPB); QVBoxLayout *pRightVBL = new QVBoxLayout; // 右侧聊天布局 pRightVBL -> addWidget(m_pShowMsgTE); pRightVBL -> addLayout(pRightDownHBL); QHBoxLayout *pAllHBL = new QHBoxLayout; // 整体水平布局 pAllHBL -> addWidget(m_pFriendLW); // 左侧左部分好友列表 pAllHBL -> addLayout(pLeftRightVBL); // 左侧右部分好友操作 pAllHBL -> addLayout(pRightVBL); // 右侧聊天布局 setLayout(pAllHBL); // 将整体布局pAllHBL设置为页面布局 }

效果如右图:

添加所有在线用户按钮事件

实现点击”显示在线用户“按钮之后弹出对应页面onlineuserwid,即在Friend中添加:

Friend.h

cpp
展开代码
class Friend : public QWidget { ... public slots: void showOrHideOnlineUserW(); // 处理显示/隐藏所有在线用户按钮点击信号的槽函数 ... private: OnlineUserWid *m_pOnlineUserW; // 所有在线用户页面 };

Friend.cpp的类构建函数

cpp
展开代码
Friend::Friend(QWidget *parent) : QWidget(parent) { ... m_pOnlineUserW = new OnlineUserWid; m_pOnlineUserW -> hide(); // 默认所有在线用户页面隐藏 // 绑定打开所有在线用户按钮与对应事件 connect(m_pSOrHOnlineUserPB, SIGNAL(clicked(bool)), this, SLOT(showOrHideOnlineUserW())); } void Friend::showOrHideOnlineUserW() { if(m_pOnlineUserW -> isHidden()) { m_pOnlineUserW -> show(); } else { m_pOnlineUserW -> hide(); } }class Friend : public QWidget { Q_OBJECT public: explicit Friend(QWidget *parent = nullptr); public slots: void showOrHideOnlineUserW(); // 显示/隐藏所有在线用户页面 signals: private: QListWidget *m_pFriendLW; // 好友列表 QLineEdit *m_pInputMsgLE; // 信息输入框 QTextEdit *m_pShowMsgTE; // 显示信息 QPushButton *m_pDelFriendPB; // 删除好友 QPushButton *m_pFlushFriendPB; // 刷新好友列表 QPushButton *m_pSOrHOnlineUserPB; // 显示/隐藏所有在线用户 QPushButton *m_pSearchUserPB; // 查找用户 QPushButton *m_pSendMsgPB; // 发送消息 QPushButton *m_pPrivateChatPB; // 私聊按钮,默认群聊 OnlineUserWid *m_pOnlineUserW; // 所有在线用户页面 };

实现效果:

文件页面

暂时只是占个位置,之后实现具体内容

TODO 实现文件系统

同样通过C++类的形式继承QWidget的FileSystem类实现。

将好友与文件页面关联到主操作页面

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

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

OperateWidget.h

cpp
展开代码
class OperateWidget : public QWidget { Q_OBJECT public: explicit OperateWidget(QWidget *parent = nullptr); signals: private: QListWidget *m_pListWidget; // 组织主页面左侧常用功能(好友、文件按钮等) Friend *m_pFriend; // 好友页面 FileSystem *m_pFileSystem; // 文件页面 QStackedWidget *m_pSW; // 容器,每次显示一个页面(好友or文件) };

OperateWidget.cpp

cpp
展开代码
OperateWidget::OperateWidget(QWidget *parent) : QWidget(parent) { m_pListWidget = new QListWidget(this); // 参数指示QWidget *parent m_pListWidget -> addItem("好友"); m_pListWidget -> addItem("文件"); m_pFriend = new Friend; m_pFileSystem = new FileSystem; m_pSW = new QStackedWidget; m_pSW -> addWidget(m_pFriend); // 如果没有设置,默认显示第一个页面 m_pSW -> addWidget(m_pFileSystem); QHBoxLayout *pMainHBL = new QHBoxLayout; pMainHBL -> addWidget(m_pListWidget); pMainHBL -> addWidget(m_pSW); setLayout(pMainHBL); }

实现效果如下:

现在点击左侧选项是没有用的,需要将QListWidget的行号变化信号currentRowChanged()与QStackedWidget窗口的设置当前页面槽函数setCurrentIndex()关联,实现切换页面槽函数。

OperateWidget.cpp

cpp
展开代码
OperateWidget::OperateWidget(QWidget *parent) : QWidget(parent) { ... setLayout(pMainHBL); // 将m_pListWidget的行号变化信号与m_pSW的设置当前页面槽函数关联 connect(m_pListWidget, SIGNAL(currentRowChanged(int)), // 函数参数为改变后的行号 m_pSW, SLOT(setCurrentIndex(int))); // 函数参数为设置的页面下标 }

登录跳转

按照相同方式将operateWidget类设计为单例模式。然后在tcpclient.cpp中的登录响应LOGIN_OK中添加登录跳转功能。

tcpclient.cpp

cpp
展开代码
case ENUM_MSG_TYPE_LOGIN_RESPOND: // 登录响应 { if(0 == strcmp(pdu -> caData, LOGIN_OK)) { // QMessageBox::information(this, "登录", LOGIN_OK); // 登录跳转 OperateWidget::getInstance().show(); // 显示主操作页面 this -> hide(); // 隐藏登陆页面 }

查看在线用户

客户端首先在friend的构造函数中绑定显示所有在线用户的按钮和槽函数

cpp
展开代码
// 绑定打开所有在线用户按钮与对应事件 connect(m_pSOrHOnlineUserPB, SIGNAL(clicked(bool)), this, SLOT(showOrHideOnlineUserW()));

并实现槽函数逻辑。

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

这里在协议中添加枚举数据:ENUM_MSG_TYPE_ONLINE_USERS_REQUEST的代码不再展示

cpp
展开代码
void Friend::showOrHideOnlineUserW() { if(m_pOnlineUserW -> isHidden()) { // 显示onlineUserWid页面 m_pOnlineUserW -> show(); // 发送请求查询数据库获取在线用户 PDU *pdu = mkPDU(0); pdu -> uiMsgType = ENUM_MSG_TYPE_ONLINE_USERS_REQUEST; TcpClient::getInstance().getTcpSocket().write((char*)pdu, pdu -> uiPDULen); free(pdu); pdu = NULL; } else { m_pOnlineUserW -> hide(); } }

服务端添加该类型信息的处理代码

MyTcpSocket.cpp

cpp
展开代码
void MyTcpSocket::receiveMsg() { ... case ENUM_MSG_TYPE_ONLINE_USERS_REQUEST: // 查询所有在线用户请求 { resPdu = handleOnlineUsersRequest(); break; }
cpp
展开代码
// 处理查询所有在线用户的请求 PDU* handleOnlineUsersRequest() { QStringList strList = DBOperate::getInstance().handleOnlineUsers(); // 查询请求,查询数据库所有在线用户 uint uiMsgLen = strList.size() * 32; // 消息报文的长度 // 响应客户端 PDU *resPdu = mkPDU(uiMsgLen); // 响应消息 resPdu -> uiMsgType = ENUM_MSG_TYPE_ONLINE_USERS_RESPOND; qDebug() << "在线用户数:" << strList.size(); for(int i = 0; i < strList.size(); ++ i) { memcpy((char*)(resPdu -> caMsg) + 32 * i, strList[i].toStdString().c_str(), strList[i].size()); qDebug() << "所有在线用户有:" << (char*)(resPdu -> caMsg) + 32 * i; } return resPdu; }

相应的,在数据库操作代码dboperate中也要加入相应查询代码

cpp
展开代码
// 处理查询所有在线用户的请求 PDU* handleOnlineUsersRequest() { QStringList strList = DBOperate::getInstance().handleOnlineUsers(); // 查询请求,查询数据库所有在线用户 uint uiMsgLen = strList.size() * 32; // 消息报文的长度 // 响应客户端 PDU *resPdu = mkPDU(uiMsgLen); // 响应消息 resPdu -> uiMsgType = ENUM_MSG_TYPE_ONLINE_USERS_RESPOND; qDebug() << "在线用户数:" << strList.size(); for(int i = 0; i < strList.size(); ++ i) { memcpy((char*)(resPdu -> caMsg) + 32 * i, strList[i].toStdString().c_str(), strList[i].size()); qDebug() << "所有在线用户有:" << (char*)(resPdu -> caMsg) + 32 * i; } return resPdu; }

那么,在服务器发送响应之后,客户端同样需要添加处理响应的代码来进行处理。

TcpClient.cpp

cpp
展开代码
void TcpClient::receiveMsg() { ... case ENUM_MSG_TYPE_ONLINE_USERS_RESPOND: // 查询所有在线用户响应 { OperateWidget::getInstance().getPFriend() -> setOnlineUsers(pdu); break; }

friend.cpp

cpp
展开代码
void Friend::setOnlineUsers(PDU* pdu) { if(NULL == pdu) { return ; } m_pOnlineUserW->setOnlineUsers(pdu); }

onlineuserwid.cpp中实现对于onlineuser_lw中列表数据的添加。

cpp
展开代码
void OnlineUserWid::setOnlineUsers(PDU *pdu) { if(NULL == pdu) { return ; } // 处理pdu的Msg部分,将所有在线用户显示出来 uint uiSize = pdu -> uiMsgLen / 32; // 消息Msg部分包含的用户数 char caTmp[32]; ui -> onlineuser_lw -> clear();// 清除之前在线用户列表的旧数据 for(uint i = 0; i < uiSize; ++ i) { memcpy(caTmp, (char*)(pdu -> caMsg) + 32 * i, 32); // qDebug() << "在线用户:" << caTmp; ui -> onlineuser_lw -> addItem(caTmp); } }

delete和free区别

为什么是free(pdu)而不是delete pdu?

1.free是C的库函数,delete是C++的关键字

2.delete在释放内存之前调用类的析构函数,但是free并没有这个操作

查找用户

在客户端添加Friend的查找按钮点击信号的处理槽函数。

cpp
展开代码
Friend::Friend(QWidget *parent) : QWidget(parent) { ... // 绑定查找用户按钮与对应事件 connect(m_pSearchUserPB, SIGNAL(clicked(bool)), this, SLOT(searchUser())); } void Friend::searchUser() { QString name = QInputDialog::getText(this, "搜索", "用户名:"); // 通过输入子页面来获取用户输入返回一个文本类型 if(!name.isEmpty()) { qDebug() << "查找:" << name; PDU *pdu = mkPDU(0); pdu -> uiMsgType = ENUM_MSG_TYPE_SEARCH_USER_REQUEST; memcpy((char*)pdu -> caData, name.toStdString().c_str(), 32); TcpClient::getInstance().getTcpSocket().write((char*)pdu, pdu -> uiPDULen); free(pdu); pdu = NULL; } }

服务器端中,mytcpsocket中同样添加对应case以及处理函数 handleSearchUserRequest

cpp
展开代码
// 处理查找用户的请求 PDU* handleSearchUserRequest(PDU* pdu) { char caName[32] = {'\0'}; strncpy(caName, pdu -> caData, 32); int ret = DBOperate::getInstance().handleSearchUser(caName); // 处理查找用户,0存在不在线,1存在并在线,2不存在 // 响应客户端 PDU *resPdu = mkPDU(0); // 响应消息 resPdu -> uiMsgType = ENUM_MSG_TYPE_SEARCH_USER_RESPOND; if(ret == 1) { strcpy(resPdu -> caData, SEARCH_USER_OK); } else if(ret == 0) { strcpy(resPdu -> caData, SEARCH_USER_OFFLINE); } else { strcpy(resPdu -> caData, SEARCH_USER_EMPTY); } return resPdu; }

然后在dboperate中添加查找数据库的代码:

cpp
展开代码
int DBOperate::handleSearchUser(const char *name) // 处理查找用户,0存在不在线,1存在并在线,2不存在 { if(NULL == name) { return 2; } QString strQuery = QString("select online from userInfo where name = \'%1\' ").arg(name); QSqlQuery query; query.exec(strQuery); if(query.next()) { return query.value(0).toInt(); // 存在并在线返回1,存在不在线返回0 } else { return 2; // 不存在该用户 } }

客户端在TcpClient需要添加查找响应的处理case

cpp
展开代码
case ENUM_MSG_TYPE_SEARCH_USER_RESPOND: // 查找用户响应 { if(0 == strcmp(SEARCH_USER_OK, pdu -> caData)) { QMessageBox::information(this, "查找", OperateWidget::getInstance().getPFriend()->getStrSearchName() + SEARCH_USER_OK); } else if(0 == strcmp(SEARCH_USER_OFFLINE, pdu -> caData)) { QMessageBox::information(this, "查找", OperateWidget::getInstance().getPFriend()->getStrSearchName() + SEARCH_USER_OFFLINE); } else if(0 == strcmp(SEARCH_USER_EMPTY, pdu -> caData)) { QMessageBox::warning(this, "查找", OperateWidget::getInstance().getPFriend()->getStrSearchName() + SEARCH_USER_EMPTY); } break; }

由于#define SEARCH_USER_OK等以及头文件中的添加元素等操作之前已经进行多次,之后不再重复介绍。

常见问题:用户名为中文时,显示乱码并且查找失败

TODO 解决方案

添加好友

客户端添加好友操作主要是在Online页面进行实现。首先需要实现OnlineUserWid中“添加好友”按钮的点击转到槽函数:

cpp
展开代码
// 添加好友按钮转到槽函数 void OnlineUserWid::on_addfriend_pb_clicked() { QString strAddName = ui -> onlineuser_lw -> currentItem()->text(); // 获得要添加好友用户名 QString strLoginName = TcpClient::getInstance().getStrName(); // 该用户自己用户名 PDU* pdu = mkPDU(0); pdu -> uiMsgType = ENUM_MSG_TYPE_ADD_FRIEND_REQUEST; memcpy(pdu->caData, strAddName.toStdString().c_str(), strAddName.size()); memcpy(pdu->caData + 32, strLoginName.toStdString().c_str(), strLoginName.size()); TcpClient::getInstance().getTcpSocket().write((char*)pdu, pdu -> uiPDULen); free(pdu); pdu = NULL; }

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

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

DBOperate.cpp

cpp
展开代码
// 0对方存在不在线,1对方存在在线,2不存在,3已是好友,4请求错误 int DBOperate::handleAddFriend(const char *addedName, const char *sourceName) { if(NULL == addedName || NULL == sourceName) { return 4; // 请求错误 } QString strQuery = QString("select * from friendInfo " "where (id = (select id from userInfo where name = \'%1\') and " "friendId = (select id from userInfo where name = \'%2\')) or " // 好友是双向的,数据库只存了单向,注意是or关系 "(id = (select id from userInfo where name = \'%3\') and " "friendId = (select id from userInfo where name = \'%4\'))") .arg(sourceName).arg(addedName).arg(addedName).arg(sourceName); qDebug() << strQuery; QSqlQuery query; query.exec(strQuery); if(query.next()) { return 3; // 双方已经是好友 } else // 不是好友 { return handleSearchUser(addedName); // 查询对方,存在并在线返回1,存在不在线返回0,不存在该用户返回2 } }

通过上面实现的DBoperate.cpp中的handleAddFriend()函数,我们可以得到被申请好友方用户的所有可能状态,然后服务器需要根据这些状态进行依此处理:

  • 0对方存在不在线,2不存在,3已是好友,4请求错误等状态,都是直接返回好友申请方响应消息
  • 1对方存在在线,则需要服务器将请求消息转发给被申请好友方,让其处理好友申请消息
cpp
展开代码
// 处理添加好友请求 PDU* handleAddFriendRequest(PDU* pdu) { char addedName[32] = {'\0'}; char sourceName[32] = {'\0'}; // 拷贝读取的信息 strncpy(addedName, pdu -> caData, 32); strncpy(sourceName, pdu -> caData + 32, 32); qDebug() << "handleAddFriendRequest " << addedName << " " << sourceName; int iSearchUserStatus = DBOperate::getInstance().handleAddFriend(addedName, sourceName); // 0对方存在不在线,1对方存在在线,2不存在,3已是好友,4请求错误 PDU* resPdu = NULL; switch (iSearchUserStatus) { case 0: // 0对方存在不在线 { resPdu = mkPDU(0); resPdu -> uiMsgType = ENUM_MSG_TYPE_ADD_FRIEND_RESPOND; strcpy(resPdu -> caData, ADD_FRIEND_OFFLINE); break; } case 1: // 1对方存在在线 { // 需要转发给对方请求添加好友消息 MyTcpServer::getInstance().forwardMsg(addedName, pdu); resPdu = mkPDU(0); resPdu -> uiMsgType = ENUM_MSG_TYPE_ADD_FRIEND_RESPOND; strcpy(resPdu -> caData, ADD_FRIEND_OK); // 表示加好友请求已发送 break; } case 2: // 2用户不存在 { resPdu = mkPDU(0); resPdu -> uiMsgType = ENUM_MSG_TYPE_ADD_FRIEND_RESPOND; strcpy(resPdu -> caData, ADD_FRIEND_EMPTY); break; } case 3: // 3已是好友 { resPdu = mkPDU(0); resPdu -> uiMsgType = ENUM_MSG_TYPE_ADD_FRIEND_RESPOND; strcpy(resPdu -> caData, ADD_FRIEND_EXIST); break; } case 4: // 4请求错误 { resPdu = mkPDU(0); resPdu -> uiMsgType = ENUM_MSG_TYPE_ADD_FRIEND_RESPOND; strcpy(resPdu -> caData, UNKNOWN_ERROR); break; } default: break; } return resPdu; }

其中,服务器转发消息的函数实现MyTcpServer::getInstance().forwardMsg()逻辑如下:

cpp
展开代码
bool MyTcpServer::forwardMsg(const QString caDesName, PDU *pdu) { if(caDesName == NULL || pdu == NULL) { return false; } // 查找目标用户名的Socket for(int i = 0; i < m_tcpSocketList.size(); ++ i) { if(caDesName == m_tcpSocketList.at(i) -> getStrName()) // 查找到 { m_tcpSocketList.at(i)->write((char*)pdu, pdu -> uiPDULen); // 转发消息 return true; } } return false; }

所以,客户端也需要实现接收到服务器转发forwardMsg()过来的申请方客户端的好友申请之后的响应操作。

tcpClient.cpp的 TcpClient::**receiveMsg**()

cpp
展开代码
case ENUM_MSG_TYPE_ADD_FRIEND_REQUEST: // 处理服务器转发过来的好友请求消息 { char sourceName[32]; // 获取发送方用户名 strncpy(sourceName, pdu -> caData + 32, 32); int ret = QMessageBox::information(this, "好友申请", QString("%1 想添加您为好友,是否同意?").arg(sourceName), QMessageBox::Yes, QMessageBox::No); // 后面两个参数是为QMessage默认支持两个按钮来设置枚举值 PDU* resPdu = mkPDU(0); strncpy(resPdu -> caData, pdu -> caData, 32); // 被加好友者用户名 strncpy(resPdu -> caData + 32, pdu -> caData + 32, 32); // 加好友者用户名 // qDebug() << "同意加好友吗?" << resPdu -> caData << " " << resPdu -> caData + 32; if(ret == QMessageBox::Yes) // 同意加好友 { resPdu->uiMsgType = ENUM_MSG_TYPE_ADD_FRIEND_AGREE; } else { resPdu->uiMsgType = ENUM_MSG_TYPE_ADD_FRIEND_REJECT; } m_tcpSocket.write((char*)resPdu, resPdu -> uiPDULen); // 发送给服务器消息,由服务器写入数据库并转发给用户 break; }

之后,被添加好友方的客户端会根据用户的选择发送给服务器不同类型的消息(ENUM_MSG_TYPE_ADD_FRIEND_AGREE、ENUM_MSG_TYPE_ADD_FRIEND_REJECT),来表示是否接收好友申请。


服务器接收到被添加好友方的好友申请回复消息之后,需要对其按不同类型分别进行处理

mytcpsocket.cpp

cpp
展开代码
// 同意加好友 void handleAddFriendAgree(PDU* pdu) { char addedName[32] = {'\0'}; char sourceName[32] = {'\0'}; // 拷贝读取的信息 strncpy(addedName, pdu -> caData, 32); strncpy(sourceName, pdu -> caData + 32, 32); // 将新的好友关系信息写入数据库 DBOperate::getInstance().handleAddFriendAgree(addedName, sourceName); // 服务器需要转发给发送好友请求方其被同意的消息 MyTcpServer::getInstance().forwardMsg(sourceName, pdu); } // 拒绝加好友 void handleAddFriendReject(PDU* pdu) { char sourceName[32] = {'\0'}; // 拷贝读取的信息 strncpy(sourceName, pdu -> caData + 32, 32); // 服务器需要转发给发送好友请求方其被拒绝的消息 MyTcpServer::getInstance().forwardMsg(sourceName, pdu); }

好友申请方客户端收到服务器转发过来的好友申请回复后,需要通过页面形式展示申请结果给用户。

TcpClient中receiveMsg()函数添加以下逻辑:

cpp
展开代码
case ENUM_MSG_TYPE_ADD_FRIEND_AGREE: // 对方同意加好友 { QMessageBox::information(this, "添加好友", QString("%1 已同意您的好友申请!").arg(pdu -> caData)); break; } case ENUM_MSG_TYPE_ADD_FRIEND_REJECT: // 对方拒绝加好友 { QMessageBox::information(this, "添加好友", QString("%1 已拒绝您的好友申请!").arg(pdu -> caData)); break; }

对于登录操作补充上TcpClient已经登录用户名存储

服务器端mytcpsocket中的处理登录请求操作中补充返回用户名的代码

cpp
展开代码
// 处理登录请求并返回响应PDU PDU* handleLoginRequest(PDU* pdu, QString& m_strName) { char caName[32] = {'\0'}; ... if(ret) { memcpy(resPdu -> caData, LOGIN_OK, 32); memcpy(resPdu -> caData + 32, caName, 32); // 将登录后的用户名传回,便于tcpclient确认已经登陆的用户名

客户端补充在用户登录成功之后设置TcpClient的用户名m_strName私有变量值的代码

cpp
展开代码
case ENUM_MSG_TYPE_LOGIN_RESPOND: // 登录响应 { if(0 == strcmp(pdu -> caData, LOGIN_OK)) { char caName[32] = {'\0'}; strncpy(caName, pdu -> caData + 32, 32); // 设置已登录用户名 m_strName = caName; ... }

补充所有在线用户列表中不显示本人信息

因为如果显示本人信息,当用户添加本人为好友会出现错误,所以在onlineUserWid中添加以下逻辑不显示本人信息,即用户不能添加本人为好友。

cpp
展开代码
void OnlineUserWid::setOnlineUsers(PDU *pdu) { ... for(uint i = 0; i < uiSize; ++ i) { memcpy(caTmp, (char*)(pdu -> caMsg) + 32 * i, 32); // qDebug() << "在线用户:" << caTmp; // 补充:不显示自己信息,防止之后添加自己为好友等操作错误 if(strcmp(caTmp, TcpClient::getInstance().getStrName().toStdString().c_str()) == 0) { continue; } ui -> onlineuser_lw -> addItem(caTmp); } }

好友功能-刷新删除

刷新好友列表

功能作用:获取最新的在线好友,更新好友列表,同时更新好友在线状态。

客户端实现刷新好友列表按钮与对应槽函数绑定,并实现槽函数逻辑

friend.cpp

cpp
展开代码
// 构造函数中绑定刷新好友列表按钮与对应事件 connect(m_pFlushFriendPB, SIGNAL(clicked(bool)), this, SLOT(flushFriendList()));
cpp
展开代码
// 刷新好友按钮的槽函数 void Friend::flushFriendList() { QString strName = TcpClient::getInstance().getStrName(); // 获取自己用户名 PDU* pdu = mkPDU(0); pdu -> uiMsgType = ENUM_MSG_TYPE_FLSUH_FRIEND_REQUEST; strncpy(pdu -> caData, strName.toStdString().c_str(), strName.size()); TcpClient::getInstance().getTcpSocket().write((char*)pdu, pdu -> uiPDULen); free(pdu); pdu = NULL; }

服务器端接收到客户端的刷新好友列表请求之后,根据PDU中的客户端name查数据库,然后进行响应用户的所有好友以及好友的在线状态。

mytcpsocket.cpp

cpp
展开代码
// 刷新好友列表请求 PDU* handleFlushFriendRequest(PDU* pdu) { char caName[32] = {'\0'}; strncpy(caName, pdu -> caData, 32); QStringList strList = DBOperate::getInstance().handleFlushFriendRequest(caName); uint uiMsgLen = strList.size() / 2 * 36; // 36 char[32] 好友名字+ 4 int 在线状态 PDU* resPdu = mkPDU(uiMsgLen); resPdu -> uiMsgType = ENUM_MSG_TYPE_FLUSH_FRIEND_RESPOND; for(int i = 0; i * 2 < strList.size(); ++ i) { strncpy((char*)(resPdu -> caMsg) + 36 * i, strList.at(i * 2).toStdString().c_str(), 32); strncpy((char*)(resPdu -> caMsg) + 36 * i + 32, strList.at(i * 2 + 1).toStdString().c_str(), 4); } return resPdu; }

查数据库,同样因为好友关系是双向的,而数据库中存储的是单向的,所以dboperate.cpp逻辑需要考虑仔细。

cpp
展开代码
QStringList DBOperate::handleFlushFriendRequest(const char *name) { QStringList strFriendList; strFriendList.clear(); // 清除内容 if (NULL == name) { return strFriendList; } // 获取请求方name对应的id QString strQuery = QString("select id from userInfo where name = \'%1\' and online = 1 ").arg(name); QSqlQuery query; int iId = -1; // 请求方name对应的id query.exec(strQuery); if (query.next()) { iId = query.value(0).toInt(); } // 查询好友信息表与用户信息表获取好友列表 strQuery = QString("select name, online from userInfo " "where id in " "((select friendId from friendinfo " "where id = %1) " "union " "(select id from friendinfo " "where friendId = %2))").arg(iId).arg(iId); query.exec(strQuery); while(query.next()) { char friName[32]; char friOnline[4]; strncpy(friName, query.value(0).toString().toStdString().c_str(), 32); strncpy(friOnline, query.value(1).toString().toStdString().c_str(), 4); strFriendList.append(friName); strFriendList.append(friOnline); // qDebug() << "好友信息 " << friName << " " << friOnline; // qDebug() << strFriendList; } return strFriendList; // 返回查询到所有在线用户的姓名 }

客户端收到服务器的响应之后,处理响应内容,将所有好友信息展示在好友页面上。

tcpclient.cpp

cpp
展开代码
void TcpClient::receiveMsg() { ... case ENUM_MSG_TYPE_FLUSH_FRIEND_RESPOND: // 刷新好友响应 { OperateWidget::getInstance().getPFriend()->updateFriendList(pdu); break; }

friend.cpp中将获取的数据显示在好友列表中

cpp
展开代码
void Friend::updateFriendList(PDU *pdu) { if(NULL == pdu) { return ; } uint uiSize = pdu -> uiMsgLen / 36; // 注意是36,32 name + 4 online char caName[32] = {'\0'}; char caOnline[4] = {'\0'}; m_pFriendLW -> clear(); // 清除好友列表原有数据 for(uint i = 0; i < uiSize; ++ i) { memcpy(caName, (char*)(pdu -> caMsg) + i * 36, 32); memcpy(caOnline, (char*)(pdu -> caMsg) + 32 + i * 36, 4); // qDebug() << "客户端好友" << caName << " " << caOnline; m_pFriendLW -> addItem(QString("%1\t%2").arg(caName) .arg(strcmp(caOnline, "1") == 0?"在线":"离线")); } }

最后实现效果:

在用户登录之后跳转到好友页面应自动刷新一次好友列表

用户登录之后,会从登录页面跳转到主页面(默认显示好友子页面),所以用户已经登录并跳转到好友子页面时应该自动刷新一次好友列表。

即在tcpclient.cpp的接收响应消息的登录响应部分添加如下逻辑

cpp
展开代码
void TcpClient::receiveMsg() { ... // 根据不同消息类型,执行不同操作 switch(pdu -> uiMsgType) { case ENUM_MSG_TYPE_LOGIN_RESPOND: // 登录响应 { if(0 == strcmp(pdu -> caData, LOGIN_OK)) { ... // 默认请求一次好友列表 OperateWidget::getInstance().getPFriend() -> flushFriendList(); this -> hide(); // 隐藏登陆页面 }

知识点:memcpy和strncpy区别

memcpy(des, source, n) 对比 strncpy(des, source, n)

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

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

删除好友

客户端在Friend中绑定删除按钮信号与对应槽函数

cpp
展开代码
// 绑定删除好友按钮与对应事件 connect(m_pDelFriendPB, SIGNAL(clicked(bool)), this, SLOT(deleteFriend()));

实现槽函数deleteFriend()功能

cpp
展开代码
void Friend::deleteFriend() { if(NULL == m_pFriendLW -> currentItem()) // 如果没有选中好友 { return ; } QString friName = m_pFriendLW -> currentItem() -> text(); // 获得选中的好友用户名 friName = friName.split("\t")[0]; QString loginName = TcpClient::getInstance().getStrName(); // 登录用户用户名 qDebug() << friName; PDU* pdu = mkPDU(0); pdu -> uiMsgType = ENUM_MSG_TYPE_DELETE_FRIEND_REQUEST; strncpy(pdu -> caData, friName.toStdString().c_str(), 32); strncpy(pdu -> caData + 32, loginName.toStdString().c_str(), 32); TcpClient::getInstance().getTcpSocket().write((char*)pdu, pdu -> uiPDULen); free(pdu); pdu = NULL; }

服务器接收客户端请求,更新数据库内容,所以dboperate.cpp实现数据库操作

cpp
展开代码
bool DBOperate::handleDeleteFriend(const char *deletedName, const char *sourceName) { if(deletedName == NULL || sourceName == NULL) { return false; } // 先查出来deletedName和sourceName对应的id int iDelId = -1; int iSouId = -1; QString strQuery = QString("select id from userInfo where name in (\'%1\', \'%2\') ").arg(deletedName).arg(sourceName); QSqlQuery query; query.exec(strQuery); if(query.next()) { iDelId = query.value(0).toInt(); } if(query.next()) { iSouId = query.value(0).toInt(); } // 删除好友信息表中两个id之间的好友关系 strQuery = QString("delete from friendInfo where id in (\'%1\', \'%2\') and friendId in (\'%3\', \'%4\') ") .arg(iDelId).arg(iSouId).arg(iDelId).arg(iSouId); qDebug() << strQuery; return query.exec(strQuery); }

基于该数据库操作,服务器可以在mytcpsocket.cpp中实现响应操作

cpp
展开代码
// 删除好友请求 PDU* handleDeleteFriendRequest(PDU* pdu) { char deletedName[32] = {'\0'}; char sourceName[32] = {'\0'}; // 拷贝读取的信息 strncpy(deletedName, pdu -> caData, 32); strncpy(sourceName, pdu -> caData + 32, 32); bool ret = DBOperate::getInstance().handleDeleteFriend(deletedName, sourceName); // 给请求删除方消息提示,以返回值形式 PDU *resPdu = mkPDU(0); resPdu -> uiMsgType = ENUM_MSG_TYPE_DELETE_FRIEND_RESPOND; if(ret) { strncpy(resPdu -> caData, DEL_FRIEND_OK, 32); } else { strncpy(resPdu -> caData, DEL_FRIEND_FAILED, 32); } // 给被删除方消息提示,如果在线的话 MyTcpServer::getInstance().forwardMsg(deletedName, pdu); return resPdu; }

发送删除好友请求的客户端接收到响应后处理逻辑:

cpp
展开代码
case ENUM_MSG_TYPE_DELETE_FRIEND_RESPOND: // 删除好友响应 { QMessageBox::information(this, "删除好友", pdu -> caData); break; }

被删除好友的客户端如果在线,也会收到响应:

cpp
展开代码
case ENUM_MSG_TYPE_DELETE_FRIEND_REQUEST: // 处理服务器转发过来的删除好友请求 { char sourceName[32]; // 获取发送方用户名 strncpy(sourceName, pdu -> caData + 32, 32); QMessageBox::information(this, "删除好友", QString("%1 已删除与您的好友关系!").arg(sourceName)); break; }

好友功能-私聊群聊

私聊

首先,创建一个私聊的页面作为用户与其他用户私聊的页面。

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

privateChatWid.h

cpp
展开代码
QString m_strChatName; // 聊天对象用户名 QString m_strLoginName; // 请求用户名

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

privateChatWid.cpp

cpp
展开代码
// 发送消息按钮的槽函数 void PrivateChatWid::on_sendMsg_pb_clicked() { QString strSendMsg = ui -> inputMsg_le -> text(); if(strSendMsg.isEmpty()) { QMessageBox::warning(this, "私聊", "发送消息不能为空!"); return ; } // 显示在自己showMsgTE窗口上 ui -> inputMsg_le -> clear(); // 清空输入框内容 ui -> showMsg_te -> append(QString("%1 : %2").arg(m_strLoginName).arg(strSendMsg)); // 发送消息给服务器来转发给对方 PDU *pdu = mkPDU(strSendMsg.size()); pdu -> uiMsgType = ENUM_MSG_TYPE_PRIVATE_CHAT_REQUEST; strncpy(pdu -> caData, m_strChatName.toStdString().c_str(), 32); // 目标用户名 strncpy(pdu -> caData + 32, m_strLoginName.toStdString().c_str(), 32); // 请求方用户名 strncpy((char*)pdu -> caMsg, strSendMsg.toStdString().c_str(), strSendMsg.size()); // 发送内容 TcpClient::getInstance().getTcpSocket().write((char*)pdu, pdu -> uiPDULen); free(pdu); pdu = NULL; }

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

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

然后我们需要实现在好友页面friend中,选中好友列表中的好友后再点击私聊按钮的槽函数privateChat()。

friend.cpp

cpp
展开代码
void Friend::privateChat() { if(NULL == m_pFriendLW -> currentItem()) // 如果没有选中好友 { return ; } QString friName = m_pFriendLW -> currentItem() -> text(); // 获得选中的好友用户名 friName = friName.split("\t")[0]; QString loginName = TcpClient::getInstance().getStrName(); // 登录用户用户名 PrivateChatWid *priChat = searchPriChatWid(friName.toStdString().c_str()); if(priChat == NULL) // 没找到该窗口,说明之前没有创建私聊窗口 { priChat = new PrivateChatWid; priChat -> setStrChatName(friName); priChat -> setStrLoginName(loginName); priChat -> setPriChatTitle(friName.toStdString().c_str()); m_priChatWidList.append(priChat); // 添加入该客户端私聊List } if(priChat->isHidden()) // 如果窗口被隐藏,则让其显示 { priChat->show(); } if(priChat -> isMinimized()) // 如果窗口被最小化了 { // qDebug() << "窗口被最小化了"; priChat->showNormal(); } }

当服务器收到客户端私聊好友的消息之后,如果私聊对象不存在则返回客户端提示信息,如果存在则将消息转发给目标用户。

mytcpsocket.cpp

cpp
展开代码
// 私聊发送消息请求 PDU* handlePrivateChatRequest(PDU* pdu) { char chatedName[32] = {'\0'}; char sourceName[32] = {'\0'}; // 拷贝读取的信息 strncpy(chatedName, pdu -> caData, 32); strncpy(sourceName, pdu -> caData + 32, 32); qDebug() << "handlePrivateChatRequest " << chatedName << " " << sourceName; PDU* resPdu = NULL; // 转发给对方消息 0对方存在不在线,1对方存在在线 bool ret = MyTcpServer::getInstance().forwardMsg(chatedName, pdu); // 发送失败则给发送者消息 if(!ret)// 0对方不在线 { resPdu = mkPDU(0); resPdu -> uiMsgType = ENUM_MSG_TYPE_PRIVATE_CHAT_RESPOND; strcpy(resPdu -> caData, PRIVATE_CHAT_OFFLINE); } return resPdu; }

客户端如果作为私聊对象,会受到私聊消息,因此其也应该能够处理私聊消息。

tcpclient.cpp

cpp
展开代码
case ENUM_MSG_TYPE_PRIVATE_CHAT_REQUEST: // 私聊好友消息请求(接收消息) { char sourceName[32]; // 获取发送方用户名 strncpy(sourceName, pdu -> caData + 32, 32); PrivateChatWid *priChatW = OperateWidget::getInstance().getPFriend()->searchPriChatWid(sourceName); if(NULL == priChatW) { priChatW = new PrivateChatWid; priChatW -> setStrChatName(sourceName); priChatW -> setStrLoginName(m_strName); priChatW -> setPriChatTitle(sourceName); OperateWidget::getInstance().getPFriend()->insertPriChatWidList(priChatW); } priChatW->updateShowMsgTE(pdu); priChatW->show(); if(priChatW->isMinimized()) // 如果窗口被最小化了 { priChatW->showNormal(); } break; }

客户端需要将接收到的消息展示在私聊窗口

privateChatWid.cpp

cpp
展开代码
void PrivateChatWid::updateShowMsgTE(PDU *pdu) { if(NULL == pdu) { return ; } char caSendName[32] = {'\0'}; strncpy(caSendName, pdu -> caData + 32, 32); QString strMsg = QString("%1 : %2").arg(caSendName).arg((char*)pdu -> caMsg); ui -> showMsg_te -> append(strMsg); }

群聊

暂时只是实现简单的群聊功能,即所有在线的好友都会收到消息。

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

friend.cpp

cpp
展开代码
void Friend::groupChatSendMsg() { QString strMsg = m_pGroupInputLE -> text(); if(strMsg.isEmpty()) { QMessageBox::warning(this, "群聊", "发送信息不能为空!"); return ; } m_pGroupInputLE->clear(); // 清空输入框 m_pGroupShowMsgTE->append(QString("%1 : %2").arg(TcpClient::getInstance().getStrName()).arg(strMsg)); PDU* pdu = mkPDU(strMsg.size() + 1); pdu -> uiMsgType = ENUM_MSG_TYPE_GROUP_CHAT_REQUEST; strncpy(pdu -> caData, TcpClient::getInstance().getStrName().toStdString().c_str(), 32); strncpy((char*)(pdu -> caMsg), strMsg.toStdString().c_str(), strMsg.size()); TcpClient::getInstance().getTcpSocket().write((char*)pdu, pdu->uiPDULen); free(pdu); pdu = NULL; }

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

mytcpsocket.cpp

cpp
展开代码
// 群聊请求处理 void handleGroupChatRequest(PDU* pdu) { QStringList strList = DBOperate::getInstance().handleFlushFriend(pdu->caData); // 查询请求,查询数据库所有在线用户 for(QString strName:strList) { MyTcpServer::getInstance().forwardMsg(strName, pdu); } }

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

tcpclient.cpp

cpp
展开代码
case ENUM_MSG_TYPE_GROUP_CHAT_REQUEST: // 群发好友信息请求(接收消息) { OperateWidget::getInstance().getPFriend()->updateGroupShowMsgTE(pdu); break; }

friend.cpp提供更新群聊展示消息窗口的函数:

cpp
展开代码
void Friend::updateGroupShowMsgTE(PDU *pdu) { QString strMsg = QString("%1 : %2").arg(pdu->caData).arg((char*)pdu->caMsg); m_pGroupShowMsgTE -> append(strMsg); }

文件功能-文件夹操作

页面设计

按照好友页面设计方法,在fileSystem中同样用代码实现文件的页面布局,效果如下:

创建文件夹

注册时按用户名分配文件夹

注册逻辑之前已经完成了,所以我们直接在原来基础上添加为新用户新建文件夹逻辑即可。

cpp
展开代码
// 处理注册请求并返回响应PDU PDU* handleRegistRequest(PDU* pdu) { char caName[32] = {'\0'}; char caPwd[32] = {'\0'}; // 拷贝读取的信息 strncpy(caName, pdu -> caData, 32); strncpy(caPwd, pdu -> caData + 32, 32); bool ret = DBOperate::getInstance().handleRegist(caName, caPwd); // 处理请求,插入数据库 // 响应客户端 PDU *resPdu = mkPDU(0); // 响应消息 resPdu -> uiMsgType = ENUM_MSG_TYPE_REGIST_RESPOND; if(ret) { strcpy(resPdu -> caData, REGIST_OK); // 注册成功,为新用户按用户名创建文件夹 QDir dir; ret = dir.mkdir(QString("./%1").arg(caName)); qDebug() << "创建新用户文件夹" << ret; } if(!ret) { strcpy(resPdu -> caData, REGIST_FAILED); } return resPdu; }

将服务器FileSystem根目录写入配置文件,改成路径拼接形式

在myTcpServer中添加m_strRootPath的属性,然后在TcpServer的load_config函数中添加读取路径的代码,并且设置为m_strRootPath的值。

load_config

cpp
展开代码
void TcpServer::loadConfig() { QFile file(":/server.config"); // 文件对象,读取资源文件 ':' + "前缀" + "文件名" if(file.open(QIODevice::ReadOnly)) // file.open() 参数:打开方式:只读(注意,这里只读是写在QIODevice下的枚举,所以调用要声明命名空间) 返回true则打开成功 { QByteArray baData = file.readAll(); // 读出所有数据,返回字节数组QByteArray QString strData = baData.toStdString().c_str(); // 转换为字符串 注意std::string不能自动转为QString,还需转为char* file.close(); strData.replace("\r\n", " "); // 替换IP地址、端口号与服务器文件系统根地址之间\r\n QStringList strList = strData.split(" "); m_strIP = strList.at(0); m_usPort = strList.at(1).toUShort(); // 无符号短整型 MyTcpServer::getInstance().setStrRootPath(strList.at(2)); // 设置文件系统根目录 qDebug() << "IP: " << m_strIP << " port: " << m_usPort << " root path: " << MyTcpServer::getInstance().getStrRootPath(); // 打印结果 } else // 文件打开失败则弹出提示窗口 { QMessageBox::critical(this, "open config", "open config failed"); // 严重 } }

那么上面设置注册用户的文件目录的代码

ret = dir.mkdir(QString("./%1").arg(caName));

就改为:

ret = dir.mkdir(QString("%1/%2").arg(m_strRootPath).arg(caName));


测试

如果说,创建一个用户名为f的新用户,运行结果如下:

登录时获取属于用户的根目录

添加用户登录成功之后,服务器返回用户的文件系统根目录的代码

myTcpSocket.cpp

cpp
展开代码
// 处理登录请求并返回响应PDU PDU* handleLoginRequest(PDU* pdu, QString& m_strName) { ... // 响应客户端 PDU *resPdu = NULL; // 响应消息 if(ret) { QString strUserRootPath = QString("%1/%2") .arg(MyTcpServer::getInstance().getStrRootPath()).arg(caName); // 用户文件系统根目录 qDebug() << "登录用户的路径:" << strUserRootPath; resPdu = mkPDU(strUserRootPath.size() + 1); memcpy(resPdu -> caData, LOGIN_OK, 32); memcpy(resPdu -> caData + 32, caName, 32); // 将登录后的用户名传回,便于tcpclient确认已经登陆的用户名 // 在登陆成功时,记录Socket对应的用户名 m_strName = caName; // qDebug() << "m_strName: " << m_strName; // 返回用户的根目录 strncpy((char*)resPdu -> caMsg, strUserRootPath.toStdString().c_str(), strUserRootPath.size() + 1); } else { resPdu = mkPDU(0); strcpy(resPdu -> caData, LOGIN_FAILED); } resPdu -> uiMsgType = ENUM_MSG_TYPE_LOGIN_RESPOND; qDebug() << "登录处理:" << resPdu -> uiMsgType << " " << resPdu ->caData << " " << resPdu ->caData + 32; return resPdu; }

同样,客户端tcpClient中也需要添加用户文件系统根目录m_strRootPath和当前文件目录m_strCurPath两个属性,便于之后文件系统操作。然后在获得服务器登录成功之后的响应,对两个属性进行赋值。

tcpClient.cpp

cpp
展开代码
case ENUM_MSG_TYPE_LOGIN_RESPOND: // 登录响应 { if(0 == strcmp(pdu -> caData, LOGIN_OK)) { // QMessageBox::information(this, "登录", LOGIN_OK); char caName[32] = {'\0'}; strncpy(caName, pdu -> caData + 32, 32); // 设置已登录用户名 // 设置用户根目录和当前目录 m_strRootPath = QString((char*)pdu -> caMsg); qDebug() << "用户根目录 " << m_strRootPath; m_strCurPath = m_strRootPath; m_strName = caName; qDebug() << "用户已登录:" << caName << " strName:" << m_strName; // 登录跳转 OperateWidget::getInstance().setUserLabel(caName); // 设置主页面用户信息 OperateWidget::getInstance().show(); // 显示主操作页面 // 默认请求一次好友列表 OperateWidget::getInstance().getPFriend() -> flushFriendList(); this -> hide(); // 隐藏登陆页面 } else if(0 == strcmp(pdu -> caData, LOGIN_FAILED)) { QMessageBox::warning(this, "登录", LOGIN_FAILED); } break; }

创建文件夹按钮实现

  1. 在客户端,fileSystem中绑定创建文件夹按钮与对应槽函数
cpp
展开代码
void FileSystem::createDir() { QString strDirName = QInputDialog::getText(this, "新建文件夹", "文件夹名:"); // 获得文件夹名 QString strCurPath = TcpClient::getInstance().getStrCurPath(); if(strDirName.isEmpty()) { QMessageBox::warning(this, "新建文件夹", "文件夹名字不能为空!"); return ; } PDU *pdu = mkPDU(strCurPath.size() + 1); pdu -> uiMsgType = ENUM_MSG_TYPE_CREATE_DIR_REQUEST; strncpy(pdu -> caData, strDirName.toStdString().c_str(), strDirName.size()); memcpy((char*)pdu ->caMsg, strCurPath.toStdString().c_str(), strCurPath.size()); TcpClient::getInstance().getTcpSocket().write((char*)pdu, pdu -> uiPDULen); free(pdu); pdu = NULL; }
  1. 在服务器端,myTcpSocket中实现创建文件夹的一些检测(路径是否合理、文件夹是否已存在等),如果合理则新建文件夹,并返回响应消息。
cpp
展开代码
// 创建文件夹请求处理 PDU* handleCreateDirRequest(PDU* pdu) { char caDirName[32]; char caCurPath[pdu -> uiMsgLen]; strncpy(caDirName, pdu -> caData, 32); strncpy(caCurPath, (char*)pdu -> caMsg, pdu -> uiMsgLen); QString strDir = QString("%1/%2").arg(caCurPath).arg(caDirName); QDir dir; PDU *resPdu = mkPDU(0); resPdu -> uiMsgType = ENUM_MSG_TYPE_CREATE_DIR_RESPOND; qDebug() << "创建文件夹:" << strDir; if(dir.exists(caCurPath)) // 路径存在 { if(dir.exists(strDir)) // 文件夹已经存在 { strncpy(resPdu -> caData, CREATE_DIR_EXIST, 32); } else { dir.mkdir(strDir); // 创建文件夹 strncpy(resPdu -> caData, CREATE_DIR_OK, 32); } } else // 路径不存在 { strncpy(resPdu -> caData, PATH_NOT_EXIST, 32); } return resPdu; }
  1. 在客户端,收到服务器的消息之后,通过socketClient进行处理并显示提示信息。

刷新文件夹查看文件

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

  1. 在客户端,在filesystem中首先绑定刷新文件夹按钮与相应槽函数,然后实现槽函数逻辑,点击之后能够向服务器发送查看请求。
cpp
展开代码
void FileSystem::flushDir() { QString strCurPath = TcpClient::getInstance().getStrCurPath(); PDU *pdu = mkPDU(strCurPath.size() + 1); pdu -> uiMsgType = ENUM_MSG_TYPE_FLUSH_DIR_REQUEST; memcpy((char*)pdu ->caMsg, strCurPath.toStdString().c_str(), strCurPath.size()); TcpClient::getInstance().getTcpSocket().write((char*)pdu, pdu -> uiPDULen); free(pdu); pdu = NULL; }
  1. 服务器收到请求之后,在mytcpsocket中要实现响应处理,读取路径下所有文件的文件信息,然后返回给客户端。
cpp
展开代码
// 刷新文件夹请求处理 PDU* handleFlushDirRequest(PDU* pdu) { char caCurDir[pdu -> uiMsgLen]; memcpy(caCurDir, (char*)pdu -> caMsg, pdu -> uiMsgLen); qDebug() << "刷新文件夹:" << caCurDir; QDir dir; PDU* resPdu = NULL; if(!dir.exists(caCurDir)) // 请求文件夹不存在 { resPdu = mkPDU(0); strncpy(resPdu -> caData, PATH_NOT_EXIST, 32); } else // 存在 { dir.setPath(caCurDir); // 设置为当前目录 QFileInfoList fileInfoList = dir.entryInfoList(); // 获取当前目录下所有文件 int iFileNum = fileInfoList.size(); resPdu = mkPDU(sizeof(FileInfo) * iFileNum); FileInfo *pFileInfo = NULL; // 创建一个文件信息结构体指针,方便之后遍历PDU空间来赋值 for(int i = 0; i < iFileNum; ++ i) { pFileInfo = (FileInfo*)(resPdu -> caMsg) + i; // 通过指针指向,直接修改PDU空间值,每次偏移FileInfo大小 memcpy(pFileInfo -> caName, fileInfoList[i].fileName().toStdString().c_str(), fileInfoList[i].fileName().size()); pFileInfo -> bIsDir = fileInfoList[i].isDir(); pFileInfo -> uiSize = fileInfoList[i].size(); QDateTime dtLastTime = fileInfoList[i].lastModified(); // 获取文件最后修改时间 QString strLastTime = dtLastTime.toString("yyyy/MM/dd hh:mm"); memcpy(pFileInfo -> caTime, strLastTime.toStdString().c_str(), strLastTime.size()); qDebug() << "文件信息:" << pFileInfo -> caName << " " << pFileInfo -> bIsDir << " " << pFileInfo -> uiSize << " " << pFileInfo -> caTime; } } resPdu -> uiMsgType = ENUM_MSG_TYPE_FLUSH_DIR_RESPOND; return resPdu; }

注意,其中值得注意的点是,我们首先创建了一个FileInfo结构体的指针,然后每次通过这个指针指向resPdu → caMsg的地址空间,由于resPdu → caMsg被强转为FileInfo*类型,所以每次移动也是以FileInfo大小为单位移动,然后后面我们直接通过FileInfo指针来操作PDU的caMsg的空间,操作更为方便。 在之后客户端接收到这个消息之后也可以以这个思路来访问caMsg中数据。

  1. 在客户端,我们接收到服务器响应过来的数据,然后将文件信息展示在FileList中。

TcpClient.cpp

cpp
展开代码
case ENUM_MSG_TYPE_FLUSH_DIR_RESPOND: // 刷新文件夹响应 { OperateWidget::getInstance().getPFileSystem()->updateFileList(pdu); break; }

FileSystem.cpp

cpp
展开代码
void FileSystem::updateFileList(PDU *pdu) { if(NULL == pdu) { return ; } uint uiFileNum = pdu -> uiMsgLen / sizeof(FileInfo); // 文件数 FileInfo *pFileInfo = NULL; // 通过FileInfo指针依此访问caMsg中数据 QListWidgetItem *pItem = NULL; m_pFileListW -> clear(); // 清除文件列表原有数据 for(uint i = 0; i < uiFileNum; ++ i) { pFileInfo = (FileInfo*)(pdu -> caMsg) + i; pItem = new QListWidgetItem; if(pFileInfo ->bIsDir) // 根据文件类型设置图标 { pItem->setIcon(QIcon(QPixmap(":/icon/dir.jpeg"))); } else { pItem->setIcon(QIcon(QPixmap(":/icon/file.jpeg"))); } // 文件名 文件大小 最后修改时间 形式展示文件 pItem ->setText(QString("%1\t%2\t%3").arg(pFileInfo->caName) .arg(pFileInfo->uiSize).arg(pFileInfo->caTime)); m_pFileListW->addItem(pItem); } }

删除文件或文件夹

注,这里将原视频中删除文件和删除文件夹两个按钮合为一个按钮,因为逻辑一致

  1. 在客户端,在filesystem中首先绑定删除按钮与相应槽函数,然后实现槽函数逻辑,点击之后能够向服务器发送查看请求。
cpp
展开代码
void FileSystem::delFileOrDir() { QString strCurPath = TcpClient::getInstance().getStrCurPath(); QListWidgetItem *qItem = m_pFileListW->currentItem(); // 获得当前选中文件 if(NULL == qItem) { QMessageBox::warning(this, "删除文件", "请选中需要删除的文件"); return ; } QString strFileName = qItem->text().split('\t')[0]; // 获取文件名 QString strDelPath = QString("%1/%2").arg(strCurPath).arg(strFileName); // 要删除文件路径 qDebug() << "删除文件:" << strDelPath; PDU *pdu = mkPDU(strDelPath.size() + 1); pdu -> uiMsgType = ENUM_MSG_TYPE_DELETE_FILE_REQUEST; memcpy((char*)pdu ->caMsg, strDelPath.toStdString().c_str(), strDelPath.size()); TcpClient::getInstance().getTcpSocket().write((char*)pdu, pdu -> uiPDULen); free(pdu); pdu = NULL; }
  1. 服务器收到请求之后,在mytcpsocket中要实现响应处理,合理性判断,如果合理则删除文件夹/文件,然后返回给客户端响应信息。
cpp
展开代码
// 删除文件或文件夹处理 PDU* handleDelFileOrDirRequest(PDU* pdu) { PDU* resPdu = mkPDU(0); char strDelPath[pdu -> uiMsgLen]; memcpy(strDelPath, (char*)pdu -> caMsg, pdu -> uiMsgLen); qDebug() << "删除文件:" << strDelPath; QDir dir; resPdu -> uiMsgType = ENUM_MSG_TYPE_DELETE_FILE_RESPOND; if(!dir.exists(strDelPath)) // 路径不存在 { strncpy(resPdu -> caData, PATH_NOT_EXIST, 32); } else { bool ret = false; QFileInfo fileInfo(strDelPath); if(fileInfo.isDir()) // 是文件目录 { dir.setPath(strDelPath); ret = dir.removeRecursively(); } else if(fileInfo.isFile()) { ret = dir.remove(strDelPath); } if(ret) { strncpy(resPdu -> caData, DELETE_FILE_OK, 32); } else { strncpy(resPdu -> caData, DELETE_FILE_FAILED, 32); } } qDebug() << resPdu -> caData; return resPdu; }
  1. 客户端,收到响应之后,tcpclient显示提示信息
cpp
展开代码
case ENUM_MSG_TYPE_DELETE_FILE_RESPOND: // 删除文件或文件夹响应 { QMessageBox::information(this, "删除文件", pdu -> caData); break; }

文件功能-文件操作

文件重命名

基本实现逻辑与其他操作相同,这里只展示服务器mytcpsocket接收请求之后的处理的代码。

cpp
展开代码
// 重命名文件或文件夹请求处理 PDU* handleRenameFileRequest(PDU* pdu) { PDU* resPdu = mkPDU(0); char caCurPath[pdu -> uiMsgLen]; char caOldName[32]; // 旧文件名 char caNewName[32]; // 新文件名 memcpy(caCurPath, (char*)pdu -> caMsg, pdu -> uiMsgLen); strncpy(caOldName, pdu -> caData, 32); strncpy(caNewName, pdu -> caData + 32, 32); qDebug() << "重命名文件:" << caCurPath << " " << caOldName << " -> " << caNewName; QDir dir; resPdu -> uiMsgType = ENUM_MSG_TYPE_RENAME_FILE_RESPOND; dir.setPath(caCurPath); if(dir.rename(caOldName, caNewName)) { strncpy(resPdu -> caData, RENAME_FILE_OK, 32); } else { strncpy(resPdu -> caData, RENAME_FILE_FAILED, 32); } qDebug() << resPdu -> caData; return resPdu; }

进入文件夹

  1. 客户端发送请求

FileSystem

cpp
展开代码
void FileSystem::entryDir(const QModelIndex &index) { QString strCurPath = TcpClient::getInstance().getStrCurPath(); QString strFileName = index.data().toString(); strFileName = strFileName.split('\t')[0]; // 获得双击的文件名 QString strEntryPath = QString("%1/%2").arg(strCurPath).arg(strFileName); qDebug() << "进入 " << strEntryPath; m_strTryEntryDir = strEntryPath; // 将想要进入的目录临时存储下来 PDU* pdu = mkPDU(strEntryPath.size() + 1); pdu -> uiMsgType = ENUM_MSG_TYPE_ENTRY_DIR_REQUEST; memcpy((char*)pdu -> caMsg, strEntryPath.toStdString().c_str(), strEntryPath.size()); TcpClient::getInstance().getTcpSocket().write((char*)pdu, pdu -> uiPDULen); free(pdu); pdu = NULL; }
  1. 服务器处理请求

mytcpsocket

cpp
展开代码
// 进入文件夹请求处理 PDU* handleEntryDirRequest(PDU* pdu) { char strEntryPath[pdu -> uiMsgLen]; // 进入文件夹路径 memcpy(strEntryPath, (char*)pdu -> caMsg, pdu -> uiMsgLen); qDebug() << "进入 " << strEntryPath; PDU* resPdu = NULL; QDir dir(strEntryPath); if(!dir.exists()) // 请求文件夹不存在 { resPdu = mkPDU(0); strncpy(resPdu -> caData, PATH_NOT_EXIST, 32); } else // 存在 { QFileInfo fileInfo(strEntryPath); if(!fileInfo.isDir()) // 不是文件夹 { resPdu = mkPDU(0); strncpy(resPdu -> caData, ENTRY_DIR_FAILED, 32); } else { resPdu = handleFlushDirRequest(pdu); // 通过该函数获取文件夹下内容 } } resPdu -> uiMsgType = ENUM_MSG_TYPE_ENTRY_DIR_RESPOND; qDebug() << "1 resPdu -> caData :" << resPdu -> caData; if(strcmp(resPdu -> caData, FLUSH_DIR_OK) == 0) { strncpy(resPdu -> caData, ENTRY_DIR_OK, 32); qDebug() << "2 resPdu -> caData :" << resPdu -> caData; } else { strncpy(resPdu -> caData, ENTRY_DIR_FAILED, 32); qDebug() << "2 resPdu -> caData :" << resPdu -> caData; } return resPdu; }
  1. 客户端接收响应

tcpclient

cpp
展开代码
case ENUM_MSG_TYPE_ENTRY_DIR_RESPOND: // 进入文件夹响应 { qDebug() << "进入文件夹响应:" << pdu -> caData; if(strcmp(ENTRY_DIR_OK, pdu -> caData) == 0) { OperateWidget::getInstance().getPFileSystem() -> updateFileList(pdu); // 刷新文件列表 QString entryPath = OperateWidget::getInstance().getPFileSystem()->strTryEntryDir(); if(!entryPath.isEmpty()) { m_strCurPath = entryPath; OperateWidget::getInstance().getPFileSystem()->clearStrTryEntryDir(); // 清空m_strTryEntryDir qDebug() << "当前路径:" << m_strCurPath; } } else { QMessageBox::warning(this, "进入文件夹", pdu -> caData); } break; }

返回上一级

  1. 客户端发送请求

FileSystem

cpp
展开代码
void FileSystem::returnPreDir() { QString strCurPath = TcpClient::getInstance().getStrCurPath(); QString strRootPath = TcpClient::getInstance().getStrRootPath(); if(strCurPath == strRootPath) { QMessageBox::warning(this, "返回上一目录", "已经是根目录!"); return ; } int index = strCurPath.lastIndexOf("/"); strCurPath = strCurPath.remove(index, strCurPath.size() - index); qDebug() << "返回到" << strCurPath; m_strTryEntryDir = strCurPath; // 临时存储目标目录 // 给服务器发消息 PDU* pdu = mkPDU(strCurPath.size() + 1); pdu -> uiMsgType = ENUM_MSG_TYPE_PRE_DIR_REQUEST; memcpy((char*)pdu -> caMsg, strCurPath.toStdString().c_str(), strCurPath.size()); TcpClient::getInstance().getTcpSocket().write((char*)pdu, pdu -> uiPDULen); free(pdu); pdu = NULL; }
  1. 服务器处理请求

mytcpsocket

cpp
展开代码
// 返回上一目录请求 PDU* handlePreDirRequest(PDU* pdu) { char strPrePath[pdu -> uiMsgLen]; // 进入文件夹路径 memcpy(strPrePath, (char*)pdu -> caMsg, pdu -> uiMsgLen); qDebug() << "上一目录: " << strPrePath; PDU* resPdu = NULL; QDir dir(strPrePath); if(!dir.exists()) // 请求文件夹不存在 { resPdu = mkPDU(0); strncpy(resPdu -> caData, PATH_NOT_EXIST, 32); } else // 存在 { resPdu = handleFlushDirRequest(pdu); // 通过该函数获取文件夹下内容 } resPdu -> uiMsgType = ENUM_MSG_TYPE_PRE_DIR_RESPOND; qDebug() << "1 resPdu -> caData :" << resPdu -> caData; if(strcmp(resPdu -> caData, FLUSH_DIR_OK) == 0) { strncpy(resPdu -> caData, PRE_DIR_OK, 32); qDebug() << "2 resPdu -> caData :" << resPdu -> caData; } else { strncpy(resPdu -> caData, PRE_DIR_FAILED, 32); qDebug() << "2 resPdu -> caData :" << resPdu -> caData; } return resPdu; }
  1. 客户端接收响应

tcpclient

cpp
展开代码
case ENUM_MSG_TYPE_PRE_DIR_RESPOND: // 上一目录响应 { qDebug() << "上一文件夹响应:" << pdu -> caData; if(strcmp(PRE_DIR_OK, pdu -> caData) == 0) { OperateWidget::getInstance().getPFileSystem() -> updateFileList(pdu); // 刷新文件列表 QString entryPath = OperateWidget::getInstance().getPFileSystem()->strTryEntryDir(); if(!entryPath.isEmpty()) { m_strCurPath = entryPath; OperateWidget::getInstance().getPFileSystem()->clearStrTryEntryDir(); // 清空m_strTryEntryDir qDebug() << "当前路径:" << m_strCurPath; } } else { QMessageBox::warning(this, "上一文件夹", pdu -> caData); } break; }

上传文件

注意,上传文件需要分为两次请求:

  1. 客户端发送”当前路径和上传文件名“,服务器接收数据然后创建文件,再响应客户端
  2. 客户端收到服务器响应无误之后,再上传文件内容

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

cpp
展开代码
void FileSystem::uploadFile() { QString strCurPath = TcpClient::getInstance().getStrCurPath(); // 当前目录 m_strUploadFilePath = QFileDialog::getOpenFileName(); // 将上传的文件的路径 qDebug() << m_strUploadFilePath; if(m_strUploadFilePath.isEmpty()) { QMessageBox::warning(this, "上传文件", "请选择需要上传的文件!"); return ; } // 获取上传文件的名字作为远程服务器新建文件的名字 int index = m_strUploadFilePath.lastIndexOf('/'); QString strFileName = m_strUploadFilePath.right(m_strUploadFilePath.size() - index - 1); // 获得文件大小 QFile file(m_strUploadFilePath); qint64 fileSize = file.size(); // 获得文件大小 qDebug() << "上传文件:" << strFileName << " " << fileSize; PDU* pdu = mkPDU(strCurPath.size() + 1); pdu -> uiMsgType = ENUM_MSG_TYPE_UPLOAD_FILE_REQUEST; memcpy(pdu -> caMsg, strCurPath.toStdString().c_str(), strCurPath.size()); sprintf(pdu -> caData, "%s %lld", strFileName.toStdString().c_str(), fileSize); TcpClient::getInstance().getTcpSocket().write((char*)pdu, pdu -> uiPDULen); free(pdu); pdu = NULL; }

服务器接收到上传文件请求之后,将文件的名字、大小、路径等信息进行保存,然后设置状态为接收上传文件,便于之后接收文件。

mytcpsocket

cpp
展开代码
// 上传文件请求处理 PDU* handleUploadFileRequest(PDU* pdu, TransFile* transFile) { char caCurPath[pdu -> uiMsgLen]; char caFileName[32] = {'\0'}; qint64 fileSize = 0; strncpy(caCurPath, (char*)pdu -> caMsg, pdu -> uiMsgLen); sscanf(pdu -> caData, "%s %lld", caFileName, &fileSize); QString strFilePath = QString("%1/%2").arg(caCurPath).arg(caFileName); // 文件路径 qDebug() << "上传文件路径:" << strFilePath; PDU* resPdu = mkPDU(0); resPdu -> uiMsgType = ENUM_MSG_TYPE_UPLOAD_FILE_RESPOND; transFile->file.setFileName(strFilePath); // 设置要上传的文件名 if(transFile->file.open(QIODevice::WriteOnly)) // 以只写的方式打开文件,文件如果不存在会自动创建 { transFile->bTransform = true; // 正在上传文件状态 transFile->iTotalSize = fileSize; transFile->iReceivedSize = 0; memcpy(resPdu -> caData, UPLOAD_FILE_START, 32); } else // 打开文件失败 { memcpy(resPdu -> caData, UPLOAD_FILE_FAILED, 32); } return resPdu; }

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

tcpClient

cpp
展开代码
case ENUM_MSG_TYPE_UPLOAD_FILE_RESPOND: // 上传文件响应 { if(strcmp(UPLOAD_FILE_START, pdu -> caData) == 0) // 开始上传文件数据内容 { OperateWidget::getInstance().getPFileSystem()->startTimer(); } else if ... break; }

fileSystem

cpp
展开代码
// FileSystem构造函数中添加: connect(m_pTimer, SIGNAL(timeout()), // 时间间隔之后再上传文件,防止粘包 this, SLOT(uploadFileData()));
cpp
展开代码
void FileSystem::startTimer() { m_pTimer -> start(1000); // 1000ms } void FileSystem::uploadFileData() { m_pTimer->stop(); // 关闭定时器,不然定时器会重新计时 QFile file(m_strUploadFilePath); if(!file.open(QIODevice::ReadOnly)) // 只读形式打开文件 { // 打开失败 QMessageBox::warning(this, "打开文件", "打开文件失败!"); } // 二进制形式传输文件 char *pBuffer = new char[4096]; // 4096个字节读写效率更高 qint64 iActualSize = 0; // 实际读取文件内容大小 while(true) { iActualSize = file.read(pBuffer, 4096); // 读数据,返回值是实际读取数据大小 if (iActualSize > 0 && iActualSize <= 4096) { TcpClient::getInstance().getTcpSocket().write(pBuffer, iActualSize); } else if (iActualSize == 0) { // 发送完成 break; } else { QMessageBox::warning(this, "上传文件", "上传失败!"); break; } } file.close(); delete [] pBuffer; pBuffer = NULL; m_strUploadFilePath.clear(); // 清楚上传文件夹名,以免影响之后上传操作 }

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

cpp
展开代码
void MyTcpSocket::receiveMsg() { // 所处状态是接收文件 if(m_uploadFile->bTransform) { // 接收数据 QByteArray baBuffer = this -> readAll(); m_uploadFile->file.write(baBuffer); // 文件在上一个请求已经打开了 m_uploadFile->iReceivedSize += baBuffer.size(); PDU* resPdu = NULL; qDebug() << "上传文件中:" << m_uploadFile->iReceivedSize; if(m_uploadFile->iReceivedSize == m_uploadFile->iTotalSize) { m_uploadFile->file.close(); // 关闭文件 m_uploadFile->bTransform = false; resPdu = mkPDU(0); resPdu -> uiMsgType = ENUM_MSG_TYPE_UPLOAD_FILE_RESPOND; strncpy(resPdu -> caData, UPLOAD_FILE_OK, 32); } else if(m_uploadFile -> iReceivedSize > m_uploadFile->iTotalSize) { m_uploadFile->file.close(); // 关闭文件 m_uploadFile->bTransform = false; resPdu = mkPDU(0); resPdu -> uiMsgType = ENUM_MSG_TYPE_UPLOAD_FILE_RESPOND; strncpy(resPdu -> caData, UPLOAD_FILE_FAILED, 32); } // 响应客户端 if(NULL != resPdu) { // qDebug() << resPdu -> uiMsgType << " " << resPdu ->caData; this -> write((char*)resPdu, resPdu -> uiPDULen); // 释放空间 free(resPdu); resPdu = NULL; } return ; } // 所处状态不是接收文件,接收到的是非文件传输的请求 ... // 其他请求的处理代码 }

客户端再进行接收resPdu

tcpClient

cpp
展开代码
case ENUM_MSG_TYPE_UPLOAD_FILE_RESPOND: // 上传文件响应 { if(strcmp(UPLOAD_FILE_START, pdu -> caData) == 0) // 开始上传文件数据内容 { ... } else if(strcmp(UPLOAD_FILE_OK, pdu -> caData) == 0) // 上传文件成功 { QMessageBox::information(this, "上传文件", pdu -> caData); } else if(strcmp(UPLOAD_FILE_FAILED, pdu -> caData) == 0) // 上传失败 { QMessageBox::warning(this, "上传文件", pdu -> caData); } break; }

下载文件

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

fileSystem

cpp
展开代码
void FileSystem::downloadFile() { QListWidgetItem *pItem = m_pFileListW->currentItem(); // 选择要下载的文件 if(NULL == pItem) { QMessageBox::warning(this, "下载文件", "请选择要下载的文件!"); return ; } // 获取保存的位置 QString strDownloadFilePath = QFileDialog::getSaveFileName(); if(strDownloadFilePath.isEmpty()) { QMessageBox::warning(this, "下载文件", "请指定下载文件的位置!"); m_downloadFile->file.setFileName(""); // 清空 return ; } m_downloadFile->file.setFileName(strDownloadFilePath); QString strCurPath = TcpClient::getInstance().getStrCurPath(); // 当前路径 QString strFileName = pItem->text().split('\t')[0]; // 获取文件名 PDU* pdu = mkPDU(strCurPath.size() + 1); pdu -> uiMsgType = ENUM_MSG_TYPE_DOWNLOAD_FILE_REQUEST; memcpy((char*)pdu -> caMsg, strCurPath.toStdString().c_str(), strCurPath.size()); strncpy(pdu -> caData, strFileName.toStdString().c_str(), strFileName.size()); TcpClient::getInstance().getTcpSocket().write((char*)pdu, pdu -> uiPDULen); qDebug() << "下载文件:" << pdu -> caData; free(pdu); pdu = NULL; }
  1. 服务器获得客户端请求,判断其合理性,然后打开所要下载的文件,同时设置计时器(1s后开始传输),然后通知客户端。

myTcpSocket

cpp
展开代码
// 下载文件请求处理 PDU* handleDownloadFileRequest(PDU* pdu, QFile *fDownloadFile, QTimer *pTimer) { char caFileName[32] = {'\0'}; char caCurPath[pdu -> uiMsgLen]; memcpy(caFileName, pdu -> caData, 32); memcpy(caCurPath, (char*)pdu -> caMsg, pdu -> uiMsgLen); QString strDownloadFilePath = QString("%1/%2").arg(caCurPath).arg(caFileName); fDownloadFile->setFileName(strDownloadFilePath); qDebug() << "下载文件:" << strDownloadFilePath; qint64 fileSize = fDownloadFile -> size(); PDU *resPdu = NULL; if(fDownloadFile->open(QIODevice::ReadOnly)) { resPdu = mkPDU(32 + sizeof (qint64) + 5); resPdu -> uiMsgType = ENUM_MSG_TYPE_DOWNLOAD_FILE_RESPOND; strncpy(resPdu -> caData, DOWNLOAD_FILE_START, 32); sprintf((char*)resPdu -> caMsg, "%s %lld", caFileName, fileSize); pTimer -> start(1000); // 开始计时器1000ms qDebug() << (char*)resPdu -> caMsg; } else // 打开文件失败 { resPdu = mkPDU(0); resPdu -> uiMsgType = ENUM_MSG_TYPE_DOWNLOAD_FILE_RESPOND; strncpy(resPdu -> caData, DOWNLOAD_FILE_FAILED, 32); } return resPdu; }
  1. 客户端收到服务器响应,设置自己状态为下载文件bDownloadFile,同时需要实现接收文件逻辑

tcpClient

cpp
展开代码
void TcpClient::receiveMsg() { // 如果处于接收文件数据的状态 TransFile *transFile = OperateWidget::getInstance().getPFileSystem()->getDownloadFileInfo(); if(transFile->bTransform) { QByteArray baBuffer = m_tcpSocket.readAll(); transFile->file.write(baBuffer); transFile->iReceivedSize += baBuffer.size(); if(transFile->iReceivedSize == transFile->iTotalSize) { QMessageBox::information(this, "下载文件", "下载文件成功!"); transFile->file.close(); transFile->file.setFileName(""); transFile->bTransform = false; transFile->iTotalSize = 0; transFile->iReceivedSize = 0; } else if(transFile->iReceivedSize > transFile->iTotalSize) { QMessageBox::warning(this, "下载文件", "下载文件失败!"); transFile->file.close(); transFile->file.setFileName(""); transFile->bTransform = false; transFile->iTotalSize = 0; transFile->iReceivedSize = 0; } return ; } // 否则,处理其他响应PDU ... // 根据不同消息类型,执行不同操作 switch(pdu -> uiMsgType) { ... case ENUM_MSG_TYPE_DOWNLOAD_FILE_RESPOND: // 下载文件响应 { if(strcmp(DOWNLOAD_FILE_START, pdu -> caData) == 0) // 开始下载文件数据内容 { // TransFile *transFile = OperateWidget::getInstance().getPFileSystem()->getDownloadFileInfo(); qint64 ifileSize = 0; char strFileName[32]; sscanf((char*)pdu -> caMsg, "%s %lld", strFileName, &ifileSize); qDebug() << "下载文件中:" << strFileName << ifileSize; if(strlen(strFileName) > 0 && transFile->file.open(QIODevice::WriteOnly)) { transFile->bTransform = true; transFile->iTotalSize = ifileSize; transFile->iReceivedSize = 0; } else { QMessageBox::warning(this, "下载文件", "下载文件失败!"); } } else if(strcmp(DOWNLOAD_FILE_OK, pdu -> caData) == 0) // 下载文件成功 { QMessageBox::information(this, "下载文件", pdu -> caData); } else if(strcmp(DOWNLOAD_FILE_FAILED, pdu -> caData) == 0) // 下载失败 { QMessageBox::warning(this, "下载文件", pdu -> caData); } break; } default: break; } // 释放空间 free(pdu); pdu = NULL; }
  1. 服务器实现传输文件逻辑槽函数,计时器结束之后触发槽函数

myTcpSocket

cpp
展开代码
void MyTcpSocket::handledownloadFileData() { m_pTimer->stop(); // 停止计时器 // 循环传输数据 char *pBuffer = new char[4096]; qint64 iActualSize = 0; // 实际读取文件大小 while(true) { iActualSize = m_pDownloadFile->read(pBuffer, 4096); if (iActualSize > 0 && iActualSize <= 4096) { this -> write(pBuffer, iActualSize); } else if (iActualSize == 0) { // 发送完成 break; } else { qDebug() << "发送文件数据给客户端出错!"; break; } } m_pDownloadFile -> close(); // 关闭文件 delete [] pBuffer; pBuffer = NULL; m_pDownloadFile->setFileName(""); // 清除上传文件夹名,以免影响之后上传操作 }

移动文件

  1. 客户端需要提供两个按钮,一个是“移动文件”,选中需要移动的文件之后,点击该按钮,可以设置需要移动的文件;另一个是“目标目录”(默认状态不可点击,需要点击过“移动文件”按钮之后才可以点击),在选中需要移动的文件之后,再跳转到想要移动的目标目录,进入目标目录之后再点击该按钮,即可设置移动的目标目录,然后向服务器发送请求。

fileSystem

cpp
展开代码
void FileSystem::moveFile() { QListWidgetItem *pItem = m_pFileListW->currentItem(); if(pItem == NULL) { QMessageBox::warning(this, "移动文件", "请选择需要移动的文件!"); return ; } m_strMoveFileName = pItem -> text().split('\t')[0]; // 设置需要移动的文件名 m_strMoveOldDir = TcpClient::getInstance().getStrCurPath(); // 设置移动文件的原目录 m_pMoveDesDirDB->setEnabled(true); // 设置目标目录可点击 QMessageBox::information(this, "移动文件", "请跳转到需要移动到的目录,\n然后点击“目标目录”按钮。"); } void FileSystem::moveDesDir() { QString strDesDir = TcpClient::getInstance().getStrCurPath(); // 设置移动文件的目标目录 QMessageBox::StandardButton sbMoveAffirm; // 确认弹框返回值 QString strMoveAffirm = QString("您确认将 %1 的 %2 文件\n移动到 %3 目录下吗?") .arg(m_strMoveOldDir).arg(m_strMoveFileName).arg(strDesDir); sbMoveAffirm = QMessageBox::question(this, "移动文件", strMoveAffirm); if(sbMoveAffirm == QMessageBox::No) // 不移动 { m_strMoveOldDir.clear(); m_strMoveFileName.clear(); m_pMoveDesDirDB->setEnabled(false); return ; } qDebug() << "移动文件:" << strMoveAffirm; // 确认移动文件 PDU *pdu = mkPDU(strDesDir.size() + m_strMoveOldDir.size() + 5); pdu -> uiMsgType = ENUM_MSG_TYPE_MOVE_FILE_REQUEST; sprintf((char*)pdu -> caMsg, "%s %s", strDesDir.toStdString().c_str(), m_strMoveOldDir.toStdString().c_str()); sprintf(pdu -> caData, "%s %d %d", m_strMoveFileName.toStdString().c_str(), strDesDir.size(), m_strMoveOldDir.size()); TcpClient::getInstance().getTcpSocket().write((char*)pdu, pdu -> uiPDULen); free(pdu); pdu = NULL; m_strMoveOldDir.clear(); m_strMoveFileName.clear(); m_pMoveDesDirDB->setEnabled(false); }
  1. 服务器接收到请求之后,对文件进行移动,并返回移动结果

myTcpSocket

cpp
展开代码
// 移动文件请求处理 PDU* handleMoveFileRequest(PDU* pdu) { char caMoveFileName[32]; // 要移动文件名 int iOldDirSize = 0; int iDesDirSize = 0; sscanf(pdu -> caData, "%s %d %d", caMoveFileName, &iDesDirSize, &iOldDirSize); char caOldDir[iOldDirSize + 33]; // +33是为了拼接文件名 char caDesDir[iDesDirSize + 33]; sscanf((char*)pdu -> caMsg, "%s %s", caDesDir, caOldDir); qDebug() << "移动文件:" << caMoveFileName << "从" << caOldDir << "到" << caDesDir; QFileInfo fileInfo(caDesDir); PDU* resPdu = mkPDU(0); resPdu -> uiMsgType = ENUM_MSG_TYPE_MOVE_FILE_RESPOND; if(!fileInfo.isDir()) { strncpy(resPdu -> caData, MOVE_FILE_FAILED, 32); return resPdu; } // 拼接文件名 strcat(caOldDir, "/"); strcat(caOldDir, caMoveFileName); strcat(caDesDir, "/"); strcat(caDesDir, caMoveFileName); if (QFile::rename(caOldDir, caDesDir)) // 移动 { strncpy(resPdu -> caData, MOVE_FILE_OK, 32); } else { strncpy(resPdu -> caData, MOVE_FILE_FAILED, 32); } return resPdu; }
  1. 客户端接收响应并提示用户。

分享文件

界面设计

选中需要分享的文件之后,用户需要选择分享的好友对象,所以我们需要设计一个选择好友的页面。

代码实现

cpp
展开代码
sharedFileFriendList::sharedFileFriendList(QWidget *parent) : QWidget(parent) { m_pSelectAllPB = new QPushButton("全选"); // 全选 m_pCancleSelectPB = new QPushButton("清空"); // 取消选择 m_pAffirmPB = new QPushButton("确认"); // 确认键 m_pCanclePB = new QPushButton("取消"); // 取消键 m_pFriendsSA = new QScrollArea; // 展示好友区 m_pFriendsWid = new QWidget; // 所有好友窗口 m_pFriendsVBL = new QVBoxLayout(m_pFriendsWid); // 好友信息垂直布局 m_pFriendsBG = new QButtonGroup(m_pFriendsWid); // bg主要用来管理好友选项,Wid设置为其父类 m_pFriendsBG->setExclusive(false); // 可以多选 QHBoxLayout *pTopHBL = new QHBoxLayout; pTopHBL->addStretch(); // 添加弹簧 pTopHBL->addWidget(m_pSelectAllPB); pTopHBL->addWidget(m_pCancleSelectPB); QHBoxLayout *pDownHBL = new QHBoxLayout; pDownHBL->addWidget(m_pAffirmPB); pDownHBL->addWidget(m_pCanclePB); QVBoxLayout *pMainVBL = new QVBoxLayout; pMainVBL->addLayout(pTopHBL); pMainVBL->addWidget(m_pFriendsSA); // SA中放置Wid,Wid是BG的父类 pMainVBL->addLayout(pDownHBL); setLayout(pMainVBL); connect(m_pSelectAllPB, SIGNAL(clicked(bool)), this, SLOT(selectAll())); connect(m_pCancleSelectPB, SIGNAL(clicked(bool)), this, SLOT(cancleSelect())); connect(m_pAffirmPB, SIGNAL(clicked(bool)), this, SLOT(affirmShare())); connect(m_pCanclePB, SIGNAL(clicked(bool)), this, SLOT(cancleShare())); }

最后实现效果

全选按钮键实现逻辑:

cpp
展开代码
void sharedFileFriendList::selectAll() { QList<QAbstractButton*> friendsButtons = m_pFriendsBG->buttons(); for(QAbstractButton* pItem:friendsButtons) { pItem->setChecked(true); } }

取消选择按钮实现逻辑:

cpp
展开代码
void sharedFileFriendList::cancleSelect() { QList<QAbstractButton*> friendsButtons = m_pFriendsBG->buttons(); for(QAbstractButton* pItem:friendsButtons) { pItem->setChecked(false); } }

需要提供当客户点击fileSystem中的”分享文件“按钮之后更新可选择好友列表的代码:

cpp
展开代码
void sharedFileFriendList::updateFriendList(QListWidget *pFriendList) { if(NULL == pFriendList) { return ; } // 移除之前的好友列表 QList<QAbstractButton*> preFriendList = m_pFriendsBG->buttons(); for(QAbstractButton* pItem:preFriendList) { m_pFriendsVBL->removeWidget(pItem); m_pFriendsBG->removeButton(pItem); delete pItem; pItem = NULL; } // 设置新的好友列表 QCheckBox *pCB = NULL; for(int i = 0; i < pFriendList->count(); i ++) { qDebug() << "好友:" << pFriendList->item(i)->text(); pCB = new QCheckBox(pFriendList->item(i)->text()); m_pFriendsVBL->addWidget(pCB); m_pFriendsBG->addButton(pCB); } m_pFriendsSA->setWidget(m_pFriendsWid); // 每次都需要重新设置SA的Widget! }

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

其中需要包含:

  1. 要分享的文件的文件名、文件路径
  2. 要分享的好友名,好友数量
cpp
展开代码
void sharedFileFriendList::affirmShare() { QString strFileName = OperateWidget::getInstance().getPFileSystem()->getStrSharedFileName(); QString strFilePath = OperateWidget::getInstance().getPFileSystem()->getStrSharedFilePath(); QList<QAbstractButton*> abList = m_pFriendsBG->buttons(); QList<QString> userList; // 要分享的好友列表 for(int i = 0; i < abList.count(); ++ i) { if(abList[i]->isChecked()) { userList.append(abList[i]->text().split('\t')[0]); } } int iUserNum = userList.count(); qDebug() << "分享好友:" << userList << " " << iUserNum; PDU* pdu = mkPDU(strFilePath.size() + userList.count() * 32 + 1); // caMsg中存放文件路径、分享好友名 pdu -> uiMsgType = ENUM_MSG_TYPE_SHARE_FILE_REQUEST; for(int i = 0; i < iUserNum; ++ i) { strncpy((char*)(pdu -> caMsg) + 32 * i, userList[i].toStdString().c_str(), 32); } memcpy((char*)(pdu -> caMsg) + 32 * iUserNum, strFilePath.toStdString().c_str(), strFilePath.size()); sprintf(pdu -> caData, "%s %d", strFileName.toStdString().c_str(), iUserNum); // caData存放文件名、分享好友数 TcpClient::getInstance().getTcpSocket().write((char*)pdu, pdu -> uiPDULen); free(pdu); pdu = NULL; this -> hide(); }

逻辑实现

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

fileSystem

cpp
展开代码
void FileSystem::shareFile() { // 获取要分享文件的信息 QListWidgetItem *pFileItem = m_pFileListW->currentItem(); if(NULL == pFileItem) { QMessageBox::warning(this, "分享文件", "请选择要分享的文件!"); return ; } m_strSharedFileName = pFileItem->text().split('\t')[0]; // 要分享文件名 m_strSharedFilePath = QString("%1/%2").arg(TcpClient::getInstance().getStrCurPath()) .arg(m_strSharedFileName); qDebug() << "分享文件:" << m_strSharedFilePath; // 获得好友列表 QListWidget *friendLW = OperateWidget::getInstance().getPFriend()->getPFriendLW(); // 选择好友窗口展示 m_pSharedFileFLW->updateFriendList(friendLW); if(m_pSharedFileFLW->isHidden()) // 如果窗口隐藏,则显示出来 { m_pSharedFileFLW->show(); } }

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

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

myTcpSocket

cpp
展开代码
// 分享文件请求处理 PDU* handleShareFileRequest(PDU* pdu, QString strSouName) { int iUserNum = 0; // 分享好友数 char caFileName[32]; // 分享的文件名 sscanf(pdu -> caData, "%s %d", caFileName, &iUserNum); qDebug() << "分享文件:" << caFileName << " 人数:" << iUserNum; // 转发给被分享的好友分享文件通知 const int iFilePathLen = pdu->uiMsgLen - iUserNum * 32; char caFilePath[iFilePathLen]; PDU* resPdu = mkPDU(iFilePathLen); resPdu -> uiMsgType = ENUM_MSG_TYPE_SHARE_FILE_NOTE; memcpy(resPdu -> caData, strSouName.toStdString().c_str(), strSouName.size()); // 发送方 memcpy(resPdu -> caData + 32, caFileName, 32); // 发送文件名 memcpy(caFilePath, (char*)(pdu -> caMsg) + 32 * iUserNum, iFilePathLen); memcpy((char*)resPdu -> caMsg, caFilePath, iFilePathLen); // 发送文件路径 // 遍历分享所有要接收文件的好友 char caDesName[32]; // 目标好友名 for(int i = 0; i < iUserNum; ++ i) { memcpy(caDesName, (char*)(pdu -> caMsg) + 32 * i, 32); MyTcpServer::getInstance().forwardMsg(caDesName, resPdu); qDebug() << caDesName; } free(resPdu); resPdu = NULL; // 回复发送方消息 resPdu = mkPDU(0); resPdu -> uiMsgType = ENUM_MSG_TYPE_SHARE_FILE_RESPOND; strncpy(resPdu -> caData, SHARE_FILE_OK, 32); return resPdu; }
  1. 发送分享文件的源客户会收到服务器的响应。

tcpClient

cpp
展开代码
case ENUM_MSG_TYPE_SHARE_FILE_RESPOND: // 分享文件响应 { QMessageBox::information(this, "分享文件", pdu -> caData); break; }
  1. 被分享文件的所有目的客户都会收到服务器发送的NOTE的PDU,询问是否接收文件,如果选择接收文件,则会向服务器发送确认报文。

tcpClient

cpp
展开代码
case ENUM_MSG_TYPE_SHARE_FILE_NOTE: // 被分享文件提醒 { char caFileName[32]; // 文件名 char caSouName[32]; // 用户名 int iFilePathLen = pdu -> uiMsgLen; char caFilePath[iFilePathLen]; // 文件路径 memcpy(caSouName, pdu -> caData, 32); memcpy(caFileName, pdu -> caData + 32, 32); QString strShareNote = QString("%1 想要分享 %2 文件给您,\n是否接收?").arg(caSouName).arg(caFileName); QMessageBox::StandardButton sbShareNote = QMessageBox::question(this, "分享文件", strShareNote); if(sbShareNote == QMessageBox::No) { // 拒绝接收 break; } // 同意接收 qDebug() << "接收文件:" << caSouName <<" " << caFileName; memcpy(caFilePath, (char*)pdu -> caMsg, iFilePathLen); QString strRootDir = m_strRootPath; // 用户根目录 PDU *resPdu = mkPDU(iFilePathLen + strRootDir.size() + 1); resPdu -> uiMsgType = ENUM_MSG_TYPE_SHARE_FILE_NOTE_RESPOND; sprintf(resPdu -> caData, "%d %d", iFilePathLen, strRootDir.size()); sprintf((char*)resPdu -> caMsg, "%s %s", caFilePath, strRootDir.toStdString().c_str()); qDebug() << (char*)resPdu -> caMsg; m_tcpSocket.write((char*)resPdu, resPdu -> uiPDULen); free(resPdu); resPdu = NULL; break; }
  1. 服务器接收到被分享方的响应之后,开始实际拷贝文件,对文件与文件夹有不同拷贝策略。并返回被分享方拷贝结果。

myTcpSocket

cpp
展开代码
// 工具函数:复制文件夹 bool copyDir(QString strOldPath, QString strNewPath) { int ret = true; QDir dir; // 目录操作 qDebug() << "分享目录:" << strOldPath << " " << strNewPath; dir.mkdir(strNewPath); // 新路径创建空目录 dir.setPath(strOldPath); // 设置为源目录 QFileInfoList fileInfoList = dir.entryInfoList(); // 获得源目录下文件列表 // 对源目录下所有文件(分为普通文件、文件夹)进行递归拷贝 QString strOldFile; QString strNewFile; for(QFileInfo fileInfo:fileInfoList) { if(fileInfo.fileName() == "." || fileInfo.fileName() == "..") { // 注意不要忘记这个判断,"."和".."文件夹不用复制,不然会死循环 continue; } strOldFile = QString("%1/%2").arg(strOldPath).arg(fileInfo.fileName()); strNewFile = QString("%1/%2").arg(strNewPath).arg(fileInfo.fileName()); if(fileInfo.isFile()) { ret = ret && QFile::copy(strOldFile, strNewFile); } else if(fileInfo.isDir()) { ret = ret && copyDir(strOldFile, strNewFile); } qDebug() << strOldFile << " -> " << strNewFile; } return ret; } // 分享文件通知响应处理 PDU* handleShareFileNoteRespond(PDU *pdu) { int iOldPathLen = 0; int iNewPathLen = 0; sscanf(pdu -> caData, "%d %d", &iOldPathLen, &iNewPathLen); char caOldPath[iOldPathLen]; char caNewDir[iNewPathLen]; sscanf((char*)pdu -> caMsg, "%s %s", caOldPath, caNewDir); // 获得文件新的路径 char *pIndex = strrchr(caOldPath, '/'); // 获得最右侧的/的指针,找到文件名 QString strNewPath = QString("%1/%2").arg(caNewDir).arg(pIndex + 1); qDebug() << "同意分享文件:" << caOldPath << " " << strNewPath; QFileInfo fileInfo(caOldPath); bool ret = false; if(fileInfo.isFile()) { ret = QFile::copy(caOldPath, strNewPath); } else if(fileInfo.isDir()) { ret = copyDir(caOldPath, strNewPath); } else { ret = false; } // 回复接收方 PDU* resPdu = mkPDU(0); resPdu -> uiMsgType = ENUM_MSG_TYPE_SHARE_FILE_NOTE_RESPOND; if(ret) { memcpy(resPdu -> caData, SHARE_FILE_OK, 32); } else { memcpy(resPdu -> caData, SHARE_FILE_FAILED, 32); } return resPdu; }
  1. 被分享方接收到服务器的响应之后显示结果
cpp
展开代码
case ENUM_MSG_TYPE_SHARE_FILE_NOTE_RESPOND: // 被分享文件通知响应的处理结果 { QMessageBox::information(this, "分享文件", pdu -> caData); break; }

面试准备

项目知识点

  • C/S架构特点,与B/S区别
  • 设计模式
    • 单例设计模式
  • socket和slots
  • Qt常见面试问题

项目不足之处

  • 用户表和好友表之间的外键是用户id,所以在客户端或是服务器的MyTcpSocket存储用户id才更为方便之后的操作,而不是存储用户名name。

因为如果存储的是name的话,涉及到已知用户id访问好友表时,需要先根据用户name查询到用户id,然后再进行操作,降低了效率。 解决方案:也可以直接将name和id都进行存储。

本文作者:冬月

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!