QT网盘项目-DAY3-登录注册功能

本文未经授权,禁止转载

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是不现实的,想要源码的话,就自己去历史提交里找吧。是下面这几次提交。

        NetdiskProject: QT网盘项目的实现。 - Gitee.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值