本文未经授权,禁止转载
Day2回顾-协议设计
在Day2中,我们完成了以下内容。
- 通过自定义消息结构体,我们解决了TCP的粘包问题
- 在客户端,通过添加输入框和发送按钮,以及发送消息的槽函数,完成了客户端向服务器发送消息的功能测试
- 在服务器,通过readyRead 信号和 recvMsg 槽函数,完成了接收客户端消息的功能测试。
Day3要完成的内容
验证客户端与服务器的收发能力正常之后,我们就开始完成真正的功能了。
首先要实现的就是注册用户和用户登录的功能。
一、数据库表的设计
想要实现登录注册功能,登录注册时需要在客户端输入账号密码,然后将账号密码发送到服务器,服务器在数据库的用户信息表中检索用户信息和密码,完成登录或者注册操作。
因此,我们需要在数据库中完成表的设计。
那想要实现登录和注册的功能,数据库中需要那些表?表中需要那些字段?字段的类型该怎么设计?字段的约束该怎么选择?
1.1 用户信息表设计
user_info
1、id是主键,为了唯一区别一条数据,每个用户都有一个唯一对应的id,因此需要添加主键约束和自增约束。
2、用户信息表中,主要的字段就是 name和pwd,目的是为了在登录注册的时候,输入用户名和密码,通过验证数据库中的name和pwd字段,来进行登录或注册。
将name和pwd的类型都设为varchar(32),用户名和密码最大为32个字节,且不能为空值。
思考:为什么要设计成32个字节呢?
这里首要目的是为了限制用户名和密码的长度,其次,还记得我们自定义的消息结构体中,固定长度数组(caData)的大小是多少么?(caData[64])
刚好是64个字节,前32个字节存储用户名,后32个字节存储密码,因此在实现登录注册功能时,是不需要使用柔性数组的,能够有效的节省内存空间。
3、online字段,保存了用户的登录状态,tinyint(1),相当于bool类型,并设置默认为0,默认为未登录状态,用户成功登录后,online字段变为1。
1.2 好友关系表设计
friend
该表是为了存储用户的好友信息表。
1、id是主键,为了唯一区别一条数据,因此需要添加主键约束和自增约束。
2、user_id 是 用户id,添加外键约束,对应用户信息表中的id。
3、friend_id 是 用户的好友id,添加外键约束,对应用户信息表中的id。
1.3 MySql安装
MySql使用的版本是5.7.3
MySQL-Front 使用的版本是6.1.1
建议和我同一版本,否则会导致QT无法连接数据库。
安装包请自行下载
链接:https://pan.baidu.com/s/1bLmpAiVlOuBPnkKqrE5wPA?pwd=1223
提取码:1223
--来自百度网盘超级会员V4的分享
安装的教程参考下面的文章。
MySQL的简介及MySQL和MySQL-front的下载安装_mysql-front安装下载-CSDN博客
1.4 在数据库中创建表
打开MySQL-Front 图形界面。选择SQL编辑器
1、创建数据库(NetdiskServer)
create database if not exists NetdiskServer;
2、创建用户信息表
create table if not exists user_info(
id int primary key auto_increment,
name varchar(32) not null,
pwd varchar(32) not null,
online tinyint(1) default 0
);
3、创建好友关系表
create table if not exists friend(
id int primary key auto_increment,
user_id int not null,
friend_id int not null,
foreign key(user_id) references user_info(id),
foreign key(friend_id) references user_info(id)
);
4、测试-向两个表中插入几条数据
insert into user_info(name,pwd)
values
('weihong','171223'),
('shisan','180110');
insert into friend(user_id,friend_id) values(1,2);
查看表中是否插入数据。
二、加载数据库
通过上面的操作,我们已经完成了数据库表的设计,接下来,我们需要将数据库加载到QT项目中。只有服务器会对数据库进行操作,因此要在数据库中连接数据库即可。
2.1 连接数据库
2.1.1 添加数据库模块,创建数据库操作类OperateDB
1、添加sql模块
想要使用sql,需要在.pro 文件中添加 sql模块。
2、 创建数据库操作类,OperateDB
在服务器端,创建一个操作数据库的类OperateDB,继承自QObject,涉及到数据库操作的函数,都要放在这个类下实现。
2.1.2 定义数据库对象,实现单例模式
1、定义数据库对象
在OperateDB类中,定义一个数据库(QSqlDatabase)的对象,作为数据库操作类的成员变量,这个变量的目的是为了连接数据库时,传入相应的配置。
2、实现操作数据库类的单例模式
为什么要将操作数据库的对象实现为单例模式?
(1)保证一个类仅有一个实例,并提供一个访问它的全局访问点,目的是想控制实例数目,节省系统资源,在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。
(2)数据库操作是非常敏感的问题,Windows 是多进程多线程的,在操作数据库的时候,就不可避免地出现多个进程或线程同时操作同一个数据表的现象,这样是比较危险的,所以必须通过唯一的实例来进行。我们在调用getInstance()函数来返回静态局部引用对象,能够防止调用拷贝构造,静态变量只能赋值一次。在C++11之后,这样的操作是默认支持线程安全的。
因此,实现单例模式,需要进行以下操作
(1)在OperateDB类中,私有化构造函数,将拷贝构造和 拷贝赋值运算符删除
// 将构造函数私有化或删除,防止创建对象
explicit OperateDB(QObject *parent = nullptr);
// 删除拷贝构造
OperateDB(const OperateDB& install) = delete ;
// 删除拷贝赋值运算符
OperateDB operator=(const OperateDB&) =delete ;
(2)定义静态成员函数来获取单例对象,在静态函数内部返回静态局部引用对象,能够防止调用拷贝构造,静态变量只能赋值一次。
// 获取数据库的单例对象
OperateDB& OperateDB::getInstance()
{
// 返回静态局部引用对象,防止调用拷贝构造,静态变量只能赋值一次
// 在C++11之后,默认支持线程安全
static OperateDB instance;
return instance;
}
2.1.3 在构造中创建数据库对象,在析构中关闭数据库连接
1、在构造函数中创建数据库对象
构造函数中通过QSqIDatabase::addDatabase()函数静态函数创建一个数据库对象QSqlDatabase,并将其赋值给我们创建的成员变量m_db。目的是为了在连接数据库是,利用m_db对象传入数据库的相关配置(主机名、端口号、数据库名称等)
operatedb.cpp
// 构造函数
OperateDB::OperateDB(QObject *parent) : QObject(parent)
{
// 添加数据库,并使用成员变量接收
m_db = QSqlDatabase::addDatabase("QMYSQL");
}
2、在析构函数中关闭数据库的连接
服务器进程结束,我们就需要将数据库断开连接,因此需要在析构函数中,将数据库断开连接。
// 析构函数
OperateDB::~OperateDB()
{
// 关闭数据库
m_db.close();
}
2.1.4 定义连接数据库函数,输入配置并连接数据库
在OperateDB类中定义一个connect函数,用来完成数据库的相关配置,并连接数据库。
传入的配置包括主机名(这里localhost代表本机)、端口号、数据库名称、用户名、和密码
配置内容在 MySQL-Front 中查看,在MySQL-Front选择文件 -----》 连接管理
选择用户----》点击属性
这里就是你的配置。挨个输入即可。
// 连接数据库的函数
void OperateDB::connect()
{
// 添加数据库的信息
m_db.setHostName("localhost"); // 主机名-本机
m_db.setPort(3306); // 端口号
m_db.setDatabaseName("NetdiskServer"); // 数据库名称
m_db.setUserName("root"); // 用户名
m_db.setPassword("xxxxxx"); // 密码
if(m_db.open()) // 连接成功的话,进行提示
{
qDebug()<<"数据库连接成功!";
}
else
{ // 连接失败,提示错误信息。
QMessageBox::critical(0,"DataBase Error",m_db.lastError().text());
}
}
在主函数中,调用connect即可。
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Server w;
// w.show();
// 通过创建的实例,调用connect函数,连接数据库
OperateDB::getInstance().connect();
return a.exec();
}
2.2 添加驱动文件
想要实现QT连接数据库,需要在QT的安装路径下添加两个驱动文件。请自行下载。
链接:https://pan.baidu.com/s/1f8oL-MOiAPBh_yWdEj0DLA?pwd=1223
提取码:1223
--来自百度网盘超级会员V4的分享
(1)将 qsqlmysql.d 放入 Qt 安装路径下的 5.14.2/mingw73_64/plugins/sqldrivers 目录下
(2)将 libmysql.d 放入 Qt安装路径下的5.14.2/mingw73_64/bin 目录下
注意:是QT的安装路径,不是MySql的安装路径。
完成之后,重新构建服务器的项目文件NetdiskServer,然后运行服务器项目即可
运行后,输出打印了 数据库连接成功!
恭喜你!!!
你已经完成了今天最重要的内容,连接数据库。
注意:如果你的输出 报错 driver not loaded
恭喜你!!中大奖了
【腾讯文档】Qt连接MySQL报错driver not loaded
https://docs.qq.com/doc/DTXNUZmFBZ2lvVWxa
请参照该文档或百度自行解决。
三、UI设计
想要实现登录和注册用户的功能,我们得在客户端设计一个登录注册的界面,总不能用意念进行登录注册哈?
那登录和注册功能的界面需要那些控件呢?每个控件需要实现什么功能呢?
(1)需要一个用户名的输入框,来记录当前要注册或者登录的用户名
(2)需要一个密码的输入框,来记录当前需要注册或登录的密码
(3)需要一个登录的按钮,点击登录按钮,触发登录按钮的点击信号,来执行登录的操作
(4)需要一个注册的按钮,点击注册按钮,触发注册按钮的点击信号,来执行注册的操作
1、我们需要点击client.ui来进入设计界面,进行登录界面的设计
2、添加控件,并修改控件对象的变量名
通过拖拽的方式,添加控件,并修改这些控件的变量名(确保变量名规范)
还可以适当的调整控件的布局已经文字的大小。这里不做太多赘述(请自行美化界面)
输入框:QLineEdit
按钮:QPushButton
3、分别右键登录和注册按钮 ----》转到槽 -----》选择 clicked()点击信号
分别生成登录按钮的槽函数和注册按钮的槽函数,方便后面使用。
这里不回的,可以回去看 Day2 ------- 4.2 UI设计
讲过的内容,不在赘述。
四、添加接口的流程
接下来我们就要具体的完成登录和注册功能了,在实现具体功能之前,我们要思考以下问题。
完成某个功能的具体流程是什么?
在这个具体的流程中客户端需要做什么?
服务器需要做什么?
服务器完成之后要不要给客户端一个反馈?
客户端接收到服务器的反馈之后,还需要进行其他操作么?
这些内容在实际的项目开发中是非常重要的,在写代码之前就需要设计好具体功能的流程,你总不能上来就库库一顿写,写到一半,都不知道自己在干什么。作为一个新时代的码农,这样的编码过程是十分失败的(狗头保命)。
下面我们就探讨一下,某个功能的具体流程是怎样的,以后在实现具体功能时,都按照这个流程去思考问题。
- 客户端发送请求(目标功能发送服务器需要的数据)
- 取到需要发送给服务器的数据
- 构建自定义消息结构体对象pdu(考虑柔性数组(caMsg)中存不存数据,存的数据多长)
- 在PDU协议中的添加对应功能的请求和响应消息类型
- 在pdu填入消息类型和要发送的数据
- 发送 pdu到服务器
- 服务器接收、处理并响应请求(目标如何处理请求)
- 判断该客户端的请求是否需要进行数据库操作。如果需要数据库操作,则需要在数据库操作类中添加对应的函数来执行 sql语句。如果不需要数据库操作,则实现普通的处理函数。(一般都是要进行数据库操作的)
- 在服务器的接收消息的函数中,根据不同消息类型进行处理。
- 取出 pdu 中的数据,调用处理函数。(如果需要数据库操作,则调用数据库操作函数进行处理,如果不需要数据库操作,则调用普通的处理函数)
- 调用处理函数完成,判断是否要对客户端进行响应。(如果需要响应,则要接收处理函数的返回值。如果不需要响应,则无需其他操作)
- 构建发给客户端的响应 pdu,将结果放入响应 pdu。
- 将响应 pdu 发送给客户端。
- 客户端接收响应并显示结果
- 接收消息函数中根据消息类型进行处理
- 将结果显示给用户
上面的内容就讲诉了完成该项目的一个功能时的流程,只有在完成思考的时候,再去编码,才能胸有成竹,减少出问题的次数。
在之后的功能设计中,在编码前,都应该先实现流程的设计,再去进行实际的编码工作。
五、注册功能
5.1 注册功能的流程
下面我们来分析一下,注册功能具体有那些流程?
- 客户端点击注册按钮,发送注册请求(注册按钮的槽函数发送用户名和密码给服务器)
- 从输入框中取出用户名和密码
- 构建自定义消息结构体对象pdu(不使用柔性数组(caMsg),使用固定长度的数组(caData[64]))
- 通过用户信息表设计,我们知道,用户名和密码的最大长度都是32个字节,加起来正好是固定数组(caData[64])的长度,因此我们在固定数组的前32个字节中存储用户名,后32个字节存储密码即可。
- 因此在初始化PDU时,传入0即可。
- 在PDU协议中的添加对应功能的请求和响应消息类型
- 在消息类型的枚举值中,添加注册的请求和响应
- 在pdu填入消息类型和要发送的数据(用户名和密码)
- 发送 pdu到服务器
- 服务器接收、处理并响应注册请求(在用户表中添加一条数据)
- 需要数据库操作,在数据库操作类中,添加处理注册的函数
- 根据用户名判断用户是否已存在(利用select查找用户名)
- 用户存在,注册失败;用户不存在,则添加一条新的数据,返回注册成功
- 在服务器的接收消息的函数中,处理消息类型为注册请求的情况。
- 取出 pdu 中的数据,调用处理函数,将取出的数据传给处理数据库操作类中注册的函数。
- 得到处理注册的函数的返回值,得到处理结果
- 构建发给客户端的响应 pdu,消息类型为 注册响应
- 将处理结果放入响应 pdu。
- 将响应 pdu 发送给客户端。
- 需要数据库操作,在数据库操作类中,添加处理注册的函数
- 客户端接收注册响应并显示结果
- 客户端的接收消息函数中,根据消息类型进行处理,进入注册响应的处理阶段
- 将结果显示给用户
这样我们就完成了注册流程的分析。下面,我们就按照这个流程来进行编码。只有这样,你的编码过程才能够避免很多问题。
5.2 客户端发送注册请求
在ui界面,选择注册的按钮,转到注册按钮点击信号的槽函数(不明白的去看Day2---4.2UI设计)Client::on_regist_PB_clicked()
5.2.1 获取输入框内容,初始化PDU对象
1、从输入框中取出用户名和密码
客户端想要注册一个新的用户,是要将用户名和密码发送给服务器的,因此需要将用户名和密码从输入框中取出来。输入框的对象在ui中。通过ui调用分别调用用户名和密码输入框的对象名即可。
// 从ui对象中选取用户名和密码的输入框,并读出内容
QString strName = ui->userName_LE->text();
QString strpwd = ui->pwd_LE->text();
2、检测输入数据的格式
// 判空,如果是空,直接返回
if(strName.isEmpty()||strpwd.isEmpty()) return;
// 检查输入内容长度,根据表的设计,数据长度应该小于32
if(strName.toStdString().size()>32||strpwd.toStdString().size()>32)
{
// 提示错误信息
QMessageBox::critical(this,"输入内容非法","用户名或密码长度应小于32");
return;
}
// 检查密码长度,要求密码必须大于8位
if(strpwd.toStdString().size()<8)
{
// 提示错误信息
QMessageBox::critical(this,"输入内容非法","密码长度应大于8)");
return;
}
3、构建自定义消息结构体对象pdu
通过用户信息表设计,我们知道,用户名和密码的最大长度都是32个字节,加起来正好是固定数组(caData[64])的长度,因此我们在固定数组的前32个字节中存储用户名,后32个字节存储密码即可。
因此不使用柔性数组(caMsg),使用固定长度的数组(caData[64]),在初始化PDU时,传入0即可。
// 初始化一个 PDU对象
PDU* pdu = initPDU(0);
5.2.2 添加消息类型,并传入数据
在消息类型的枚举值中,添加注册的请求和响应的枚举值。
给PDU对象,传入消息类型,消息类型为 注册请求。将用户名和密码存储到caData[64]中。
// 输入消息类型为,注册请求
pdu->uiMsgType = ENUM_MSG_TYPE_REGIST_REQUEST;
// 将用户名和密码放入caData中,用户名防止前32位,密码放在后32位。
memcpy(pdu->caData,strName.toStdString().c_str(),32);
memcpy(pdu->caData+32,strpwd.toStdString().c_str(),32);
5.2.3 发送 pdu到服务器,释放pdu
利用socket,调用write将数据发送到服务器。
// 通过socket将消息发送到服务器
m_tcpSocket.write((char*)pdu,pdu->uiPDULen);
// 释放pdu
free(pdu);
// 将pdu置为空
pdu = NULL;
测试:客户端输入消息
5.3 服务器处理客户端注册请求,并发回响应
5.3.1 在数据库操作类中,添加处理注册的函数
我们要明白,客户端发来注册请求,以及用户名和密码。服务器是要到数据库中进行操作的。具体操作的流程就是,在数据库的用户信息表中查找用户名,如果用户名已经存在,则注册失败,该函数返回注册失败;如果用户名不存在,则需要在数据库中添加一条新的数据,并返回注册成功。
因此需要在数据库操作类OperateDB中添加一个处理注册的函数handleRegist()
1、判断输入是否为空,为空直接返回错误
// 判断参数是否为空指针
if(name==NULL || pwd==NULL)
{
return false;
}
2、判断当前注册的用户是否已存在
(1)将sql语句保存到QString中,'%1'是占位符。利用.arg(name),输入变量
(2) 创建一个 QSqlQuery 对象,用来执行 sql语句
(3)q.exec(sql)执行语句,返回值为是否成功,如果成功了,就去执行q.next()
(4)q.next() 如果取到结果了,说明该用户名是存在的,存在就不能注册新用户,要返回错误
// 判断用户是否存在
// 根据用户名查找用户的语句,利用arg传递变量('%1' 是占位符)
QString sql = QString("select * from user_info where name = '%1'").arg(name);
// 创建一个 QSqlQuery 对象,用来执行 sql语句
QSqlQuery q;
// q.exec(sql)执行语句,返回值为是否成功,如果成功了,就去执行q.next()
// q.next() 如果取到结果了,说明该用户名是存在的
if(!q.exec(sql)||q.next())
{
return false;
}
3、当前用户不存在,则添加一条新数据
// 插入一条数据
sql = QString("insert into user_info(name,pwd) values('%1','%2')").arg(name).arg(pwd);
// 返回执行结果
return q.exec(sql);
operatedb.cpp
bool OperateDB::handleRegist(const char* name, const char* pwd)
{
// 判断参数是否为空指针
if(name==NULL || pwd==NULL)
{
return false;
}
// 判断用户是否存在
// 根据用户名查找用户的语句,利用arg传递变量('%1' 是占位符)
QString sql = QString("select * from user_info where name = '%1'").arg(name);
// 创建一个 QSqlQuery 对象,用来执行 sql语句
QSqlQuery q;
// q.exec(sql)执行语句,返回值为是否成功,如果成功了,就去执行q.next()
// q.next() 如果取到结果了,说明该用户名是存在的
if(!q.exec(sql)||q.next())
{
return false;
}
// 插入一条数据
sql = QString("insert into user_info(name,pwd) values('%1','%2')").arg(name).arg(pwd);
// 返回执行结果
return q.exec(sql);
}
5.3.2 在服务器的接收消息的函数中,处理消息类型为注册请求的情况
我们在Day2中完成了接收消息的函数MyTcpSocket::recvMsg()
此时,客户端发来的消息已经存储到pdu中。
mytcpsocket.cpp
// 接收消息的槽函数
void MyTcpSocket::recvMsg()
{
// 打印socket中的数据长度
qDebug()<<"socket中接收到的数据长度:"<<this->bytesAvailable();
// 读出消息结构体的总长度,这部分内容会从socket中读出去
uint uiPDULen = 0; // 传出参数
// 读取的大小为 消息结构体总长度的数据类型大小
this->read((char*)&uiPDULen,sizeof(uint));
// 计算实际消息长度--柔性数组长度(消息结构体总长度-结构体大小)
uint uiMsgLen = uiPDULen-sizeof(PDU);
// 初始化一个PUD结构体,用于存储
PDU* pdu = initPDU(uiMsgLen);
// 读出socket中剩余的数据,消息结构体的总长度已经被读出去了,剩下的不是一个完整的PDU结构体。
// 因此需要对pdu这个指针进行偏移,偏移的长度就是已经读出的数据长度(消息结构体总长度的数据类型大小)
// 而剩余需要读出的内容为,消息结构体总长度 减去 消息结构体总长度的数据类型大小
this->read((char*)pdu+sizeof(uint),uiPDULen-sizeof(uint));
// 释放pdu
free(pdu);
pdu=NULL;
}
在这个函数中,我们已经拿到了消息的全部内容,并存储在pdu中。因此,我们需要根据pdu中的不同消息类型,进行不同的处理,所以我们需要用到switch语句来进行分支处理。
这里需要我们实现处理消息类型为注册请求的情况 ENUM_MSG_TYPE_REGIST_REQUEST
1、将用户名和密码从pdu中取出,并传给数据库操作类中处理注册的函数,拿到返回值
// 定义两个数组,用来存放用户名和密码
char caName[32] = {'\0'};
char caPwd[32] = {'\0'};
// 将用户名和密码从pdu的caData中取出,并存放到数组中
memcpy(caName,pdu->caData,32);
memcpy(caPwd,pdu->caData+32,32);
// 将用户名和密码传给处理注册的数据库操作函数,得到返回值
bool ret = OperateDB::getInstance().handleRegist(caName,caPwd);
2、构建一个响应注册请求的PDU对象,将得到的返回值发回客户端
// 向客户端发送响应
// 初始化响应注册的PDU对象
PDU* registPdu = initPDU(0);
// 消息类型为注册响应
registPdu->uiMsgType = ENUM_MSG_TYPE_REGIST_RESPOND;
// 将消息存储到消息结构体
memcpy(registPdu->caData,&ret,sizeof(bool));
// 利用socket 向客户端发送 注册的响应
write((char*)registPdu,registPdu->uiPDULen);
break;
mytcpsocket.cpp ---recvMsg()
void MyTcpSocket::recvMsg()
{
// 打印socket中的数据长度
qDebug()<<"socket中接收到的数据长度:"<<this->bytesAvailable();
// 读出消息结构体的总长度,这部分内容会从socket中读出去
uint uiPDULen = 0; // 传出参数
// 读取的大小为 消息结构体总长度的数据类型大小
this->read((char*)&uiPDULen,sizeof(uint));
// 计算实际消息长度--柔性数组长度(消息结构体总长度-结构体大小)
uint uiMsgLen = uiPDULen-sizeof(PDU);
// 初始化一个PUD结构体,用于存储
PDU* pdu = initPDU(uiMsgLen);
// 读出socket中剩余的数据,消息结构体的总长度已经被读出去了,剩下的不是一个完整的PDU结构体。
// 因此需要对pdu这个指针进行偏移,偏移的长度就是已经读出的数据长度(消息结构体总长度的数据类型大小)
// 而剩余需要读出的内容为,消息结构体总长度 减去 消息结构体总长度的数据类型大小
this->read((char*)pdu+sizeof(uint),uiPDULen-sizeof(uint));
// 根据消息类型对消息进行处理
switch (pdu->uiMsgType)
{
// 注册请求
case ENUM_MSG_TYPE_REGIST_REQUEST:
{
// 将消息取出
char caName[32] = {'\0'};
char caPwd[32] = {'\0'};
memcpy(caName,pdu->caData,32);
memcpy(caPwd,pdu->caData+32,32);
// 测试
qDebug()<<"recvMsg REGIST caName: "<<caName;
qDebug()<<"recvMsg REGIST caPwd: "<<caPwd;
// 处理消息
bool ret = OperateDB::getInstance().handleRegist(caName,caPwd);
// 向客户端发送响应
// 初始化响应注册的PDU对象
PDU* registPdu = initPDU(0);
// 消息类型为注册响应
registPdu->uiMsgType = ENUM_MSG_TYPE_REGIST_RESPOND;
// 将消息存储到消息结构体
memcpy(registPdu->caData,&ret,sizeof(bool));
// 利用socket 向客户端发送 注册的响应
write((char*)registPdu,registPdu->uiPDULen);
break;
}
default:
break;
}
// 释放pdu
free(pdu);
pdu=NULL;
}
5.4 客户端接收服务器发回的注册响应
- 客户端的接收消息函数中,根据消息类型进行处理,进入注册响应的处理阶段
- 将结果显示给用户
client.cpp ---recvMsg()
// 根据消息类型对消息进行处理
switch (pdu->uiMsgType)
{
// 注册响应
case ENUM_MSG_TYPE_REGIST_RESPOND:
{
// 将消息取出
bool ret;
memcpy(&ret,pdu->caData,sizeof(bool));
// 根据返回的响应进行处理
if(ret)
{
QMessageBox::information(this,"注册","注册成功");
}
else
{
QMessageBox::information(this,"注册","注册失败:用户名或密码非法");
}
break;
}
default:
break;
}
六、登录功能
6.1 登录功能的流程
下面我们来分析一下,登录功能具体有那些流程?
- 客户端点击登录按钮,发送登录请求(登录按钮的槽函数发送用户名和密码给服务器)
- 定义一个成员变量,记录登录成功的用户名,以便后续调用
- 从输入框中取出用户名和密码
- 构建自定义消息结构体对象pdu(不使用柔性数组(caMsg),使用固定长度的数组(caData[64]))
- 在PDU协议中的添加对应功能的请求和响应消息类型
- 在消息类型的枚举值中,添加登录的请求和响应
- 在pdu填入消息类型和要发送的数据(用户名和密码)
- 发送 pdu到服务器
- 服务器接收、处理并响应登录请求(在用户表中验证用户名和密码)
- 在myTcpSocket类中,定义一个成员变量,存储登录成功的用户名
- 需要数据库操作,在数据库操作类中,添加处理登录的函数
- 根据用户名判断用户和密码是否正确(利用select查找用户名和密码)
- 用户存在密码正确,登录成功;用户不存在密码不正确,登录失败。
- 登录成功,将用户信息表中的online字段改为1
- 在服务器的接收消息的函数中,处理消息类型为登录请求的情况。
- 取出 pdu 中的数据,调用处理函数,将取出的数据传给处理数据库操作类中处理登录的函数。
- 得到处理登录的函数的返回值,得到处理结果
- 如果登录成功,将登录成功的用户名存储到成员变量中
- 构建发给客户端的响应 pdu,消息类型为 登录响应
- 将处理结果和登录成功的用户名,放入响应 pdu。
- 将响应 pdu 发送给客户端。
- 客户端接收登录响应并显示结果
- 客户端的接收消息函数中,根据消息类型进行处理,进入登录响应的处理阶段
- 如果登录成功,将登录成功的用户名存储到成员变量中
- 将结果显示给用户
6.2 客户端发送登录请求
在ui界面,选择登录的按钮,转到登录按钮点击信号的槽函数(不明白的去看Day2---4.2UI设计)Client::on_login_PB_clicked()
- 客户端点击登录按钮,发送登录请求(登录按钮的槽函数发送用户名和密码给服务器)
- 定义一个成员变量,记录登录成功的用户名,以便后续调用
- 从输入框中取出用户名和密码
- 构建自定义消息结构体对象pdu(不使用柔性数组(caMsg),使用固定长度的数组(caData[64]))
- 在PDU协议中的添加对应功能的请求和响应消息类型
- 在消息类型的枚举值中,添加登录的请求和响应
- 在pdu填入消息类型和要发送的数据(用户名和密码)
- 发送 pdu到服务器
代码同注册请求没有太大差别,这里不做解释了。
在Client类中,定义一个成员变量,记录登录成功的用户名,以便后续调用
client.cpp
// 登录按钮的槽函数
void Client::on_login_PB_clicked()
{
// 从ui对象中选取 输入框,并读出内容
QString strName = ui->userName_LE->text();
QString strpwd = ui->pwd_LE->text();
// 判空,如果是空,直接返回
if(strName.isEmpty()||strpwd.isEmpty()) return;
// 检查输入内容长度,根据表的设计,数据长度应该小于32
if(strName.toStdString().size()>32||strpwd.toStdString().size()>32)
{
QMessageBox::critical(this,"输入内容非法","用户名或密码长度应小于32");
return;
}
// 检查密码长度,要求密码必须大于8位
if(strpwd.toStdString().size()<8)
{
QMessageBox::critical(this,"输入内容非法","密码长度应大于8)");
return;
}
// 初始化一个 PDU对象
PDU* pdu = initPDU(0);
// 输入消息类型为,登录请求
pdu->uiMsgType = ENUM_MSG_TYPE_LOGIN_REQUEST;
// 将用户名和密码放入caData中,用户名防止前32位,密码放在后32位。
memcpy(pdu->caData,strName.toStdString().c_str(),32);
memcpy(pdu->caData+32,strpwd.toStdString().c_str(),32);
// 测试--打印检测发送的内容
qDebug()<<"login uiMsgType: "<<pdu->uiMsgType;
qDebug()<<"login strName: "<<pdu->caData;
qDebug()<<"login strpwd: "<<pdu->caData+32;
// 通过socket将消息发送到服务器
m_tcpSocket.write((char*)pdu,pdu->uiPDULen);
// 释放pdu
free(pdu);
// 将pdu置为空
pdu = NULL;
}
6.3 服务器处理登录请求,并作出相应
- 服务器接收、处理并响应登录请求(在用户表中验证用户名和密码)
- 需要数据库操作,在数据库操作类中,添加处理登录的函数
- 根据用户名判断用户和密码是否正确(利用select查找用户名和密码)
- 用户存在密码正确,登录成功;用户不存在密码不正确,登录失败。
- 登录成功,将用户信息表中的online字段改为1
- 在服务器的接收消息的函数中,处理消息类型为登录请求的情况。
- 取出 pdu 中的数据,调用处理函数,将取出的数据传给处理数据库操作类中处理登录的函数。
- 得到处理登录的函数的返回值,得到处理结果
- 构建发给客户端的响应 pdu,消息类型为 登录响应
- 将处理结果放入响应 pdu。
- 将响应 pdu 发送给客户端。
- 需要数据库操作,在数据库操作类中,添加处理登录的函数
在myTcpSocket类中,定义一个成员变量,存储登录成功的用户名
6.3.1 数据库操作类添加处理登录信息的函数
- 根据用户名判断用户和密码是否正确(利用select查找用户名和密码)
- 用户存在密码正确,登录成功;用户不存在密码不正确,登录失败。
- 登录成功,将用户信息表中的online字段改为1
operatedb.cpp
// 处理登录的函数
bool OperateDB::handleLogin(const char *name, const char *pwd)
{
// 判断参数是否为空指针
if(name==NULL || pwd==NULL)
{
return false;
}
// 判断用户是否存在,密码是否正确
QString sql = QString("select * from user_info where name = '%1' and pwd = '%2'").arg(name).arg(pwd);
// 创建一个 QSqlQuery 对象,用来执行 sql语句
QSqlQuery q;
// q.exec(sql)执行语句,返回值为是否成功,如果成功了,就去执行q.next()
// q.next() 如果取到结果了,说明该用户名是存在的,密码也正确
if(q.exec(sql)&&q.next())
{
// 用户登录状态改为1
QString sql = QString("update user_info set online = 1 where name = '%1' and pwd = '%2'").arg(name).arg(pwd);
if(q.exec(sql))
{
// 修改成功,返回真
return true;
}
else
{ // 修改失败,返回错误
return false;
}
}
else
{ // 执行失败,或没取到结果,返回错误
return false;
}
}
6.3.2 接收消息的函数中,根据根据消息的类型,调用处理登录的函数,得到返回值,并发送相应给客户端
- 在服务器的接收消息的函数中,处理消息类型为登录请求的情况。
- 取出 pdu 中的数据,调用处理函数,将取出的数据传给处理数据库操作类中处理登录的函数。
- 得到处理登录的函数的返回值,得到处理结果
- 如果登录成功,将登录成功的用户名存储到成员变量中
- 构建发给客户端的响应 pdu,消息类型为 登录响应
- 将处理结果和登录成功的用户名,放入响应 pdu。
- 将响应 pdu 发送给客户端。
mytcpsocket.cpp ---recvMsg()
// 登录请求
case ENUM_MSG_TYPE_LOGIN_REQUEST:
{
// 将消息取出
char caName[32] = {'\0'};
char caPwd[32] = {'\0'};
memcpy(caName,pdu->caData,32);
memcpy(caPwd,pdu->caData+32,32);
// 测试
qDebug()<<"recvMsg LOGIN caName: "<<caName;
qDebug()<<"recvMsg LOGIN caPwd: "<<caPwd;
// 处理消息
bool ret = OperateDB::getInstance().handleLogin(caName,caPwd);
if(ret)
{
// 登录成功,记录登陆成功的用户名
m_LoginName = caName;
}
// 向客户端发送响应
// 初始化响应登录的PDU对象
PDU* loginPdu = initPDU(0);
// 消息类型为注册响应
loginPdu->uiMsgType = ENUM_MSG_TYPE_LOGIN_RESPOND;
// 将消息存储到消息结构体
memcpy(loginPdu->caData,&ret,sizeof(bool));
memcpy(loginPdu->caData+32,caName,32);
// 利用socket 向客户端发送 登录的响应
write((char*)loginPdu,loginPdu->uiPDULen);
// 释放 loginPdu
free(loginPdu);
loginPdu=NULL;
break;
}
6.4 客户端接收服务器发回的登录响应
- 客户端的接收消息函数中,根据消息类型进行处理,进入登录响应的处理阶段
- 如果登录成功,将登录成功的用户名存储到成员变量中
- 将结果显示给用户
client.cpp ---recvMsg()
// 登录响应
case ENUM_MSG_TYPE_LOGIN_RESPOND:
{
// 将消息取出
bool ret;
char caName[32] = {'\0'};
memcpy(&ret,pdu->caData,sizeof(bool));
memcpy(caName,pdu->caData+32,32);
// 根据返回的响应进行处理
if(ret)
{
// 登录成功,成员变量记录登录成功的用户名
m_LoginName = caName;
// 测试
qDebug()<<"recvMsg LOGIN_REQUEST m_userName"<<m_LoginName;
// 给出登录成功提示
QMessageBox::information(this,"登录","登录成功");
}
else
{ // 登录失败,给出错误提示
QMessageBox::information(this,"登录","登录失败:用户名或密码非法");
}
break;
}
七、处理客户端关闭(客户端下线)
在客户端登录的功能中,我们将登陆成功用户的online字段置为了1,但是,如果用户关闭了客户端(即客户端断开连接或下线),我们就应该将该用户的online字段置为0。
还记得,我们在客户端与服务器建立连接时,将成功建立连接的socket存储到了一个列表中m_tcpSocketList。因此,除了要将用户信息表中的online字段置为1,还需要将断开连接的socket从这个存储socket的列表中删除。
下面我们来分析一下,处理客户端下线具体有那些流程?
- 用户关闭客户端(无需其他操作)
- 服务器处理客户端下线(接收客户端下线的信号,将下线用户的online字段置为1)
- 需要数据库操作,在数据库操作类中定义处理客户端下线的函数
- 根据用户名,将下线用户的online字段置为1
- MyTcpSocket类中增加客户端下线的槽函数,连接disconnected信号
- 在mytcpsocket类中,我们可以通过disconnected信号来查看是socket是否断开连接,一旦socket断开连接,就会触发disconnected信号
- 定义一个客户端下线的槽函数,将disconnected信号与该槽函数进行关联
- 槽函数中,调用数据库操作类中处理下线的函数
- 下线的 socket从 socket列表中移除,通过下线信号通知 MyTcpServer移除
- MyTcpSocket定义下线信号,通过处理下线槽函offline数发出下线信号
- MyTcpServer定义删除socket的槽函数,一旦下线信号被触发,就调用删除socket的函数,将socket从socket列表中移除。
- 在 MyTcpServer的incomingConnection 连接定义的下线信号和删除下线socket的槽函数
-
- 需要数据库操作,在数据库操作类中定义处理客户端下线的函数
7.1 数据库操作类中,定义处理下线的函数
在数据库操作类中定义处理客户端下线的函数,根据用户名,将下线用户的online字段置为1
operatedb.cpp
// 处理下线的函数
void OperateDB::handleOffline(const char *name)
{
// 判断参数是否为空指针
if(name==NULL)
{
return;
}
QString sql = QString("update user_info set online = 0 where name = '%1'").arg(name);
// 创建一个 QSqlQuery 对象,用来执行 sql语句
QSqlQuery q;
// 执行更新语句
q.exec(sql);
}
7.2 增加处理下线的槽函数,连接disconnected信号
- MyTcpSocket类中增加客户端下线的槽函数,连接disconnected信号
- 在mytcpsocket类中,我们可以通过disconnected信号来查看是socket是否断开连接,一旦socket断开连接,就会触发disconnected信号
- 定义一个客户端下线的槽函数,将disconnected信号与该槽函数进行关联
- 槽函数中,调用数据库操作类中处理下线的函数
1、MyTcpSocket类中增加客户端下线的槽函数
(1)添加下线信号
(2)添加处理客户端下线的槽函数
mytcpsocket.cpp
// 处理下线的槽函数
void MyTcpSocket::clientOffline()
{
// 将当前下线客户端的用户名 传给数据库进行下线操作
OperateDB::getInstance().handleOffline(m_LoginName.toStdString().c_str());
// 发送下线信号,将下线客户端的socket发送给TcpServer
emit offline(this);
}
2、MyTcpSocket类的构造函数中,将客户端下线的信号disconnected的信号和客户端下线的槽函数进行绑定
mytcpsocket.cpp
MyTcpSocket::MyTcpSocket()
{
// 将socket中,接收到信息触发的信号,与取出信息的信号槽函数进行关联
connect(this,&MyTcpSocket::readyRead,this,&MyTcpSocket::recvMsg);
// 将socket中,接收客户端关闭的信号,与处理下线的槽函数进行关联
connect(this,&MyTcpSocket::disconnected,this,&MyTcpSocket::clientOffline);
}
7.3 将下线的socket从列表中删除
- 下线的 socket从 socket列表中移除,通过下线信号通知 MyTcpServer移除
- MyTcpSocket定义下线信号,通过处理下线槽函offline数发出下线信号
- MyTcpServer定义删除socket的槽函数,一旦下线信号被触发,就调用删除socket的函数,将socket从socket列表中移除。
- 在 MyTcpServer的incomingConnection 连接定义的下线信号和删除下线socket的槽函数
1、MyTcpServer定义删除socket的槽函数
在客户端与服务器建立连接时,我们是在堆区new出了一块空间来存储新的socket,因此要释放掉。这里是不能使用delete释放的,因为即使客户端关闭了,是不会立马关闭的,得等一会才能关闭。
因此得调用等待关闭的函数。
// 删除已下线客户端socket的槽函数--将已下线的socket,从socket列表(m_tcpSocketList)中删除
void MyTcpServer::deleteSocket(MyTcpSocket *mytcpsocket)
{
// 将已下线的socket,从socket列表(m_tcpSocketList)中删除
m_tcpSocketList.removeOne(mytcpsocket);
// 在客户端与服务器建立连接时,我们是在堆区new 出了一块空间来 存储新的socket,因此要释放掉
// 这里是不能使用delete释放的,因为即使客户端关闭了,是不会立马关闭的,得等一会才能关闭
// 因此得调用等待关闭的函数
mytcpsocket->deleteLater();
mytcpsocket = NULL;
// 测试 是否移除成功
qDebug()<<"m_tcpSocketList size: "<<m_tcpSocketList.size();
for(int i= 0;i<m_tcpSocketList.size();i++)
{
qDebug()<<"m_LoginName: "<<m_tcpSocketList[i]->m_LoginName;
}
}
2、 在 MyTcpServer的incomingConnection 连接定义的下线信号和删除下线socket的槽函数
为什么要在socket连接成功的函数中连接删除socket的信号槽呢?
因为连接成功时,会产生一个新的socket,在这个socket断开连接时,也是由这个socket发送的下线信号disconnected。
因此就需要在一个socket建立连接时,就关联好下线的信号。
mytcpserver.cpp
// 重写连接成功的信号槽函数,并将连接成功的socket 放入 m_tcpSocketList 列表中。
void MyTcpServer::incomingConnection(qintptr handle)
{
// 展示连接成功。
qDebug()<<"新客户端连接成功";
// 将新连接的客户端socket 存到 m_tcpSocketList 列表中。
MyTcpSocket* tcpSocket = new MyTcpSocket;
tcpSocket->setSocketDescriptor(handle);
m_tcpSocketList.append(tcpSocket);
// 将 下线信号 和 删除已下线客户端socket的槽函数 进行关联
connect(tcpSocket,&MyTcpSocket::offline,this,&MyTcpServer::deleteSocket);
}
这样就完成的客户端下线的所以操作。
Day3总结
1、为了实现登陆注册功能,在数据库中添加了用户信息表等,并在QT中连接了数据库
2、完成了登录与注册的功能
3、完成了客户端下线的处理
注意:
再次声明本文未经授权,禁止转载
1、未进行客户端登录与注册的功能展示,请自行测试功能是否完善
2、对于登录与注册功能的响应,还有待改善,这里只是为了简单的实现功能,请自行完善
完整代码
代码量增加,把所有代码复制粘贴到CSDN是不现实的,想要源码的话,就自己去历史提交里找吧。是下面这几次提交。