写在前面
这篇博客主要工作是解释和思考,不记录我做项目的过程,因为这篇博客是我要理解其他人的代码,其中涉及到tcp的服务器客户端交互、MySQL、多线程
这部分涉及到计算机网络,是笔者没学的部分,所以对我来说理解它们是一个很困难的事情,不过有AI解释加学长线上线下的答疑,我可以勉强理解运行,然后争取自己再写一个板块的传输
本篇技术含量相比往篇很高很高,但是由于笔者没学过这部分,所以解释起来可能不是很好
前期准备
我们的项目交接过程是:你做一部分,然后把文件压缩包发过来,我再继承你的做,我做完一部分再把文件压缩包发给你,你再继承我的做,所以三个人每个人的电脑上都需要配置好环境。因为之前的部分不涉及服务器和客户端的交互,也不涉及数据库存储,所以只要在Tick_Task这个项目里写代码就可以了,运行也不需要什么准备。
但是昨天笔者打开学长发的压缩包发现项目无法正常运行了。为什么?因为这个项目已经连接到服务器了,所以需要做的是:先打开服务器,让服务器连接到数据库------>点击服务器的监视按钮,开始监视接入端口请求------>打开客户端,让客户端与服务器连接上
需要有这样的运行顺序,程序才可以正常跑,单独运行客户端项目是跑不动的,在登录界面就卡死了
还有一个需要注意的点:我们三个人的数据库是独立的。这说明什么问题?我需要在自己的电脑部署一个和服务器要连接的数据库相同属性的数据库,并且需要在数据库中自己开存数据的表
sql
-- 建立数据库
CREATE DATABASE tick_task;
USE tick_task;
-- 建立账号测试用表
CREATE TABLE account (
Account VARCHAR(30) NOT NULL PRIMARY KEY,
Password VARCHAR(200) NOT NULL
);
-- 建立task数据表
create table task
(
Account VARCHAR(30) NOT NULL,
Tname VARCHAR(50) NOT NULL,
StartTime TIMESTAMP,
EndTime TIMESTAMP,
ReminderTime TIMESTAMP,
Tkind VARCHAR(30),
Tpriority VARCHAR(30),
Tdesc VARCHAR(300),
Flag BOOLEAN DEFAULT 0,
FOREIGN KEY(Account) REFERENCES Account(Account),
CONSTRAINT unique_account_tname UNIQUE (Account, Tname)
);
不过值得庆幸的是学长已经提供了代码,我只需要在MySQL里运行这段就可以了
解释一下这段代码:先创建一个叫"tick_task"的数据库,然后跳到这个数据库之后在这里建立账号密码表,账号为主键,再建立一个存待办的表,这个表的数据有:用户、任务名、开始时间、结束时间、提醒时间、任务类别、任务优先级、任务描述,这个表要求账户必须在刚才的账号密码表里存在且任务名不能相同
在我自己的电脑开好数据库之后,就可以给自己注册一个账号了
关键理解
实话实说,笔者起初面对学长的代码是无从下手的,就算是理解也不知道从哪里开始,后来笔者使用了由总体到局部、由浅入深的顺序来理解:先把每个类的作用弄清楚,然后分别理解每个类的函数的作用,然后理解每个函数的实现方法,再回归整体,把类与类的联系弄清楚
不过那是笔者的理解顺序,在博客中我自然是只会展示我的理解,而背后的事情是不会具体说的,所以在这里先讨论两个问题:数据库与服务器如何交互数据?服务器与客户端如何交互数据?这两个问题搞清楚,再思考细分功能:登录注册功能是怎么实现的?待办的数据是怎么存储的?按照这个顺序就可以讲清楚了,不过现在我们先要对这个我从来没展示过的"Server"工程文件有一个宏观的理解
(1)"Server"工程的头文件对应类的作用

ClientWorker类:与客户端进行数据传输,包括数据的装包拆包解析、接收数据并调用数据库实现对应功能、数据回传
DbConnectionPool类:开一个存数据库的池子,起到了节省开销的作用,如果要调用数据库,就从池子中取一个出来,用完再放回去
FileDbWorker类:负责文件传输,这部分还没实现
LoginSever类:从数据库中查找账号密码,判断是否重复或正确
ServerDlg类:服务器的界面类,输出用户操作
TaskDbWorker类:向数据库中新增、修改、删除数据
TcpServer类:捕获客户端端口号和地址,并奠定多线程的基础
TranSpack类:只有头文件,存的是令牌,及各种命令
这个工程从头到尾都是学长写的,质量确实很高,鲁棒性、准确性、性能优化、技术门槛都比我强得多
(2)服务器如何与客户端进行数据交互
想要交互,首先要做的肯定是连接,毕竟要连接起来才可以进行数据交互
这里涉及到三个类:客户端工程的TcpClient类和服务器工程的TcpServer类及ClientWorker类,他们三个各自提供了连接的一部分方法。
具体连接原理在我之前的博客《Qt基础知识记录(第八篇)》,这里只讲关键函数
客户端方面:
cpp
//用于客户端连接服务器的函数
void TcpClient::connectToServer(const QString &ip, int port)
{
qDebug() << "Connecting to server at" << ip << ":" << port;
m_socket->connectToHost(ip, port);
}
利用Qt的库函数把端口和地址传给套接字,并在主窗口类的构造函数里调用这个函数:
cpp
Client->connectToServer("127.0.0.1", 8256);
ip地址"127.0.0.1"是本机的ip地址,这是死东西,"8256"是初始化服务器自定义的端口号,没什么特殊意义,就是看它顺眼
服务器方面:
cpp
//监听客户端端口函数
void TCPServer::IocpListen()
{
// 监听端口
if (!listen(QHostAddress::Any, m_ListenPort))
{
qDebug() << "Server could not start!"<< ip_addr << ":" << m_ListenPort;
}
else
{
qDebug() << "Server started!"<< ip_addr << ":" << m_ListenPort;;
}
这个函数的作用是监听来自所有可能地址的客户端,只要连接的是这个服务器的端口都会被监听到
被监听到有什么用呢?又没有实现通信功能。所以就有了下边的函数,这个函数被归到了ClientWorker类里
cpp
//多线程函数,支持多个客户端并发运行
void ClientWorker::start()
{
socket = new QTcpSocket;
socket->moveToThread(QThread::currentThread());
if (!socket->setSocketDescriptor(socketDescriptor)) {
qDebug() << "Failed to set socket descriptor!";
emit finished();
return;
}
QString peerAddress = socket->peerAddress().toString();
quint16 peerPort = socket->peerPort();
qDebug() << "New client connected:" << peerAddress << ":" << peerPort;
emit ConnectedtoClient(peerAddress, peerPort);
qDebug() << "ClientWorker::start thread ID: " << QThread::currentThreadId();
connect(socket, &QTcpSocket::readyRead, this, &ClientWorker::readClientData);
connect(socket, &QTcpSocket::disconnected, this, [this]() {
qDebug() << "Client disconnected";
emit DisconnectedtoClient(socket->peerAddress().toString(), socket->peerPort());
socket->deleteLater();
emit finished();
});
}
来讲一下这个函数的实现内容:这个函数是可以多线程的,也就是说可以有多个客户端连接到同一个服务器中。首先需要把套接字移到当前线程,这个套接字连接的是刚才监听到的客户端。此外的代码都是一些辅助功能,像安全性、断开线程、拓展下一个线程等,和本篇博客讲的重点不符,这里就不细说了
通过这几个函数,服务器和客户端就完美地建立起联系了
然后就是交互数据了,这部分宏观可以这样看:
服务器和客户端的数据交互其实是通过一个叫做"数据包"的东西实现的,客户端想传数据,就把数据打包传给服务器,服务器再拆包,然后解析这部分数据,知道它属于哪里,再调用对应函数去处理,然后回传的时候也打包一个数据包,传回客户端,客户端再拆开看反馈
具体实现是怎样的呢?需要用下面几个函数:
cpp
//用于将客户端传来的数据包拆包并分类的函数
void ClientWorker::readClientData()
{
if (!socket) return;
buffer.append(socket->readAll());
qDebug() << "readClientData() 收到字节数:" << buffer.size();
while (true) {
if (receivingFile) {
qint64 remaining = currentFileSize - receivedBytes;
if (buffer.size() < remaining) {
currentFile.write(buffer);
receivedBytes += buffer.size();
buffer.clear();
return;
} else {
currentFile.write(buffer.constData(), remaining);
receivedBytes += remaining;
buffer.remove(0, remaining);
currentFile.close();
receivingFile = false;
qDebug() << "文件接收完成:" << currentFileName;
// 校验CRC32
QFile verifyFile(currentFile.fileName());
if (!verifyFile.open(QIODevice::ReadOnly)) {
qDebug() << "无法打开文件进行校验:" << currentFile.fileName();
return;
}
quint32 checkCRC = crc32(0L, Z_NULL, 0);
while (!verifyFile.atEnd()) {
QByteArray block = verifyFile.read(4096);
checkCRC = crc32(checkCRC, reinterpret_cast<const Bytef*>(block.constData()), block.size());
}
verifyFile.close();
CheckFileCRC(checkCRC);
// 继续循环
}
} else {
if (buffer.size() < 9) return;
int consumed = JudgeClientInformation(buffer);
if (consumed <= 0) {
// 数据包不完整,等待更多数据
return;
}
buffer.remove(0, consumed);
// 继续循环处理下一包
}
}
}
此为拆包并分类函数,具体实现逻辑不细讲,这部分一方面是笔者还没学,一方面是做打卡板块的数据传输不需要改动
cpp
//将分好类的数据包拆开解析内容的函数
int ClientWorker::JudgeClientInformation(QByteArray data)
{
Qt::HANDLE currentThreadId = QThread::currentThreadId();
qDebug() << "current Thread ID: in JudgeClientInformation" << currentThreadId;
if (data.size() < 9) return 0;
QDataStream stream(data);
stream.setByteOrder(QDataStream::BigEndian);
quint8 istoken;
quint32 packLength;
quint32 checkNum;
stream >> istoken;
stream >> packLength;
stream >> checkNum;
if (data.size() < (int)packLength) {
// 不完整包,等待更多数据
return 0;
}
QByteArray packData = data.mid(9, packLength - 9); // 提取数据部分
switch (istoken)
{
case CLIENT_LOGIN:
DoServerLogin(packData);
break;
case CLIENT_REGISTER:
DoRegister(packData);
break;
case CLIENT_REGISTER_ACCREPEAT:
DoRegisterAccRepeat(packData);
break;
case CLIENT_TASK_UPLOAD:
DoTaskUpload(packData);
break;
case CLIENT_TASK_READ:
DoTaskRead(packData);
break;
case CLIENT_TASK_MODIFY:
DoTaskModify(packData);
break;
case CLIENT_TASK_DELETE:
DoTaskDelete(packData);
break;
case CLIENT_FILE_UPLOAD:
filecheckNum = checkNum;
handleFileMetaData(packData); //处理上传文件元信息
break;
default:
qDebug() << "Unknown istoken:" << istoken;
break;
}
return packLength; // 告知调用者处理了多少字节
}
此为拆包之后的解析函数,把令牌、内存和数据分别拆出来进行对应操作
cpp
//将数据打包成数据包的函数
void ClientWorker::PreSendData(QByteArray &sendData, const QByteArray &data, unsigned char Istoken)
{
// 构造TransPack
unsigned char istoken = Istoken; // 登录成功
quint32 packLength = sizeof(istoken) + sizeof(quint32) + sizeof(quint32) + data.size();
quint32 checkNum = 0; // 根据需求实现校验逻辑
// 逐字段序列化到QByteArray
QDataStream stream(&sendData, QIODevice::WriteOnly);
stream.setByteOrder(QDataStream::BigEndian); // 网络字节序
// 写入IsToken
stream << istoken;
// 写入packLength
stream << packLength;
// 写入checkNum
stream << checkNum;
// 写入Data
stream.writeRawData(data.constData(), data.size());
}
此为打包函数,将数据的内存、令牌、内容做成数据包,便于后续传回客户端
上述几个是服务器方面的函数,接下来是客户端方面的函数:
cpp
//实现封装数据的函数
void TcpClient::PreSendData(QByteArray &sendData, const QByteArray &data, unsigned char Istoken)
{
// 构造TransPack
unsigned char istoken = Istoken;
quint32 packLength = sizeof(istoken) + sizeof(quint32) + sizeof(quint32) + data.size();
quint32 checkNum = 0; // 根据需求实现校验逻辑
// 逐字段序列化到QByteArray
QDataStream stream(&sendData, QIODevice::WriteOnly);
stream.setByteOrder(QDataStream::BigEndian); // 网络字节序
// 写入IsToken
stream << istoken;
// 写入packLength
stream << packLength;
// 写入checkNum
stream << checkNum;
// 写入Data
stream.writeRawData(data.constData(), data.size());
}
这个函数也是为了把数据打包,然后便于后续传给服务器
cpp
//发送数据函数,向服务器写入数据
void TcpClient::sendResponse(const QByteArray &data)
{
m_socket->write(data);
}
这个函数是向服务器写入数据的函数
cpp
//客户端从服务器中提取数据的函数
void TcpClient::JudgeClientInformation()
{
while (m_buffer.size() >= sizeof(unsigned char) + sizeof(quint32) + sizeof(quint32))
{
QDataStream stream(m_buffer);
stream.setByteOrder(QDataStream::BigEndian);
unsigned char istoken;
quint32 packLength;
quint32 checkNum;
stream >> istoken; // 读取IsToken
stream >> packLength; // 读取PackLength
stream >> checkNum; // 读取CheckNum
if (m_buffer.size() < packLength) {
break; // 数据不完整,等待更多数据
}
QByteArray packData;
packData.resize(packLength - sizeof(istoken) - sizeof(packLength) - sizeof(checkNum));
stream.readRawData(packData.data(), packData.size()); // 读取Data部分
// 处理数据包
handlePacket(istoken, packData);
// 移除已处理的数据
m_buffer = m_buffer.mid(packLength);
}
}
这个函数是收到服务器的数据包之后解析数据的函数
通过这些:封装->传->拆包->解析,就可以实现客户端和服务器的数据传输
(3)数据库与服务器如何进行数据交互
这一部分只在"Server"工程里,这是真正的后端
这里宏观层次讲一下调动思路:开一个存数据库位置的池子,**这个池子里装的数据库已经是连接好的了,即我们预先准备的"tick_task"数据库。**有需要的时候从池子取一个出来,然后用完再放回去
这一功能主要是通过这几个函数实现的:
cpp
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
//连接到数据库
DBConfig config;
config.host = "localhost";
config.port = 3306;
config.dbName = "tick_task";
config.user = "root";
config.password = "**********";
DbConnectionPool::instance().setConfig(config);
// 获取当前线程的 ID
Qt::HANDLE currentThreadId = QThread::currentThreadId();
// 打印当前线程的 ID
qDebug() << "main Thread ID:" << currentThreadId;
ServerDlg w;
w.show();
return a.exec();
}
主函数先固定数据库属性,此后的数据库池子存的数据库都是连的这个数据库
cpp
//初始化数据库,并把它们放到数据库连接池中
void DbConnectionPool::setConfig(const DBConfig& config) {
QMutexLocker locker(&mutex);
if (!configSet) {
dbConfig = config;
configSet = true;
initConnections(10); // 初始化连接数:10 个
}
}
注意那里的赋值操作,这一步就把数据库池里的数据库全部初始化,由于这一部分又涉及到了多线程,我也不是很懂,所以只讲基本的数据库连接
cpp
//预创建并初始化指定数量的数据库连接,并把它们放入连接池的可用队列中的函数
void DbConnectionPool::initConnections(int count) {
for (int i = 0; i < count; ++i) {
QString connName = createConnectionName();
QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL", connName);
db.setHostName(dbConfig.host);
db.setPort(dbConfig.port);
db.setDatabaseName(dbConfig.dbName);
db.setUserName(dbConfig.user);
db.setPassword(dbConfig.password);
if (db.open()) {
availableConnections.enqueue(connName);
allConnections.insert(connName, db);
} else {
qDebug() << "Initial connection failed:" << db.lastError().text();
}
}
}
在这个函数里才真正物理意义上把数据库放到了池子里,可以在后续使用
cpp
//从数据库连接池中获取可用数据库连接的函数
QSqlDatabase DbConnectionPool::getConnection() {
QMutexLocker locker(&mutex);
qDebug() << "[DbConnectionPool] getConnection called from thread:" << QThread::currentThreadId();
qDebug() << "[DbConnectionPool] Available connections:" << availableConnections.size();
while (availableConnections.isEmpty()) {
if (allConnections.size() < maxConnections) {
QString connName = createConnectionName();
QSqlDatabase db = QSqlDatabase::addDatabase("QMYSQL", connName);
db.setHostName(dbConfig.host);
db.setPort(dbConfig.port);
db.setDatabaseName(dbConfig.dbName);
db.setUserName(dbConfig.user);
db.setPassword(dbConfig.password);
if (db.open()) {
qDebug() << "[DbConnectionPool] Dynamic connection created:" << connName;
allConnections.insert(connName, db);
return db;
} else {
qDebug() << "Dynamic connection failed:" << db.lastError().text();
return QSqlDatabase(); // 返回无效连接
}
} else {
waitCondition.wait(&mutex);
}
}
QString connName = availableConnections.dequeue();
return allConnections.value(connName);
}
//将使用完的数据库放回数据库连接池的函数
void DbConnectionPool::releaseConnection(const QString& connectionName) {
QMutexLocker locker(&mutex);
if (allConnections.contains(connectionName)) {
availableConnections.enqueue(connectionName);
waitCondition.wakeOne();
}
}
这两个函数是从数据库池子中取一个数据库来用,和用完再把数据库放回池子里
这部分核心就是数据库池,一方面可以实现多线程,一方面节省开销提升运行效率,这才真正起到了中介作用
接下来讲细分功能:
(4)登录注册功能是怎么实现的
这一部分核心还是:客户端发送请求->服务器接收请求并按令牌类型处理->返回处理结果->客户端接收后做出反应
先讲登录功能:提取用户输入的账号密码,带着登录请求打包发给服务器,服务器从数据库中查找账号密码是否正确反馈给客户端,客户端收到做出反应
具体函数:
cpp
//登录的网络功能函数,用于向服务器发送登录请求,将用户的账号密码发给服务器
void TcpClient::sendLoginRequest(const QString &username, const QString &password)
{
//qDebug() <<"accpass connected";
QJsonObject json;
json["username"] = username;
json["password"] = password;
QJsonDocument doc(json);
QByteArray data = doc.toJson();
// 构造TransPack
unsigned char istoken = CLIENT_LOGIN;
quint32 packLength = sizeof(istoken) + sizeof(quint32) + sizeof(quint32) + data.size();
quint32 checkNum = 0; // 根据需求实现校验逻辑
// 逐字段序列化到QByteArray
QByteArray sendData;
QDataStream stream(&sendData, QIODevice::WriteOnly);
stream.setByteOrder(QDataStream::BigEndian); // 网络字节序
stream << istoken; // 写入IsToken
stream << packLength; // 写入packLength
stream << checkNum; // 写入checkNum
stream.writeRawData(data.constData(), data.size()); // 写入packData
m_socket->write(sendData);
}
此为客户端发送登录请求的函数
cpp
//函数作用是确认账号密码是否正确,实现逻辑是调用数据库直接查找
bool LoginServer::ConfirmAccount(QString username, QString password) {
qDebug() << "[LoginServer] Requesting DB connection, thread:" << QThread::currentThreadId();
QSqlDatabase db = DbConnectionPool::instance().getConnection();
QString connName = db.connectionName();
qDebug()<<"connecttionname:"<<connName;
QSqlQuery query(db);
query.prepare("SELECT * FROM account WHERE Account = :username AND Password = :password");
query.bindValue(":username", username);
query.bindValue(":password", password);
query.exec();
bool result = query.next();
DbConnectionPool::instance().releaseConnection(connName);
return result;
此为具体实现服务器从数据库中查找账号密码的函数
cpp
//将客户端传来的账号密码的json格式提取出来并进行判断,之后实现登录的函数
void ClientWorker::DoServerLogin(QByteArray packdata)
{
QJsonDocument jsonDoc = QJsonDocument::fromJson(packdata);
if (jsonDoc.isNull())
{
qDebug() << "Invalid JSON data received";
return;
}
QJsonObject jsonObj = jsonDoc.object();
QString username = jsonObj.value("username").toString();
QString password = jsonObj.value("password").toString();
qDebug()<<username<<" "<<password;
Qt::HANDLE currentThreadId = QThread::currentThreadId();
qDebug() << "slot Thread ID:" << currentThreadId;
LoginServer *lserver = new LoginServer;
// 验证账号密码
if(lserver->ConfirmAccount(username,password))
{
//检测通过
QByteArray data = username.toUtf8();
// 构造TransPack
unsigned char istoken = CLIENT_LOGIN_SUCCESSFUL; //登陆成功
QByteArray sendData;
PreSendData(sendData,data,istoken);
sendResponse(sendData);
//
emit LogintoAccount(username);
}
else
{
QByteArray data = "login failed";
// 构造TransPack
unsigned char istoken = CLIENT_LOGIN_FAILED; //登陆失败
// 逐字段序列化到QByteArray
QByteArray sendData;
PreSendData(sendData,data,istoken);
sendResponse(sendData);
emit LoginFailed();
}
lserver->deleteLater();
}
这是宏观实现接收并反馈账号密码的函数,它主要实现接收信息,调用具体方法,返回信息
有这几个函数再加上辅助的信号,就可以实现登录功能,注册功能同理,为了控制篇幅就不细说了
注意:这只是一个大概的功能,其中还涉及到很多信号互传,穿针引线式一段一段完成功能
(5)待办的数据是怎么存储的
这一部分和我们最初的规划不同,最初规划是析构函数里把所有东西一并放到数据库,不过由于要做多线程等原因,后来又改思路了。改成了实时请求,只要待办板块有新建、修改、删除操作,数据库方面就会立刻更新
在客户端工程里专用的客户端工作类里有这样一个函数:
cpp
//负责连接待办界面,负责调用待办的网络功能
void TcpClient::GetTaskClient(Task *Tparameter)
{
m_TaskClient = Tparameter;
if(m_TaskClient)
{
connect(m_TaskClient, &Task::UploadTask, this, &TcpClient::DoTaskUpload);
connect(m_TaskClient, &Task::ReadTask, this, &TcpClient::SendTaskReadRequest);
connect(m_TaskClient, &Task::ModifyTask, this, &TcpClient::DoTaskModify);
connect(m_TaskClient, &Task::DeleteTask, this, &TcpClient::DoTaskDelete);
}
}
这个函数主要作用是统筹,把待办内的请求发给服务器,然后调用服务器方面的函数
以下是服务器方面的函数:
cpp
//将客户端待办传来的消息传到数据库中的函数
void ClientWorker::DoTaskUpload(QByteArray packdata)
{
TaskDBWorker *tworker = new TaskDBWorker;
//反序列化packdata中的内容到vector中
QDataStream stream(&packdata,QIODevice::ReadOnly);
QVector<TaskClass> recTasks;
QString account;
int taskcount;
stream>>account>>taskcount;
{
qDebug() << "account:" << account << ", taskcount:" << taskcount;
}
for(int i = 0; i < taskcount;i++)
{
TaskClass task;
stream >> task.TaskName;
stream >> task.StartTime;
stream >> task.EndTime;
stream >> task.ReminderTime;
stream >> task.TKind;
stream >> task.TPriority;
recTasks.append(task);
qDebug()<<task.TaskName <<" "<<task.StartTime.toString("yyyy/MM/dd HH:mm")<<" " <<task.EndTime.toString("yyyy/MM/dd HH:mm");
}
QVector<int> res = tworker->SaveTaskData(account,recTasks,1);
if(res.empty()) //所有数据均上传成功
{
QByteArray data = "All tasks upload successfully.";
unsigned char istoken = CLIENT_TASK_UPLOAD_SUCCESSFUL;
QByteArray sendData;
PreSendData(sendData,data,istoken);
sendResponse(sendData);
emit TaskUploadSuccess(account,taskcount);
}
else //有至少一条数据上传失败
{
QByteArray data;
QDataStream stream(&data,QIODevice::WriteOnly);
stream << res;
QByteArray sendData;
unsigned char istoken = CLIENT_TASK_UPLOAD_FAILED;
PreSendData(sendData,data,istoken);
sendResponse(sendData);
emit TaskUploadFailed(account,taskcount,data);
}
}
这个函数主要用于新增一个待办
cpp
//将客户端待办板块修改后的数据传到数据库的函数,理论上可支持多条数据的修改
void ClientWorker::DoTaskModify(QByteArray packdata)
{
TaskDBWorker *tworker = new TaskDBWorker;
//反序列化packdata中的内容到vector中
QDataStream stream(&packdata,QIODevice::ReadOnly);
QVector<TaskClass> recTasks;
QString account;
int taskcount;
stream>>account>>taskcount;
for(int i = 0; i < taskcount;i++)
{
TaskClass task;
stream >> task.TaskName;
stream >> task.StartTime;
stream >> task.EndTime;
stream >> task.ReminderTime;
stream >> task.TKind;
stream >> task.TPriority;
recTasks.append(task);
}
QVector<int> res = tworker->SaveTaskData(account,recTasks,2);
if(res.empty()) //所有数据均修改成功
{
QByteArray data = "All tasks modify successfully.";
unsigned char istoken = CLIENT_TASK_MODIFY_SUCCESSFUL;
QByteArray sendData;
PreSendData(sendData,data,istoken);
sendResponse(sendData);
emit TaskModifySuccess(account,taskcount);
}
else //有至少一条数据修改失败
{
QByteArray data;
QDataStream stream(&data,QIODevice::WriteOnly);
stream << res;
QByteArray sendData;
unsigned char istoken = CLIENT_TASK_UPLOAD_FAILED;
PreSendData(sendData,data,istoken);
sendResponse(sendData);
emit TaskUploadFailed(account,taskcount,data);
}
}
这个函数主要用于修改一个待办
这两个函数里又调用了数据库函数,数据库函数在TaskWorker类里,
cpp
//保存/更新待办板块内容的函数
QVector<int> TaskDBWorker::SaveTaskData(QString account, QVector<TaskClass> Tasks, int method)
{
qDebug() << "[TaskDBWorker] Requesting DB connection, thread:" << QThread::currentThreadId();
QSqlDatabase db = DbConnectionPool::instance().getConnection();
QString connName = db.connectionName();
qDebug()<<"connecttionname"<<connName;
QSqlQuery query(db);
QVector<int> res;
if(method==1)
{
for(int i = 0 ; i<Tasks.size();i++)
{
TaskClass task = Tasks[i];
query.prepare(R"(INSERT INTO task
(Account, Tname, StartTime, EndTime, ReminderTime, Tkind, Tpriority, Tdesc)
VALUES
(:account, :tname, :startTime, :endTime, :reminderTime, :tkind, :tpriority, :tdesc))");
query.bindValue(":account", account);
query.bindValue(":tname", task.TaskName);
query.bindValue(":startTime", task.StartTime);
query.bindValue(":endTime", task.EndTime);
query.bindValue(":reminderTime", task.ReminderTime);
query.bindValue(":tkind", task.TKind);
query.bindValue(":tpriority", task.TPriority);
if(query.exec()) continue;
else res.push_back(i+1);
}
}
else if(method==2)
{
for(int i = 0 ; i<Tasks.size();i++)
{
TaskClass task = Tasks[i];
query.prepare(R"(
UPDATE task
SET
StartTime = :startTime,
EndTime = :endTime,
ReminderTime = :reminderTime,
Tkind = :tkind,
Tpriority = :tpriority,
Tdesc = :tdesc,
Flag = :flag
WHERE Account = :account AND Tname = :tname
)");
query.bindValue(":account", account);
query.bindValue(":tname", task.TaskName);
query.bindValue(":startTime", task.StartTime);
query.bindValue(":endTime", task.EndTime);
query.bindValue(":reminderTime", task.ReminderTime);
query.bindValue(":tkind", task.TKind);
query.bindValue(":tpriority", task.TPriority);
if(query.exec()) continue;
else res.push_back(i+1);
}
}
DbConnectionPool::instance().releaseConnection(connName);
return res;
}
这样做就可以实现数据库的实时更新
(6)信号的传递
这一部分可以说相当重要,刚才所说的都是功能的实现,而这些功能的联系是一个繁琐且细致的事情,笔者本来不想写这一部分的,因为实在是太麻烦太繁琐了,不过考虑到只罗列功能函数,一方面读者看不懂,一方面我如果要做打卡板块也避不开这个,所以就写下来了
首先有函数调用方式,学长的编程风格比我档次高多了,我的代码都是把所有东西放在一个函数里,学长是细分功能,哪怕一个功能只有一两行代码也要专门开一个函数,所以函数调用就很频繁,函数调用中调用其他类的函数技术含量更高,这也是我很少用到的方式,学长的类是按功能分的,专类专用,所以在服务器运行过程中需要调用对应类的函数
其次是信号与槽机制,这个机制太灵活了,而且在代码非常长的时候,想找到一个信号发出之后在哪里接收是一个相当困难的事情,而且他们也没写注释,我只能按照名字依次筛查,如果在注释中写下去向,我认为理解效率会大大提高。这些信号本质上也是为了实现函数的连接,达到某个条件之后就调用对应函数,就是这样
然后是socket套接字的使用,socket->write()这个函数是出现最多的库函数,可以把数据写入中转站,然后双端从中拿取数据
篇末总结
这篇博客写完笔者其实不是很满意,因为我觉得自己没讲清楚,一个原因是我还不太理解,另一个原因是代码量太大,不写注释方向很难找,还有就是学长的编程风格与我不同,按功能细分函数固然好,但是这样太灵活了,我想要跟上思路很困难,下一篇我会更新打卡板块的写作,在那一篇我应该会有更深入的理解
笔者还有一些感悟:
-
即使是加上计算机网络,也只不过是在更大维度上增删改查罢了,本质上仍围绕数据操作,但增加了网络传输和跨设备协作的复杂性
-
服务器、数据库、客户端的关系如图:

-
学习学长的优质代码,代码结构要以功能为单位,以调用为联系,穿针引线一样来做项目,这样的代码清晰度高,耦合性低,可复用性强
-
注释一定要写,一个函数的功能、一个信号的去向、一个类的作用,都要写清楚,一方面便于别人理解,一方面给自己当备忘录
还有一些从技术引申到其他方面的启示:
对于一个事物,粗浅了解它之后、宏观了解它之后、细致了解它之后理解是完全不一样的。笔者震惊地发现,我最开始对这个项目提出的问题和我在解析了代码之后提出的问题在架构、本质、方法上都有很多的不同