恋恋风辰的个人博客


  • Home

  • Archives

  • Categories

  • Tags

  • Search

零基础C++(5) 引用类型

Posted on 2024-09-16 | In 零基础C++

类型分类

1. 内置类型(基本类型)

内置类型是C++语言自带的基本数据类型,主要包括以下几种:

  • 整型(Integer Types)
    • int:标准整型,通常为4字节。
    • short:短整型,通常为2字节。
    • long:长整型,通常为4或8字节(取决于平台)。
    • long long:更长的整型,通常为8字节。
    • unsigned:无符号整型,不支持负数。
    • unsigned short、unsigned long、unsigned long long:对应的无符号版本。
  • 字符型(Character Types)
    • char:字符型,通常为1字节。
    • wchar_t:宽字符型,通常为2或4字节(用于表示Unicode字符)。
    • char16_t、char32_t:用于表示UTF-16和UTF-32编码的字符。
  • 浮点型(Floating Point Types)
    • float:单精度浮点型,通常为4字节。
    • double:双精度浮点型,通常为8字节。
    • long double:扩展精度浮点型,通常为8字节或16字节(取决于平台)。
  • 布尔型(Boolean Type)
    • bool:布尔型,表示真(true)或假(false)。

2. 复合类型

复合类型是由内置类型或其他复合类型组合而成的类型,主要包括以下几种:

  • 数组(Array)
    • 一组相同类型的数据元素,可以通过索引访问。
  • 结构体(Struct)
    • 一种用户定义的数据类型,可以包含不同类型的数据成员。
  • 联合体(Union)
    • 与结构体类似,但所有成员共享同一内存位置,只有一个成员可以在任何给定时间存储值。
  • 枚举(Enum)
    • 一种用户定义的类型,用于定义一组命名的整型常量。
  • 类(Class)
    • C++的面向对象编程特性,允许定义包含数据和成员函数的复杂数据类型。
  • 指针(Pointer)
    • 指向其他类型的内存地址,可以用来动态分配内存和实现复杂的数据结构。
  • 引用(Reference)
    • 对现有变量的别名,提供了对变量的另一种访问方式。

引用类型

定义

C++ 中的引用类型是一种复合类型,它是对另一个变量的别名。在C++中使用引用,可以让我们直接访问和操作另一个变量的内存地址,而不需要通过指针的解引用操作。引用在语法上比指针更简洁,且在许多情况下更安全。

变量内存演示

上面的定义很多人看起来很吃力,那我们回想一下变量的存储。

https://cdn.llfc.club/1726452128688.jpg

上面的图表示:

  • 定义一个变量a,并且初始化为100,编译器会为变量a开辟空间,绿色的是开辟的空间,存储100,这个空间的首地址为0x2be3,也就是变量a的地址。
  • 地址大家可以裂解为门牌号,我们可以通过门牌号找到绿色的家,a是绿色的家的名字,进而取出家里的物品,数据100可以理解为物品。
  • 执行b = a,将a赋值给b后,编译器又开辟了一块空间,存储100,这个空间的首地址为0x3f2b。 打个比方,我们又创建了一个家,家的名字是b,家里也存储了100这个物品,但是这个家的地址和a的不一样。

引用内存演示

https://cdn.llfc.club/1726453490297.jpg

  • 我们同样定义了一个变量a,并为它开辟空间,存储100,空间的首地址为0x2be3
  • 我们定义了一个引用b,它是a的别名,所以b的地址和a的地址都一样,都是0x2be3

从上述图形可以看出,引用是变量的别名。

写法

引用的基本写法

1
2
3
4
// 定义变量a
int a = 100;
// 定义引用b并且指向a, b就是a的别名
int &b = a;

大家能看到,在定义引用b的时候在int和b之间我们加了&符号, 这个int &表示的就是b是int类型的引用变量。

这里再提前告诉大家一个方法查看a和b地址是否相同, 当我们想输出a和b的地址的时候,只需要在a和b前加&即可输出他们的地址

1
2
std::cout << "a的地址为:" << &a << std::endl;
std::cout << "b的地址为: " << &b << std::endl;

上面输出

1
2
a的地址为:0x7ff65cae3000
b的地址为: 0x7ff65cae3000

可以看到a和b的地址相同。也证明了a和b是指向同一个地址空间。所以我们修改a的值,b的值也会变

1
2
3
4
5
6
7
8
9
10
11
12
// 定义变量a
int a = 100;
// 定义引用b并且指向a, b就是a的别名
int &b = a;
// 输出a和b的值
std::cout << "a的值:" << a << std::endl;
std::cout << "b的值: " << b << std::endl;

// a和b是指向同一个变量。所以我们修改a的值,b的值也会变
a = 200;
std::cout << "修改a的值后,a和b的值分别为:\n" << a << std::endl;
std::cout << b << std::endl;

程序输出如下

1
2
3
4
5
a的值:100
b的值: 100
修改a的值后,a和b的值分别为:
200
200

可以看到修改了a的值,b也跟着变化了,接下来我们修改b的值

1
2
3
4
// 修改b的值,a的值也会变
b = 300;
std::cout << "修改b的值后,a和b的值分别为:\n" << a << std::endl;
std::cout << b << std::endl;

程序输出

1
2
3
修改b的值后,a和b的值分别为:
300
300

可以看到b的值修改了,a的值也变化了

1
2
3
4
5
6
7
8
// 定义c,存储a的值
int c = a;
std::cout << "c的值:" << c << std::endl;
// 修改c的值
c = 400;
std::cout << "修改c的值后,c为:" << c << std::endl;
std::cout << "修改c的值后,a和b的值分别为:\n" << a << std::endl;
std::cout << b << std::endl;

程序输出如下

1
2
3
4
修改c的值后,c为:400
修改c的值后,a和b的值分别为:
300
300

可以看到c为a的副本,修改c不影响到a和b。

特性

  1. 必须初始化:引用在创建时必须被初始化,它必须指向某个已存在的对象。
  2. 一旦绑定,不可改变:引用一旦被初始化后,它将一直保持与其初始对象的绑定,不能改变为另一个对象的引用。
  3. 没有空引用:引用必须指向某个对象,不能存在空引用。

看下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>

int main() {
int a = 100;
int &b = a; // b是a的引用

std::cout << "a = " << a << ", b = " << b << std::endl; // 输出: a = 100, b = 100

b = 200; // 更改b的值也会更改a的值
std::cout << "a = " << a << ", b = " << b << std::endl; // 输出: a = 200, b = 200


// int c = 300;
// 表示修改b的值为c的值
// b = c;

return 0;
}

注意事项

  • 引用主要用于函数参数和返回值,以及类的成员变量等场景,以提供对原始数据的直接访问,从而提高程序的效率和可读性。
  • 引用可以是const的,这表示你不能通过引用来修改它所指向的对象的值。
  • 引用在内部实现上通常是通过指针来实现的,但它们在语法和用途上与指针有显著的不同。引用提供了更直观、更安全的访问方式。

左值引用和右值引用

在C++中,左值(lvalue)和右值(rvalue)是表达式的两种基本分类,它们决定了表达式的结果在内存中的位置和状态。左值通常指的是具有持久状态的对象,它们有明确的内存地址,可以被多次赋值。而右值通常是临时的、没有持久状态的值,它们通常没有内存地址,或者其内存地址在表达式结束后就变得无效。

C++11引入了右值引用(rvalue reference),用T&&表示,作为对左值引用(lvalue reference,用T&表示)的补充。这一特性极大地增强了C++的表达能力,特别是在资源管理和性能方面。

左值引用

左值引用是C++98就有的特性,它允许我们为已存在的对象创建一个别名。左值引用必须被初始化为一个左值,即一个具有持久状态的对象。

1
2
int a = 10;
int& b = a; // b是a的左值引用

右值引用

右值引用是C++11新增的特性,它允许我们为右值(即临时对象或即将被销毁的对象)创建一个引用。这样,我们就可以对右值进行更复杂的操作,比如移动语义(move semantics)。

1
2
3
4
5
6
7
int&& c = 20; // c是整数字面量20的右值引用(但这种情况不常见,通常用于函数参数或返回值)

std::string foo() {
return std::string("Hello, World!"); // 返回的临时字符串是一个右值
}

std::string &&d = foo(); // d是foo()返回的临时字符串的右值引用

但请注意,直接绑定一个右值到右值引用(如int&& c = 20;)并不是右值引用的主要用途。右值引用的主要用途是作为函数参数(实现移动语义)和返回值(允许链式调用等)。

移动语义和完美转发

右值引用的引入主要是为了支持移动语义(move semantics),它允许我们在对象被销毁前“窃取”其资源(如动态分配的内存、文件句柄等),而不是进行深拷贝。这可以显著提高性能,特别是在处理大型对象或容器时。

完美转发(perfect forwarding)是另一个与右值引用相关的概念,它允许我们将参数原封不动地传递给另一个函数,无论是左值还是右值。这通过模板和std::forward函数实现。

总结

  • 左值引用(T&)是C++98就有的特性,用于为已存在的对象创建别名。
  • 右值引用(T&&)是C++11新增的特性,用于为右值(即临时对象)创建引用,支持移动语义和完美转发等高级特性。
  • 右值引用的主要用途不是直接绑定到字面量或简单的右值表达式上,而是在函数参数和返回值中,以实现更高效的资源管理和更灵活的代码编写方式。

零基础C++(4) extern的作用

Posted on 2024-09-14 | In 零基础C++

重定义问题如何解决

产生原因

前文我们说过头文件只能用来做文件的声明,源文件用来做文件的实现。如果我们在头文件中定义变量会怎么样呢?

假设我们在global.h中定义了两个变量

1
2
3
4
5
6
#ifndef DAY05_EXTERN_GLOBAL_H
#define DAY05_EXTERN_GLOBAL_H
#include <string>
int global_age = 10;
std::string global_name = "llfc";
#endif //DAY05_EXTERN_GLOBAL_H

然后我们创建global.cpp包含global.h

1
#include "global.h"

然后我们在main.cpp中包含global.h

1
2
3
4
5
6
7
8
#include <iostream>
#include "global.h"
int main() {
std::cout << "Hello, World!" << std::endl;
std::cout << "globbal name is" << global_name << std::endl;
std::cout << "global age is " << global_age << std::endl;
return 0;
}

我们运行上述代码, 程序编译并未产生问题,但是链接产生问题了

1
2
3
4
[1/3] Building CXX object CMakeFiles/day05_extern.dir/global.cpp.obj
[2/3] Building CXX object CMakeFiles/day05_extern.dir/main.cpp.obj
[3/3] Linking CXX executable day05_extern.exe
FAILED: day05_extern.exe

再往下看

1
2
multiple definition of `global_age'; CMakeFiles/day05_extern.dir/main.cpp.obj: /global.h:8: first defined here
multiple definition of `global_name[abi:cxx11]'; CMakeFiles/day05_extern.dir/main.cpp.obj:/global.h:9: first defined here

上述报错的意思是在链接main.cpp.obj时发现global_age重定义,第一次定义在global.h这里。

同样链接mian.cpp.obj时发现global_name重定义,第一次定义在global.h。

因为global.h中定义了global_age,根据我们之前学习的预编译知识,只要是包含global.h的文件都会展开global.h,main.cpp展开了一次,global.cpp展开了一次

解决办法

extern的作用

在C++中,extern 关键字用于声明一个变量或函数是在另一个文件或同一个文件的其他位置定义的。这主要用于处理全局变量或函数声明,确保在多个源文件中能够正确地链接到这些全局变量或函数的定义。

解决办法

可以采用extern关键字声明变量,然后将变量的定义放在global.cpp中, 这样main.cpp包含global.h就只会展开声明。声明可以重复声明,不会有问题。

在global.h中用extern声明两个变量

1
2
3
4
5
6
#ifndef DAY05_EXTERN_GLOBAL_H
#define DAY05_EXTERN_GLOBAL_H
#include <string>
extern int global_age ;
extern std::string global_name ;
#endif //DAY05_EXTERN_GLOBAL_H

在global.cpp中定义这些变量

1
2
3
4
5
#include "global.h"
#include <string>
// 定义全局变量
std::string global_name = "John Doe";
int global_age = 30;

在main.cpp中包含global.h

1
2
3
4
5
6
7
8
#include <iostream>
#include "global.h"
int main() {
std::cout << "Hello, World!" << std::endl;
std::cout << "globbal name is" << global_name << std::endl;
std::cout << "global age is " << global_age << std::endl;
return 0;
}

再次编译,运行成功

1
2
3
Hello, World!
globbal name isJohn Doe
global age is 30

总结

  • 头文件只做变量的声明,不能做变量的定义

  • 头文件声明变量可以采用extern的方式

零基础C++(3) 认识头文件和源文件

Posted on 2024-09-08 | In 零基础C++

头文件和源文件

在C++中,头文件(.h 或 .hpp 文件)和源文件(.cpp 文件)是组织代码的重要部分,它们共同工作以构建程序。这种分离有助于模块化和代码重用,同时也使得编译过程更加高效。

头文件(.h 或 .hpp)

头文件主要用于声明(declarations),包括:

  • 类(class)的声明
  • 函数(functions)的原型(prototypes)
  • 模板(templates)的声明
  • 宏定义(#define)
  • 外部变量(extern variables)的声明
  • 内联函数(inline functions)

头文件通常包含预处理指令如 #ifndef、#define 和 #endif,这些指令用于防止头文件被重复包含(也称为“头文件保护”或“包含卫士”)。

示例头文件(example.h):

1
2
3
4
5
6
7
8
9
10
#ifndef EXAMPLE_H
#define EXAMPLE_H

class MyClass {
public:
MyClass(); // 构造函数声明
void myFunction(); // 成员函数声明
};

#endif

源文件(.cpp)

源文件包含实际的代码实现,即函数体、类的成员函数的实现等。源文件通常包括必要的头文件,以便编译器知道它们正在使用的函数、类等是如何声明的。

示例源文件(example.cpp):

1
2
3
4
5
6
7
8
9
10
#include "example.h"
#include <iostream>

MyClass::MyClass() {
// 构造函数实现
}

void MyClass::myFunction() {
std::cout << "Hello from MyClass::myFunction!" << std::endl;
}

编译过程

在编译C++程序时,编译器会首先处理源文件(.cpp 文件)。对于源文件中的每个 #include 指令,编译器都会查找并包含相应的头文件(.h 或 .hpp 文件)。然后,编译器将处理源文件中的所有实现代码,并将它们与从头文件中获取的声明进行匹配。

注意事项

  • 头文件应该只包含声明,源文件应该包含实现。
  • 使用头文件保护来避免头文件被重复包含。
  • 在大型项目中,合理组织头文件和源文件可以提高项目的可维护性和可扩展性。
  • 在编译时,确保所有的源文件都被编译,并且所有的头文件都被正确包含。

通过这种方式,C++程序的结构变得更加清晰和模块化,有利于多人协作和代码重用。

#pragma once 和 宏定义(如 #ifndef, #define, #endif)都是用来防止头文件被重复包含的机制,但它们在工作方式和使用场景上存在一些区别。

pragma once

pragma once作用

  • 工作方式:#pragma once 是一个非标准的但广泛支持的预处理指令,它告诉编译器该头文件在单个编译过程中只应被包含一次。编译器在第一次遇到 #pragma once 时会记住该文件名,并在后续的包含操作中忽略它。
  • 优点:简单、直观、易于使用。不需要生成唯一的宏名,减少了出错的可能性。
  • 缺点:不是 C++ 标准的一部分,尽管大多数现代编译器都支持它,但在某些旧的或特定的编译器中可能不受支持。
  • 使用场景:在支持 #pragma once 的编译器中,推荐使用它作为防止头文件重复包含的首选方法。

宏定义(#ifndef, #define, #endif)

  • 工作方式:通过宏定义(通常称为“包含卫士”或“头文件保护”)来防止头文件被重复包含。首先检查一个特定的宏是否已定义,如果没有定义,则定义它并包含头文件的其余部分。如果宏已经定义,则跳过头文件的其余部分。
  • 优点:是 C++ 标准的一部分,因此在所有 C++ 编译器中都是可用的。
  • 缺点:需要为每个头文件生成一个唯一的宏名,这可能会增加出错的机会(例如,如果两个头文件不小心使用了相同的宏名)。
  • 使用场景:在需要确保代码与所有 C++ 编译器兼容时,或者在不支持 #pragma once 的编译器中,使用宏定义来防止头文件重复包含。

总结

尽管 #pragma once 和 宏定义在功能上相似,但它们在实现方式和使用场景上有所不同。在大多数现代 C++ 项目中,推荐使用 #pragma once,因为它更简单、更直观,并且大多数现代编译器都支持它。然而,在需要确保与所有 C++ 编译器兼容的情况下,或者在不支持 #pragma once 的环境中,仍然需要使用宏定义来防止头文件被重复包含。

程序如何编译的

g++编译

g++是GNU(GNU’s Not Unix)项目开发的C++编译器,它是GCC(GNU Compiler Collection,GNU编译器套件)的一个重要组成部分。GCC是一个支持多种编程语言的编译器集合,而g++则专门用于编译C++代码。

  • 在使用g++编译C++程序时,可能需要安装GCC或g++编译器。在大多数Linux发行版和Unix系统中,GCC和g++通常作为标准软件包的一部分进行安装。在Windows系统中,则可能需要下载并安装MinGW或Cygwin等工具来提供GCC和g++的支持。

当使用 g++ 编译器编译 main.cpp 并希望包含相关的头文件时,你实际上不需要在编译命令中直接指定头文件。编译器会在编译过程中自动查找并包含你在 main.cpp 或其他已包含的头文件中通过 #include 指令指定的头文件。

然而,如果你的头文件位于非标准路径(即不在编译器的默认搜索路径中),你可能需要使用 -I 选项来指定额外的头文件搜索路径。

假设你的头文件 example.h 位于与 main.cpp 相同的目录下,或者位于编译器默认搜索的头文件路径中,你可以简单地使用以下命令来编译 main.cpp:

1
g++ main.cpp -o myprogram

这里,-o myprogram 指定了输出文件的名称(在这个例子中是 myprogram)。如果你没有指定 -o 选项,编译器通常会生成一个名为 a.out 的可执行文件(在 Unix-like 系统中)。

如果你的头文件位于不同的目录,比如 include 目录,并且 main.cpp 中包含了 #include "example.h",你需要使用 -I 选项来告诉编译器在哪里查找这个头文件:

1
g++ -Iinclude main.cpp -o myprogram

在这个例子中,-Iinclude 告诉编译器在 include 目录下查找头文件。注意,-I 选项后面紧跟的是目录名,而不是文件名。

如果你的项目包含多个源文件(.cpp 文件)和/或多个头文件,并且它们位于不同的目录中,

你可能还需要使用 -L 选项来指定库文件的搜索路径(如果你链接了外部库的话),以及使用 -l 选项来指定要链接的库名(去掉前缀 lib 和后缀 .so 或 .a)。但是,对于仅包含头文件和源文件的简单项目,通常只需要上述的编译命令即可。

CMake跨平台编译

CMake是一个跨平台的自动化构建系统,它使用CMakeLists.txt文件来描述构建过程。下面是一个CMake的基本写法示例,这将指导你如何编写一个简单的CMakeLists.txt文件来构建一个可执行文件。

示例:构建单个可执行文件

假设你有一个C++源文件main.cpp,你想用CMake来构建它。首先,你需要创建一个名为CMakeLists.txt的文件,通常这个文件位于你的项目根目录下。

1
2
3
4
5
6
7
8
9
# 设置CMake最小版本要求
cmake_minimum_required(VERSION 3.10)

# 设置项目名称和版本
project(MyProject VERSION 1.0)

# 添加一个可执行文件
# 语法:add_executable(目标名 源文件...)
add_executable(MyExecutable main.cpp)

在这个例子中:

  • cmake_minimum_required(VERSION 3.10):这行设置了CMake构建系统的最小版本要求。你需要确保你的CMake版本至少是3.10或更高。
  • project(MyProject VERSION 1.0):这行设置了项目的名称(MyProject)和版本(1.0)。这个命令也会创建一个变量${PROJECT_NAME}和${MyProject_VERSION},尽管直接使用MyProject_VERSION不是强制的,因为CMake通常建议使用${PROJECT_VERSION}来引用版本。
  • add_executable(MyExecutable main.cpp):这行定义了一个可执行文件目标。它告诉CMake你想将main.cpp编译成一个名为MyExecutable的可执行文件。构建时,CMake将自动找到适合你的平台的编译器和编译选项,并将main.cpp编译成可执行文件。

构建项目

在命令行中,首先进入包含CMakeLists.txt的目录,然后运行以下命令来配置CMake项目(这将生成一个构建系统,如Makefile):

1
2
3
mkdir build  # 创建一个名为build的目录(不是必须的,但推荐)
cd build
cmake .. # 使用上级目录中的CMakeLists.txt配置项目

配置完成后,你可以使用生成的构建系统来构建项目。如果你使用的是Makefile(大多数Unix-like系统),则可以运行:

1
make

这将编译你的项目,并生成可执行文件(在这个例子中是MyExecutable)。如果你是在Windows上,并且CMake配置的是生成Visual Studio项目文件,那么你需要使用Visual Studio来打开生成的项目文件并构建项目。

注意

  • CMake是一个非常强大的构建系统,支持多种编程语言、复杂的目标关系、库依赖、条件编译等高级功能。上面的例子仅展示了最基础的用法。
  • 总是建议使用一个单独的构建目录(如上面的build目录),这样就不会污染你的源代码目录。
  • CMake提供了大量的命令和变量,可以用来精确控制构建过程。建议查阅CMake的官方文档以了解更多信息。

项目难点和面试技巧

Posted on 2024-09-07 | In C++聊天项目

简介

项目第一季完结了,本文做一个整理,介绍面试将被问到的相关问题和技术难点,以及第二季将涉及的开发内容。

下面先介绍项目如何描述。

项目如何描述

按照HR搜索简历的方式,基本都是采用关键字搜索,所以要在简历中暴露项目中的技术亮点。

为了让面试官通过简历快速的了解项目和采用的技术,需在项目介绍时融入技术细节,让项目描述更饱满一点。

可增加个人业绩或者个人成长,让面试官了解到项目的意义等。

所以综上所述,简单做个总结,一个项目的描述需包含如下几点:

  • 项目描述
  • 项目亮点
  • 项目价值

项目描述

前端基于QT实现气泡聊天对话框,通过QListWidget实现好友列表,利用GridLayout和QPainter封装气泡聊天框,基于QT network模块封装http和tcp服务。支持添加好友,好友通信,聊天记录展示等功能,仿微信布局并使用qss优化界面

后端采用分布式设计,分为GateServer网关服务,多个ChatServer聊天服务,StatusServer状态服务以及VerifyServer验证服务。

各服务通过grpc通信,支持断线重连。GateServer网关对外采用http服务,负责处理用户登录和注册功能。登录时GateServer从StatusServer查询聊天服务达到负载均衡,ChatServer聊天服务采用asio实现tcp可靠长链接异步通信和转发, 采用多线程模式封装iocontext池提升并发性能。数据存储采用mysql服务,并基于mysqlconnector库封装连接池,同时封装redis连接池处理缓存数据,以及grpc连接池保证多服务并发访问。

经测试单服务器支持8000连接,多服务器分布部署可支持1W~2W活跃用户。

技术点

asio 网络库,grpc,Node.js,多线程,Redis, MySql,Qt 信号槽,网络编程,设计模式

项目意义

关于项目意义可结合自身讨论,比如项目解决了高并发场景下单个服务连接数吃紧的情况,提升了自己对并发和异步的认知和处理能力等。

考察点

1 如何利用asio实现的tcp服务

利用asio 的多线程模式,根据cpu核数封装连接池,每个连接池跑在独立线程,采用异步async_read和assync_write方式读写,通过消息回调完成数据收发。每个连接通过session类管理,底层绑定用户id和session关联,回调函数可根据session反向查找用户进行消息推送。通过tlv方式封装消息包防止粘包。通过心跳机制检测连接可用性。

2 如何保证服务高可用

  1. 故障检测与自动恢复:
    • 实施监控系统,实时检测服务的健康状况。
    • 配置自动重启或故障转移机制,确保在故障发生时能够迅速恢复服务。
  2. 分布式架构:
    • 采用微服务架构,将应用拆分为多个独立的服务,降低单个服务故障对整体系统的影响。
  3. 数据备份与恢复:
    • 定期备份数据,并进行恢复演练,确保在数据丢失或损坏时能够快速恢复。
  4. 多活部署:
    • 在不同地理位置部署多个活跃的数据中心,确保在某个数据中心发生故障时,其他数据中心可以继续提供服务。

3 为何封装Mysql连接池

​ 首先多个线程使用同一个mysql连接是不安全的,所以要为每个线程分配独立连接,而连接数不能随着线程数无线增加,所以考虑连接池,每个线程想要操作mysql的时候从连接池取出连接进行数据访问。Mysql连接池封装包括Mgr管理层和Dao数据访问层,Mgr管理层是单例模式,Dao层包含了一个连接池,采用生产者消费者模式管理可用连接,并且通过心跳定时访问mysql保活连接。

4 如何测试性能

​ 测试性能分为三个方面:

  • 压力测试,测试服务器连接上限

  • 测试一定连接数下,收发效率稳定性

  • 采用pingpong协议,收发效率稳定在10ms下,连接数上限

压力测试,看服务器性能,客户端初始多个线程定时间隔连接,单服务节点连接上限两w以上稳定连接,并未出现掉线情况

测试稳定性,单服务节点连接数1W情况下,收发稳定未出现丢包和短线,并且延迟稳定在10ms

保证10ms延迟情况下,增加连接数,测下连接数上限,这个看机器性能,8000~2W连接不等。

5 用到哪些设计模式和思想

  • 生产者消费者模式(涉及线程池)
  • 单例模式(网络管理和数据库管理类)
  • RAII思想(defer 回收连接)
  • 代理模式(数据库,redis等通过代理对接应用层调用,底层线程池隐藏技术细节)
  • MVC控制思想,客户端通过MVC三层结构设计
  • 线程分离,网络线程,数据处理线程,以及UI渲染线程分离
  • 心跳服务
  • 数据序列化压缩发送(Protobuf)
  • 队列解耦合,服务器采用发送队列保证异步顺序,通过接受队列缓存收到数据,通过逻辑队列处理数据。
  • 分布式设计,多服务通过grpc通信,支持断线重连
  • C++11 现代化技术,智能指针,模板类型推导,线程池等

6 描述线程池封装

​ 描述线程池封装,线程池采用C++ 11 风格编写,整体来说线程池通过单例封装,内部初始化N个线程,采用生产者消费者方式管理线程,包含任务队列,任务队列采用package_task打包存储,提供对外接口commit提交任务,采用bind语法实现任务提交在commit内部自行绑定,通过智能指针伪闭包方式保证任务生命周期。同时使用C++ 11 future特性,允许外部等待任务执行完成。

第二季待完成内容

待开发内容

  • 未实现资源服务器及断点续传

  • 客户端和聊天服务的心跳机制

  • 实现断线重连和踢人操作(未完全实现,目前仅支持客户端重新登录,服务器重新绑定连接,原连接未踢掉)

  • 未完整实现用户离线后数据清空操作

  • 客户端未实现信息编辑,头像上传等UI和逻辑

  • 未实现文本,图片,语音等信息传输

  • 未实现语音,视频实时通信

零基础C++(2) 作用域和存储空间

Posted on 2024-08-31 | In 零基础C++

变量计算

整型, 浮点,双精度等变量支持计算,所谓计算就是我们熟悉的 + ,-,*,/,%等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
void calculate(){
//整形变量支持计算,所谓计算就是我们熟悉的 `+` ,`-`,`*`,`/`,`%`等
int a = 10;
int b = 20;
std::cout << "a + b = " << a+b << std::endl;
std::cout << "a - b = " << a-b << std::endl;
std::cout << "a * b = " << a*b << std::endl;
std::cout << "a / b = " << a/b << std::endl;
std::cout << "a % b = " << a%b << std::endl;
//浮点型变量支持计算,所谓计算就是我们熟悉的 `+` ,`-`,`*`,`/`等
float c = 10.5;
float d = 20.3;
std::cout << "c + d = " << c+d << std::endl;
std::cout << "c - d = " << c-d << std::endl;
std::cout << "c * d = " << c*d << std::endl;
std::cout << "c / d = " << c/d << std::endl;
//浮点型变量支持计算,所谓计算就是我们熟悉的 `+` ,`-`,`*`,`/`等
double e = 10.5;
double f = 20.3;
std::cout << "e + f = " << e+f << std::endl;
std::cout << "e - f = " << e-f << std::endl;
std::cout << "e * f = " << e*f << std::endl;
std::cout << "e / f = " << e/f << std::endl;
}

ASCII码表

计算机中字符是用ASCII码记录的,ASCII码为128字符(0-127)分配了唯一的数字编码,包括英文字母(大小写)、数字、标点符号和一些控制字符(如换行、回车等)。

https://cdn.llfc.club/ascii-1-1.png

比如字符‘A’ 对应十进制的65,字符‘a'对应十进制的97.

所以字符也可以计算

1
2
3
4
5
6
7
//字符变量支持计算,所谓计算就是我们熟悉的  `+` ,`-`,`*`,`/`等
char g = 'a';
char h = 'b';
std::cout << "g + h = " << (int)(g+h) << std::endl;
std::cout << "g - h = " << (int)(g-h) << std::endl;
std::cout << "g * h = " << (int)(g*h) << std::endl;
std::cout << "g / h = " << (int)(g/h) << std::endl;

类型划分

各种数据类型可以支持转换,double, float,int, char 这种C++给我们提供的基本类型也叫做内置类型。

我们以后学习了结构体struct和class等自定义类型后,这些类型叫做复合类型,引用和指针也属于复合类型。

变量大小

前文提过变量是存储在存储单元中,那么计算机为不同的变量分配的大小也不一样, 可以通过sizeof计算类型的大小

1
2
3
4
5
6
7
void sizeofnum(){
std::cout << "Size of char: " << sizeof(char) << " bytes\n";
std::cout << "Size of int: " << sizeof(int) << " bytes\n";
std::cout << "Size of float: " << sizeof(float) << " bytes\n";
std::cout << "Size of double: " << sizeof(double) << " bytes\n";
std::cout << "Size of long long: " << sizeof(long long) << " bytes\n";
}
1
2
3
4
5
Size of char: 1 bytes
Size of int: 4 bytes
Size of float: 4 bytes
Size of double: 8 bytes
Size of long long: 8 bytes

类型转换

这里仅看下内置类型转换,C++会自动推导转换, 复合类型之后再介绍。

1B = 8bit 最大能表示127

1
2
3
4
5
6
7
8
9
char a = 100; // 100 是int类型,然后赋值给 char, int是四字节,char 是1字节,如果数字过大会损失精度。
int b = a; // char 转换为 int类型,不会损失数据
double c = b; // int 转换为 double(算数转换)

std::cout << "a = " << static_cast<int>(a) << ", b = " << b << ", c = " << c << std::endl;

// 注意:下面的转换可能会导致数据丢失
unsigned int d = -1; // int 转换为 unsigned int,导致数据丢失
std::cout << "d = " << d << std::endl; // 输出一个很大的数

变量作用域

在C++中,变量作用域(Scope)指的是程序中变量可以被访问的代码区域。作用域决定了变量的生命周期和可见性。

我可以解释几种常见的变量作用域类型:

  1. 全局作用域:在函数外部声明的变量具有全局作用域。它们可以在程序的任何地方被访问,但通常建议在需要时才使用全局变量,因为它们可能导致代码难以理解和维护。
  2. 局部作用域:在函数内部、代码块(如if语句、for循环等)内部声明的变量具有局部作用域。它们只能在声明它们的代码块内被访问。一旦离开该代码块,这些变量就不再可见。
  3. 命名空间作用域:在命名空间中声明的变量(实际上是实体,如变量、函数等)具有命名空间作用域。它们只能在相应的命名空间内被直接访问,但可以通过使用命名空间的名称作为前缀来从外部访问。
  4. 类作用域:在类内部声明的成员变量和成员函数具有类作用域。成员变量和成员函数可以通过类的对象来访问,或者在某些情况下(如静态成员)可以通过类名直接访问。
  5. 块作用域:这是局部作用域的一个特例,指的是由大括号{}包围的代码块内部声明的变量。这些变量只能在该代码块内被访问。

全局作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

// 全局变量,具有全局作用域
int globalVar = 42;

void func() {
std::cout << "Inside func: globalVar = " << globalVar << std::endl;
}

int main() {
std::cout << "Inside main: globalVar = " << globalVar << std::endl;
func(); // 访问全局变量
return 0;
}

局部作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>

void func() {
// 局部变量,具有局部作用域
int localVar = 10;
std::cout << "Inside func: localVar = " << localVar << std::endl;
// localVar 在这里之后就不再可见
}

int main() {
// 尝试访问 localVar 会导致编译错误
// std::cout << "localVar = " << localVar << std::endl; // 错误
func(); // 局部变量仅在func函数内部可见
return 0;
}

命名空间作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

// 定义一个命名空间
namespace MyNamespace {
// 命名空间内的变量,具有命名空间作用域
int namespaceVar = 20;

void printVar() {
std::cout << "Inside MyNamespace: namespaceVar = " << namespaceVar << std::endl;
}

int globalVar = 0;
}

namespace MyNamespace2 {
int globalVar = 0;
}

int main() {
// 使用命名空间前缀访问变量
std::cout << "Outside MyNamespace: namespaceVar = " << MyNamespace::namespaceVar << std::endl;
MyNamespace::printVar(); // 访问命名空间内的函数
return 0;
}

类作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>

class MyClass {
public:
// 成员变量,具有类作用域
int classVar;

// 成员函数,也可以访问类作用域内的成员变量
void printVar() {
std::cout << "Inside MyClass: classVar = " << classVar << std::endl;
}
};

int main() {
MyClass obj;
obj.classVar = 30; // 通过对象访问成员变量
obj.printVar(); // 访问成员函数
// 尝试直接访问 classVar 会导致编译错误
// std::cout << "classVar = " << classVar << std::endl; // 错误
return 0;
}

块作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

void func() {
{
// 块内局部变量,具有块作用域
int blockVar = 5;
std::cout << "Inside block: blockVar = " << blockVar << std::endl;
// blockVar 在这个代码块之后就不再可见
}
// 尝试访问 blockVar 会导致编译错误
// std::cout << "blockVar = " << blockVar << std::endl; // 错误
}

int main() {
func(); // 访问块作用域变量仅在func函数内部的代码块内有效
return 0;
}

存储区域

在C++中,内存存储通常可以大致分为几个区域,这些区域根据存储的数据类型、生命周期和作用域来划分。这些区域主要包括:

  1. 代码区(Code Segment/Text Segment):
    • 存储程序执行代码(即机器指令)的内存区域。这部分内存是共享的,只读的,且在程序执行期间不会改变。
    • 举例说明:当你编译一个C++程序时,所有的函数定义、控制结构等都会被转换成机器指令,并存储在代码区。
  2. 全局/静态存储区(Global/Static Storage Area):
    • 存储全局变量和静态变量的内存区域。这些变量在程序的整个运行期间都存在,但它们的可见性和生命周期取决于声明它们的作用域。
    • 举例说明:全局变量(在函数外部声明的变量)和静态变量(使用static关键字声明的变量,无论是在函数内部还是外部)都会存储在这个区域。
  3. 栈区(Stack Segment):
    • 存储局部变量、函数参数、返回地址等的内存区域。栈是一种后进先出(LIFO)的数据结构,用于存储函数调用和自动变量。
    • 举例说明:在函数内部声明的变量(不包括静态变量)通常存储在栈上。当函数被调用时,其参数和局部变量会被推入栈中;当函数返回时,这些变量会从栈中弹出,其占用的内存也随之释放。
  4. 堆区(Heap Segment):
    • 由程序员通过动态内存分配函数(如new和malloc)分配的内存区域。堆区的内存分配和释放是手动的,因此程序员需要负责管理内存,以避免内存泄漏或野指针等问题。
    • 举例说明:当你使用new操作符在C++中动态分配一个对象或数组时,分配的内存就来自堆区。同样,使用delete操作符可以释放堆区中的内存。
  5. 常量区(Constant Area):
    • 存储常量(如字符串常量、const修饰的全局变量等)的内存区域。这部分内存也是只读的,且通常在程序执行期间不会改变。
    • 举例说明:在C++中,使用双引号括起来的字符串字面量通常存储在常量区。此外,使用const关键字声明的全局变量,如果其值在编译时就已确定,也可能存储在常量区。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <cstring> // 用于strlen

// 全局变量,存储在全局/静态存储区
int globalVar = 10;

// 静态变量,也存储在全局/静态存储区,但仅在其声明的文件或函数内部可见
static int staticVar = 20;

void func() {
// 局部变量,存储在栈区
int localVar = 30;

// 静态局部变量,虽然声明在函数内部,但存储在全局/静态存储区,且只在第一次调用时初始化
static int staticLocalVar = 40;

std::cout << "Inside func:" << std::endl;
std::cout << "localVar = " << localVar << std::endl;
std::cout << "staticLocalVar = " << staticLocalVar << std::endl;

// 尝试通过动态内存分配在堆区分配内存
int* heapVar = new int(50);

std::cout << "heapVar = " << *heapVar << std::endl;

// 释放堆区内存(重要:实际使用中不要忘记释放不再使用的堆内存)
delete heapVar;
}

int main() {
// 访问全局变量
std::cout << "Inside main:" << std::endl;
std::cout << "globalVar = " << globalVar << std::endl;
std::cout << "staticVar = " << staticVar << std::endl; // 注意:staticVar在外部不可见(除非在同一个文件中或通过特殊方式)

// 调用函数,展示栈区和堆区的使用
func();

// 字符串常量通常存储在常量区,但直接访问其内存地址并不是标准C++的做法
// 这里我们仅通过指针来展示其存在
const char* strConst = "Hello, World!";
// 注意:不要尝试修改strConst指向的内容,因为它是只读的
std::cout << "strConst = " << strConst << std::endl;
// 尝试获取字符串常量的长度(这不会修改常量区的内容)
std::cout << "Length of strConst = " << strlen(strConst) << std::endl;

return 0;
}

在这个示例中,我使用了全局变量、静态变量、局部变量、静态局部变量以及通过new操作符在堆上分配的内存来展示不同内存区域的使用。同时,我也提到了字符串常量,但请注意,直接访问其内存地址并不是C++编程中的标准做法,因为字符串常量通常是只读的,并且其存储位置取决于编译器和操作系统的实现。

另外,请注意,我在func函数中分配了堆内存并通过delete操作符释放了它。这是管理堆内存时的一个重要实践,以避免内存泄漏。然而,在实际应用中,更复杂的内存管理策略(如智能指针)可能更为合适。

当您编译这个程序时,编译器会将main函数和func函数的代码转换成机器指令,并将这些指令存储在可执行文件的代码区中(尽管实际上是在磁盘上的可执行文件中,但在程序运行时,操作系统会将这些指令加载到内存的代码区中)。然后,当您运行这个程序时,CPU会从内存的代码区中读取这些指令并执行它们。

程序编译过程

C++程序的编译过程是一个相对复杂但有序的过程,它涉及将高级语言(C++)代码转换为机器可以执行的低级指令。在这个过程中,通常会生成几个中间文件,包括.i(预处理文件)、.s(汇编文件)和.o(目标文件或对象文件)。下面是这个过程的详细解释:

1. 预处理(Preprocessing)

  • 输入:C++源代码文件(通常以.cpp或.cxx为后缀)。
  • 处理:预处理器(通常是cpp)读取源代码文件,并对其进行宏展开、条件编译、文件包含(#include)等处理。
  • 输出:生成预处理后的文件,通常具有.i后缀(尽管这个步骤可能不是所有编译器都会自动生成.i文件,或者可能需要特定的编译器选项来生成)。

2. 编译(Compilation)

  • 输入:预处理后的文件(如果有的话,否则直接是源代码文件)。
  • 处理:编译器(如g++、clang++等)将预处理后的文件或源代码文件转换为汇编语言代码。这个步骤是编译过程的核心,它执行词法分析、语法分析、语义分析、中间代码生成、代码优化等任务。
  • 输出:生成汇编文件,通常具有.s或.asm后缀。

3. 汇编(Assembly)

  • 输入:汇编文件。
  • 处理:汇编器(如as、gas等)将汇编语言代码转换为机器语言指令(即目标代码),但这些指令仍然是针对特定架构的,并且尚未被链接成可执行文件。
  • 输出:生成目标文件(或对象文件),通常具有.o、.obj或.out后缀。

4. 链接(Linking)

  • 输入:一个或多个目标文件,以及可能需要的库文件(如C++标准库)。
  • 处理:链接器(如ld、lld等)将目标文件和库文件合并成一个可执行文件或库文件。在这个过程中,链接器会解决外部符号引用(即函数和变量的调用),并将它们链接到正确的地址。
  • 输出:生成可执行文件(在Unix-like系统中通常是.out、.exe或没有特定后缀,在Windows系统中是.exe)。

我们将代码的CMakeList中设置编译选项,保存临时文件

1
2
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -save-temps=obj")
set(CMAKE_C_FLAGS "${CMAKE_CXX_FLAGS} -save-temps=obj")

点击Clion中的build,可以看到目录中生成了临时文件,包括

.i 文件表示预处理文件

.s(汇编文件)

.o(目标文件或对象文件)

https://cdn.llfc.club/1725082193757.jpg

总结

  • .i文件是预处理后的文件,包含了所有宏展开、条件编译和文件包含的结果。
  • .s文件是汇编文件,包含了将C++代码转换为汇编语言后的结果。
  • .o文件是目标文件或对象文件,包含了汇编器生成的机器语言指令,但尚未被链接成可执行文件。

这些文件在编译过程中扮演了重要的角色,帮助开发者理解和调试代码,同时也是编译链中不可或缺的一部分。不过,值得注意的是,并非所有编译器都会默认生成.i和.s文件,这可能需要特定的编译器选项来启用。

赞赏

感谢支持

https://cdn.llfc.club/dashang.jpg

零基础C++(1) 变量和输出

Posted on 2024-08-31 | In 零基础C++

变量

变量定义

在C++中,变量是用来存储数据值的一种实体。每个变量都有一个类型,这个类型决定了变量可以存储的数据的种类以及变量在内存中所占的空间大小。

通俗例子

我们家中会有收纳柜,收纳柜存储的是我们的物品。物品就相当于数据,收纳柜就相当于变量,不同的柜子有不同的类型,有的装内衣,有的装袜子。

https://cdn.llfc.club/%E4%BC%81%E4%B8%9A%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_17245418515191.png

装内衣的柜子一般不会装螺丝刀,一个柜子在定义好用来做什么,它存储的内容就确定了。

补充知识

计算机中数据是按照二进制存储的,一个字节占8bit, bit就是位的意思,比如数字2会转化为二进制00000010, 然后将这个00000010放到计算机为我们分配好的存储单元里,这个存储单元本身还有一个地址,假设存储单元的地址为5,转化为二进制就是00000101.

https://cdn.llfc.club/%E4%BC%81%E4%B8%9A%E5%BE%AE%E4%BF%A1%E6%88%AA%E5%9B%BE_17245430325039.png

当我们想要取出数据00000010时,需要先访问地址00000101找到存储单元,然后取出存储单元存储的数据。

再次理解以下,存储单元可以理解为一个变量,存储了数据00000010,变量的地址为00000101。理解这个,之后我们会介绍指针的概念。

变量的声明与初始化

在C++中,你首先需要声明一个变量,然后(可选地)可以初始化它。声明变量时,你需要指定变量的类型和名称。

1
2
3
4
5
6
7
8
// 声明一个整型变量age,未初始化
int age;
// 声明并初始化一个整型变量height
int height = 175;
// 声明并初始化一个双精度浮点型变量weight
double weight = 65.5;
// 声明并初始化一个字符型变量gender
char gender = 'M';

变量命名规则

在C++中,变量名可以包含字母、数字和下划线(_),但不能以数字开头。此外,C++是大小写敏感的,因此age和Age被视为两个不同的变量。

变量类型

C++支持多种基本数据类型,包括整型(int、short、long、long long)、浮点型(float、double、long double)、字符型(char)、布尔型(bool)等。此外,C++还支持枚举(enum)、结构体(struct)、联合体(union)和类(class)等复合数据类型。

示例:使用变量

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

int main() {
//初始化变量a和b
int a = 5, b = 10;
//a+b的值赋值给sum
int sum = a + b;
//输出求和的结果
std::cout << "The sum of " << a << " and " << b << " is " << sum << std::endl;
return 0;
}

在这个示例中,我们声明了三个整型变量a、b和sum,并将a和b的值分别初始化为5和10。然后,我们计算a和b的和,并将结果存储在变量sum中。最后,我们使用std::cout输出计算结果。

1
The sum of 5 and 10 is 15

输出运算符

在C++中,输出通常是通过标准输出流(std::cout)来完成的。std::cout 是C++标准库中的一个对象,它代表标准输出设备,通常是屏幕。为了使用 std::cout 来输出数据,你需要包含头文件 <iostream>。

下面是一个简单的C++程序示例,展示了如何使用 std::cout 来输出文本和数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream> // 包含标准输入输出流库

int main() {
// 输出字符串
std::cout << "Hello, World!" << std::endl;

// 输出数字
int number = 42;
std::cout << "The number is: " << number << std::endl;

// 输出浮点数
double pi = 3.14159;
std::cout << "The value of pi is approximately: " << pi << std::endl;

// 输出字符
char ch = 'A';
std::cout << "The character is: " << ch << std::endl;

// 输出布尔值(C++11及以后)
bool truth = true;
std::cout << std::boolalpha; // 启用布尔值的true/false输出
std::cout << "The truth is: " << truth << std::endl;

return 0;
}

在这个例子中,<< 是插入运算符,用于将右侧的数据发送到左侧的流对象(在这个例子中是 std::cout)。std::endl 是一个操纵符,用于在输出后插入换行符,并刷新输出缓冲区,确保立即在屏幕上显示输出。

1
2
3
4
5
Hello, World!
The number is: 42
The value of pi is approximately: 3.14159
The character is: A
The truth is: true

理解即可

注意,为了输出布尔值 true 和 false 而不是整数 1 和 0,我们使用了 std::boolalpha 操纵符。这是C++11及以后版本中引入的,用于控制布尔值的输出格式。

此外,C++还支持格式化输出,但这通常涉及更复杂的语法,如使用 std::iomanip 头文件中定义的操纵符(如 std::setw 和 std::setprecision或使用C++20引入的新特性(如格式化库 std::format,尽管这不是通过 std::cout 直接使用的,而是用于生成格式化字符串)。

练习

定义初始化一个变量pai_val数值为 3.14, 初始化一个int类型的变量int_val变量值为5,分别输出这两个变量的值。

初始化一个bool变量数值为-100,输出bool变量的值,看看是true还是false。

赞赏

感谢支持

https://cdn.llfc.club/dashang.jpg

C++ 全栈聊天项目(1)架构概述和登录界面

Posted on 2024-08-31 | In C++聊天项目

简介

本项目为C++全栈聊天项目实战,包括PC端QT界面编程,asio异步服务器设计,beast网络库搭建http网关,nodejs搭建验证服务,各服务间用grpc通信,server和client用asio通信等,也包括用户信息的录入等。实现跨平台设计,先设计windows的server,之后再考虑移植到windows中。较为全面的展示C++ 在实际项目中的应用,可作为项目实践学习案例,也可写在简历中。

架构设计

一个概要的结构设计如下图
https://cdn.llfc.club/1709009717000.jpg

  1. GateServer为网关服务,主要应对客户端的连接和注册请求,因为服务器是是分布式,所以GateServer收到用户连接请求后会查询状态服务选择一个负载较小的Server地址给客户端,客户端拿着这个地址直接和Server通信建立长连接。
  2. 当用户注册时会发送给GateServer, GateServer调用VarifyServer验证注册的合理性并发送验证码给客户端,客户端拿着这个验证码去GateServer注册即可。
  3. StatusServer, ServerA, ServerB都可以直接访问Redis和Mysql服务。

创建应用

我们先创建客户端的登录界面,先用qt创建qt application widgets

https://cdn.llfc.club/1709012628800.jpg

项目名称叫做llfcchat,位置大家自己选择。

接下来一路同意,最后生成项目。

https://cdn.llfc.club/1709014472288.jpg

为了增加项目可读性,我们增加注释模板

选择“工具”->“选项”,再次选择“文本编辑器”->“片段”->“添加”,按照下面的模板编排

1
2
3
4
5
6
7
8
9
/******************************************************************************
*
* @file %{CurrentDocument:FileName}
* @brief XXXX Function
*
* @author 恋恋风辰
* @date %{CurrentDate:yyyy\/MM\/dd}
* @history
*****************************************************************************/

如下图
https://cdn.llfc.club/1709014829278.jpg

以后输入header custom就可以弹出注释模板了.

修改mainwindow.ui属性,长宽改为300*500
https://cdn.llfc.club/1709017541569.jpg

将window title 改为llfcchat

大家自己找个icon.ico文件放到项目根目录,或者用我的也行,然后在项目pro里添加输出目录文件和icon图标

1
2
RC_ICONS = icon.ico
DESTDIR = ./bin

将图片资源添加ice.png添加到文件夹res里,然后右键项目选择添加新文件,选择qt resource files, 添加qt的资源文件,名字设置为rc。

添加成功后邮件rc.qrc选择添加现有资源文件,

选择res文件夹下的ice.png,这样ice.png就导入项目工程了。

创建登录界面

右键项目,选择创建,点击设计师界面类

https://cdn.llfc.club/1709018322347.jpg

选择 dialog without buttons

https://cdn.llfc.club/1709018398183.jpg

创建的名字就叫做LoginDialog

https://cdn.llfc.club/1709018587338.jpg

将LoginDialog.ui修改为如下布局

https://cdn.llfc.club/1709019305029.jpg

在mainwindow.h中添加LoginDialog指针成员,然后在构造函数将LoginDialog设置为中心部件

1
2
3
4
5
6
7
8
9
10
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
//创建一个CentralWidget, 并将其设置为MainWindow的中心部件
_login_dlg = new LoginDialog();
setCentralWidget(_login_dlg);
_login_dlg->show();
}

创建注册界面

注册界面创建方式和登录界面类似,我们创建的界面如下:

https://cdn.llfc.club/1709030381543.jpg

创建好界面后接下来在LoginDialog类声明里添加信号切换注册界面

1
2
signals:
void switchRegister();

在LoginDialog的构造函数里连接按钮点击事件

1
connect(ui->reg_btn, &QPushButton::clicked, this, &LoginDialog::switchRegister);

按钮点击后LoginDialog发出switchRegister信号,该信号发送给MainWindow用来切换界面。

我们在MainWindow里声明注册类变量

1
2
private:
RegisterDialog* _reg_dlg;

在其构造函数中添加注册类对象的初始化以及连接switchRegister信号

1
2
3
4
5
//创建和注册消息的链接
connect(_login_dlg, &LoginDialog::switchRegister,
this, &MainWindow::SlotSwitchReg);

_reg_dlg = new RegisterDialog();

接下来实现槽函数SlotSwitchReg

1
2
3
4
5
void MainWindow::SlotSwitchReg(){
setCentralWidget(_reg_dlg);
_login_dlg->hide();
_reg_dlg->show();
}

这样启动程序主界面优先显示登录界面,点击注册后跳转到注册界面

优化样式

我们在项目根目录下创建style文件夹,在文件夹里创建stylesheet.qss文件,然后在qt项目中的rc.qrc右键添加现有文件,选择stylesheet.qss,这样qss就被导入到项目中了。

在主程序启动后加载qss

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int main(int argc, char *argv[])
{
QApplication a(argc, argv);

QFile qss(":/style/stylesheet.qss");

if( qss.open(QFile::ReadOnly))
{
qDebug("open success");
QString style = QLatin1String(qss.readAll());
a.setStyleSheet(style);
qss.close();
}else{
qDebug("Open failed");
}

MainWindow w;
w.show();

return a.exec();
}

然后我们写qss样式美化界面

1
2
3
QDialog#LoginDialog{
background-color:rgb(255,255,255)
}

主界面有一道灰色的是toolbar造成的,去mainwindow.ui里把那个toolbar删了就行了。

C++ 全栈聊天项目(10) 使用iocontext连接池提高并发

Posted on 2024-08-31 | In C++聊天项目

VerifyServer增加redis

我们为了让验证码有一个过期时间,可以利用redis实现,在Verify文件夹用npm安装redis服务。

1
npm install ioredis

完善config.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"email": {
"user": "secondtonone1@163.com",
"pass": "CRWTAZOSNCWDDQQTllfc"
},

"mysql": {
"host": "81.68.86.146",
"port": 3308,
"passwd": "123456"
},

"redis":{
"host": "81.68.86.146",
"port": 6380,
"passwd": "123456"
}
}

服务里添加redis模块,封装redis操作在redis.js中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92

const config_module = require('./config')
const Redis = require("ioredis");

// 创建Redis客户端实例
const RedisCli = new Redis({
host: config_module.redis_host, // Redis服务器主机名
port: config_module.redis_port, // Redis服务器端口号
password: config_module.redis_passwd, // Redis密码
});


/**
* 监听错误信息
*/
RedisCli.on("error", function (err) {
console.log("RedisCli connect error");
RedisCli.quit();
});

/**
* 根据key获取value
* @param {*} key
* @returns
*/
async function GetRedis(key) {

try{
const result = await RedisCli.get(key)
if(result === null){
console.log('result:','<'+result+'>', 'This key cannot be find...')
return null
}
console.log('Result:','<'+result+'>','Get key success!...');
return result
}catch(error){
console.log('GetRedis error is', error);
return null
}

}

/**
* 根据key查询redis中是否存在key
* @param {*} key
* @returns
*/
async function QueryRedis(key) {
try{
const result = await RedisCli.exists(key)
// 判断该值是否为空 如果为空返回null
if (result === 0) {
console.log('result:<','<'+result+'>','This key is null...');
return null
}
console.log('Result:','<'+result+'>','With this value!...');
return result
}catch(error){
console.log('QueryRedis error is', error);
return null
}

}

/**
* 设置key和value,并过期时间
* @param {*} key
* @param {*} value
* @param {*} exptime
* @returns
*/
async function SetRedisExpire(key,value, exptime){
try{
// 设置键和值
await RedisCli.set(key,value)
// 设置过期时间(以秒为单位)
await RedisCli.expire(key, exptime);
return true;
}catch(error){
console.log('SetRedisExpire error is', error);
return false;
}
}

/**
* 退出函数
*/
function Quit(){
RedisCli.quit();
}

module.exports = {GetRedis, QueryRedis, Quit, SetRedisExpire,}

server.js中包含redis.js

1
const redis_module = require('./redis')

获取验证码之前可以先查询redis,如果没查到就生成uid并且写入redis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
async function GetVarifyCode(call, callback) {
console.log("email is ", call.request.email)
try{
let query_res = await redis_module.GetRedis(const_module.code_prefix+call.request.email);
console.log("query_res is ", query_res)
if(query_res == null){

}
let uniqueId = query_res;
if(query_res ==null){
uniqueId = uuidv4();
if (uniqueId.length > 4) {
uniqueId = uniqueId.substring(0, 4);
}
let bres = await redis_module.SetRedisExpire(const_module.code_prefix+call.request.email, uniqueId,600)
if(!bres){
callback(null, { email: call.request.email,
error:const_module.Errors.RedisErr
});
return;
}
}

console.log("uniqueId is ", uniqueId)
let text_str = '您的验证码为'+ uniqueId +'请三分钟内完成注册'
//发送邮件
let mailOptions = {
from: 'secondtonone1@163.com',
to: call.request.email,
subject: '验证码',
text: text_str,
};

let send_res = await emailModule.SendMail(mailOptions);
console.log("send res is ", send_res)

callback(null, { email: call.request.email,
error:const_module.Errors.Success
});


}catch(error){
console.log("catch error is ", error)

callback(null, { email: call.request.email,
error:const_module.Errors.Exception
});
}

}

验证服务联调

开启VerifyServer和GateServer,再启动客户端,点击获取验证码,客户端就会发送请求给GateServer,GateServer再调用内部服务VerifyServer。最后将请求返回客户端,完成了验证码发送的流程。

如果10分钟之内多次请求,因为验证码被缓存在redis中,所以会被复用返回给客户端。

https://cdn.llfc.club/1711079058273.jpg

看起来客户端收到服务器的回复了,我们去邮箱看看是否收到验证码

https://cdn.llfc.club/1711080431619.jpg

确实收到了验证码。好的多服务调用实现了,大家可以把这个功能理解下,接下来去实现注册逻辑。

C++ 全栈聊天项目(12) C++封装redis连接池

Posted on 2024-08-31 | In C++聊天项目

增加定时按钮

点击获取验证码后需要让按钮显示倒计时,然后倒计时结束后再次可点击。
添加TimberBtn类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#ifndef TIMERBTN_H
#define TIMERBTN_H
#include <QPushButton>
#include <QTimer>

class TimerBtn : public QPushButton
{
public:
TimerBtn(QWidget *parent = nullptr);
~ TimerBtn();

// 重写mouseReleaseEvent
virtual void mouseReleaseEvent(QMouseEvent *e) override;
private:
QTimer *_timer;
int _counter;
};

#endif // TIMERBTN_H

添加实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include "timerbtn.h"
#include <QMouseEvent>
#include <QDebug>

TimerBtn::TimerBtn(QWidget *parent):QPushButton(parent),_counter(10)
{
_timer = new QTimer(this);

connect(_timer, &QTimer::timeout, [this](){
_counter--;
if(_counter <= 0){
_timer->stop();
_counter = 10;
this->setText("获取");
this->setEnabled(true);
return;
}
this->setText(QString::number(_counter));
});
}

TimerBtn::~TimerBtn()
{
_timer->stop();
}

void TimerBtn::mouseReleaseEvent(QMouseEvent *e)
{
if (e->button() == Qt::LeftButton) {
// 在这里处理鼠标左键释放事件
qDebug() << "MyButton was released!";
this->setEnabled(false);
this->setText(QString::number(_counter));
_timer->start(1000);
emit clicked();
}
// 调用基类的mouseReleaseEvent以确保正常的事件处理(如点击效果)
QPushButton::mouseReleaseEvent(e);
}

然后将注册界面获取按钮升级为TimerBtn

调整输入框错误提示

在RegisterDialog构造函数中删除原来的输入框editing信号和逻辑,添加editingFinished信号和处理逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//day11 设定输入框输入后清空字符串
ui->err_tip->clear();

connect(ui->user_edit,&QLineEdit::editingFinished,this,[this](){
checkUserValid();
});

connect(ui->email_edit, &QLineEdit::editingFinished, this, [this](){
checkEmailValid();
});

connect(ui->pass_edit, &QLineEdit::editingFinished, this, [this](){
checkPassValid();
});

connect(ui->confirm_edit, &QLineEdit::editingFinished, this, [this](){
checkConfirmValid();
});

connect(ui->varify_edit, &QLineEdit::editingFinished, this, [this](){
checkVarifyValid();
});

global.h中添加TipErr定义

1
2
3
4
5
6
7
8
9
enum TipErr{
TIP_SUCCESS = 0,
TIP_EMAIL_ERR = 1,
TIP_PWD_ERR = 2,
TIP_CONFIRM_ERR = 3,
TIP_PWD_CONFIRM = 4,
TIP_VARIFY_ERR = 5,
TIP_USER_ERR = 6
};

RegisterDialog声明中添加

1
QMap<TipErr, QString> _tip_errs;

_tip_errs用来缓存各个输入框输入完成后提示的错误,如果该输入框错误清除后就显示剩余的错误,每次只显示一条

实现添加错误和删除错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void ResetDialog::AddTipErr(TipErr te, QString tips)
{
_tip_errs[te] = tips;
showTip(tips, false);
}

void ResetDialog::DelTipErr(TipErr te)
{
_tip_errs.remove(te);
if(_tip_errs.empty()){
ui->err_tip->clear();
return;
}

showTip(_tip_errs.first(), false);
}

实现错误检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
bool ResetDialog::checkUserValid()
{
if(ui->user_edit->text() == ""){
AddTipErr(TipErr::TIP_USER_ERR, tr("用户名不能为空"));
return false;
}

DelTipErr(TipErr::TIP_USER_ERR);
return true;
}


bool ResetDialog::checkPassValid()
{
auto pass = ui->pwd_edit->text();

if(pass.length() < 6 || pass.length()>15){
//提示长度不准确
AddTipErr(TipErr::TIP_PWD_ERR, tr("密码长度应为6~15"));
return false;
}

// 创建一个正则表达式对象,按照上述密码要求
// 这个正则表达式解释:
// ^[a-zA-Z0-9!@#$%^&*]{6,15}$ 密码长度至少6,可以是字母、数字和特定的特殊字符
QRegularExpression regExp("^[a-zA-Z0-9!@#$%^&*]{6,15}$");
bool match = regExp.match(pass).hasMatch();
if(!match){
//提示字符非法
AddTipErr(TipErr::TIP_PWD_ERR, tr("不能包含非法字符"));
return false;;
}

DelTipErr(TipErr::TIP_PWD_ERR);

return true;
}



bool ResetDialog::checkEmailValid()
{
//验证邮箱的地址正则表达式
auto email = ui->email_edit->text();
// 邮箱地址的正则表达式
QRegularExpression regex(R"((\w+)(\.|_)?(\w*)@(\w+)(\.(\w+))+)");
bool match = regex.match(email).hasMatch(); // 执行正则表达式匹配
if(!match){
//提示邮箱不正确
AddTipErr(TipErr::TIP_EMAIL_ERR, tr("邮箱地址不正确"));
return false;
}

DelTipErr(TipErr::TIP_EMAIL_ERR);
return true;
}

bool ResetDialog::checkVarifyValid()
{
auto pass = ui->varify_edit->text();
if(pass.isEmpty()){
AddTipErr(TipErr::TIP_VARIFY_ERR, tr("验证码不能为空"));
return false;
}

DelTipErr(TipErr::TIP_VARIFY_ERR);
return true;
}

除此之外修改之前点击确认按钮的逻辑,改为检测所有条件成立后再发送请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void ResetDialog::on_sure_btn_clicked()
{
bool valid = checkUserValid();
if(!valid){
return;
}

valid = checkEmailValid();
if(!valid){
return;
}

valid = checkPassValid();
if(!valid){
return;
}

valid = checkVarifyValid();
if(!valid){
return;
}

//发送http重置用户请求
QJsonObject json_obj;
json_obj["user"] = ui->user_edit->text();
json_obj["email"] = ui->email_edit->text();
json_obj["passwd"] = xorString(ui->pwd_edit->text());
json_obj["varifycode"] = ui->varify_edit->text();
HttpMgr::GetInstance()->PostHttpReq(QUrl(gate_url_prefix+"/reset_pwd"),
json_obj, ReqId::ID_RESET_PWD,Modules::RESETMOD);
}

隐藏和显示密码

我们在输入密码时希望能通过点击可见还是不可见,显示密码和隐藏密码,这里先添加图片放入资源中,然后在Register.ui中添加两个label,分别命名为pass_visible和confirm_visible, 用来占据位置。

因为我们要做的点击后图片要有状态切换,以及浮动显示不一样的效果等,所以我们重写ClickedLabel,继承自QLabel.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#ifndef CLICKEDLABEL_H
#define CLICKEDLABEL_H
#include <QLabel>
#include "global.h"

class ClickedLabel:public QLabel
{
Q_OBJECT
public:
ClickedLabel(QWidget* parent);
virtual void mousePressEvent(QMouseEvent *ev) override;
virtual void enterEvent(QEvent* event) override;
virtual void leaveEvent(QEvent* event) override;
void SetState(QString normal="", QString hover="", QString press="",
QString select="", QString select_hover="", QString select_press="");

ClickLbState GetCurState();
protected:

private:
QString _normal;
QString _normal_hover;
QString _normal_press;

QString _selected;
QString _selected_hover;
QString _selected_press;

ClickLbState _curstate;
signals:
void clicked(void);

};

#endif // CLICKEDLABEL_H

一个Label有六种状态,普通状态,普通的悬浮状态,普通的点击状态,选中状态,选中的悬浮状态,选中的点击状态。

当Label处于普通状态,被点击后,切换为选中状态,再次点击又切换为普通状态。

ClickLbState定义在global.h中,包含两种状态一个是普通状态,一个是选中状态。而Label中的六种状态就是基于这两种状态嵌套实现的。

1
2
3
4
enum ClickLbState{
Normal = 0,
Selected = 1
};

六种状态用qss写好,这样我们只需要根据鼠标事件切换不同的qss就可以实现样式变换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#pass_visible[state='unvisible']{
border-image: url(:/res/unvisible.png);
}

#pass_visible[state='unvisible_hover']{
border-image: url(:/res/unvisible_hover.png);
}

#pass_visible[state='visible']{
border-image: url(:/res/visible.png);
}

#pass_visible[state='visible_hover']{
border-image: url(:/res/visible_hover.png);
}


#confirm_visible[state='unvisible']{
border-image: url(:/res/unvisible.png);
}

#confirm_visible[state='unvisible_hover']{
border-image: url(:/res/unvisible_hover.png);
}

#confirm_visible[state='visible']{
border-image: url(:/res/visible.png);
}

#confirm_visible[state='visible_hover']{
border-image: url(:/res/visible_hover.png);
}

我们实现ClickedLabel功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include "clickedlabel.h"
#include <QMouseEvent>
ClickedLabel::ClickedLabel(QWidget* parent):QLabel (parent),_curstate(ClickLbState::Normal)
{

}


// 处理鼠标点击事件
void ClickedLabel::mousePressEvent(QMouseEvent* event) {
if (event->button() == Qt::LeftButton) {
if(_curstate == ClickLbState::Normal){
qDebug()<<"clicked , change to selected hover: "<< _selected_hover;
_curstate = ClickLbState::Selected;
setProperty("state",_selected_hover);
repolish(this);
update();

}else{
qDebug()<<"clicked , change to normal hover: "<< _normal_hover;
_curstate = ClickLbState::Normal;
setProperty("state",_normal_hover);
repolish(this);
update();
}
emit clicked();
}
// 调用基类的mousePressEvent以保证正常的事件处理
QLabel::mousePressEvent(event);
}

// 处理鼠标悬停进入事件
void ClickedLabel::enterEvent(QEvent* event) {
// 在这里处理鼠标悬停进入的逻辑
if(_curstate == ClickLbState::Normal){
qDebug()<<"enter , change to normal hover: "<< _normal_hover;
setProperty("state",_normal_hover);
repolish(this);
update();

}else{
qDebug()<<"enter , change to selected hover: "<< _selected_hover;
setProperty("state",_selected_hover);
repolish(this);
update();
}

QLabel::enterEvent(event);
}

// 处理鼠标悬停离开事件
void ClickedLabel::leaveEvent(QEvent* event){
// 在这里处理鼠标悬停离开的逻辑
if(_curstate == ClickLbState::Normal){
qDebug()<<"leave , change to normal : "<< _normal;
setProperty("state",_normal);
repolish(this);
update();

}else{
qDebug()<<"leave , change to normal hover: "<< _selected;
setProperty("state",_selected);
repolish(this);
update();
}
QLabel::leaveEvent(event);
}

void ClickedLabel::SetState(QString normal, QString hover, QString press,
QString select, QString select_hover, QString select_press)
{
_normal = normal;
_normal_hover = hover;
_normal_press = press;

_selected = select;
_selected_hover = select_hover;
_selected_press = select_press;

setProperty("state",normal);
repolish(this);
}

ClickLbState ClickedLabel::GetCurState(){
return _curstate;
}

将label升级为ClickedLabel,然后在RegisterDialog的构造函数中添加label点击的响应函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

//设置浮动显示手形状
ui->pass_visible->setCursor(Qt::PointingHandCursor);
ui->confirm_visible->setCursor(Qt::PointingHandCursor);

ui->pass_visible->SetState("unvisible","unvisible_hover","","visible",
"visible_hover","");

ui->confirm_visible->SetState("unvisible","unvisible_hover","","visible",
"visible_hover","");
//连接点击事件

connect(ui->pass_visible, &ClickedLabel::clicked, this, [this]() {
auto state = ui->pass_visible->GetCurState();
if(state == ClickLbState::Normal){
ui->pass_edit->setEchoMode(QLineEdit::Password);
}else{
ui->pass_edit->setEchoMode(QLineEdit::Normal);
}
qDebug() << "Label was clicked!";
});

connect(ui->confirm_visible, &ClickedLabel::clicked, this, [this]() {
auto state = ui->confirm_visible->GetCurState();
if(state == ClickLbState::Normal){
ui->confirm_edit->setEchoMode(QLineEdit::Password);
}else{
ui->confirm_edit->setEchoMode(QLineEdit::Normal);
}
qDebug() << "Label was clicked!";
});

这样就实现了通过点击切换密码的显示和隐藏。

注册成功提示页面

注册成功后要切换到提示页面,所以在initHandlers函数内实现收到服务器注册回复的请求

1
2
3
4
5
6
7
8
9
10
11
12
13
//注册注册用户回包逻辑
_handlers.insert(ReqId::ID_REG_USER, [this](QJsonObject jsonObj){
int error = jsonObj["error"].toInt();
if(error != ErrorCodes::SUCCESS){
showTip(tr("参数错误"),false);
return;
}
auto email = jsonObj["email"].toString();
showTip(tr("用户注册成功"), true);
qDebug()<< "email is " << email ;
qDebug()<< "user uuid is " << jsonObj["uuid"].toString();
ChangeTipPage();
});

页面切换逻辑

1
2
3
4
5
6
7
8
void RegisterDialog::ChangeTipPage()
{
_countdown_timer->stop();
ui->stackedWidget->setCurrentWidget(ui->page_2);

// 启动定时器,设置间隔为1000毫秒(1秒)
_countdown_timer->start(1000);
}

在RegisterDialog.ui中stackwidget的page2添加标签和返回按钮

https://cdn.llfc.club/1712821604946.jpg

在RegisterDialog构造函数中添加定时器回调

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建定时器
_countdown_timer = new QTimer(this);
// 连接信号和槽
connect(_countdown_timer, &QTimer::timeout, [this](){
if(_countdown==0){
_countdown_timer->stop();
emit sigSwitchLogin();
return;
}
_countdown--;
auto str = QString("注册成功,%1 s后返回登录").arg(_countdown);
ui->tip_lb->setText(str);
});

除此之外在返回按钮的槽函数中停止定时器并发送切换登录的信号

1
2
3
4
5
void RegisterDialog::on_return_btn_clicked()
{
_countdown_timer->stop();
emit sigSwitchLogin();
}

取消注册也发送切换登录信号

1
2
3
4
5
void RegisterDialog::on_cancel_btn_clicked()
{
_countdown_timer->stop();
emit sigSwitchLogin();
}

界面跳转

回到mainwindow,构造函数简化,只做登录界面初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
//创建一个CentralWidget, 并将其设置为MainWindow的中心部件
_login_dlg = new LoginDialog(this);
_login_dlg->setWindowFlags(Qt::CustomizeWindowHint|Qt::FramelessWindowHint);
setCentralWidget(_login_dlg);

//连接登录界面注册信号
connect(_login_dlg, &LoginDialog::switchRegister, this, &MainWindow::SlotSwitchReg);
//连接登录界面忘记密码信号
connect(_login_dlg, &LoginDialog::switchReset, this, &MainWindow::SlotSwitchReset);
}

在点击注册按钮的槽函数中

1
2
3
4
5
6
7
8
9
10
11
12
13
void MainWindow::SlotSwitchReg()
{
_reg_dlg = new RegisterDialog(this);
_reg_dlg->hide();

_reg_dlg->setWindowFlags(Qt::CustomizeWindowHint|Qt::FramelessWindowHint);

//连接注册界面返回登录信号
connect(_reg_dlg, &RegisterDialog::sigSwitchLogin, this, &MainWindow::SlotSwitchLogin);
setCentralWidget(_reg_dlg);
_login_dlg->hide();
_reg_dlg->show();
}

切换登录界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//从注册界面返回登录界面
void MainWindow::SlotSwitchLogin()
{
//创建一个CentralWidget, 并将其设置为MainWindow的中心部件
_login_dlg = new LoginDialog(this);
_login_dlg->setWindowFlags(Qt::CustomizeWindowHint|Qt::FramelessWindowHint);
setCentralWidget(_login_dlg);

_reg_dlg->hide();
_login_dlg->show();
//连接登录界面注册信号
connect(_login_dlg, &LoginDialog::switchRegister, this, &MainWindow::SlotSwitchReg);
//连接登录界面忘记密码信号
connect(_login_dlg, &LoginDialog::switchReset, this, &MainWindow::SlotSwitchReset);
}

这样登录界面和注册界面的切换逻辑就写完了。

C++ 全栈聊天项目(13) 实现验证码服务和用户注册功能

Posted on 2024-08-31 | In C++聊天项目

重置密码label

当我们在登录忘记密码的时候可以支持重置密码,重置密码label也要实现浮动和点击效果,以及未点击效果。所以我们复用之前的ClickedLabel,
在登录界面中升级forget_label为ClickedLabel。

1
2
3
4
5
6
7
8
9
10
LoginDialog::LoginDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::LoginDialog)
{
ui->setupUi(this);
connect(ui->reg_btn, &QPushButton::clicked, this, &LoginDialog::switchRegister);
ui->forget_label->SetState("normal","hover","","selected","selected_hover","");
ui->forget_label->setCursor(Qt::PointingHandCursor);
connect(ui->forget_label, &ClickedLabel::clicked, this, &LoginDialog::slot_forget_pwd);
}

点击忘记密码发送对应的信号

1
2
3
4
5
void LoginDialog::slot_forget_pwd()
{
qDebug()<<"slot forget pwd";
emit switchReset();
}

我们在mainwindow中连接了重置密码的信号和槽

1
2
//连接登录界面忘记密码信号
connect(_login_dlg, &LoginDialog::switchReset, this, &MainWindow::SlotSwitchReset);

实现SlotSwitchReset

1
2
3
4
5
6
7
8
9
10
11
12
void MainWindow::SlotSwitchReset()
{
//创建一个CentralWidget, 并将其设置为MainWindow的中心部件
_reset_dlg = new ResetDialog(this);
_reset_dlg->setWindowFlags(Qt::CustomizeWindowHint|Qt::FramelessWindowHint);
setCentralWidget(_reset_dlg);

_login_dlg->hide();
_reset_dlg->show();
//注册返回登录信号和槽函数
connect(_reset_dlg, &ResetDialog::switchLogin, this, &MainWindow::SlotSwitchLogin2);
}

ResetDialog是我们添加的界面类,新建ResetDialog界面类,界面布局如下

https://cdn.llfc.club/1712826184149.jpg

重置界面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include "resetdialog.h"
#include "ui_resetdialog.h"
#include <QDebug>
#include <QRegularExpression>
#include "global.h"
#include "httpmgr.h"

ResetDialog::ResetDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::ResetDialog)
{
ui->setupUi(this);

connect(ui->user_edit,&QLineEdit::editingFinished,this,[this](){
checkUserValid();
});

connect(ui->email_edit, &QLineEdit::editingFinished, this, [this](){
checkEmailValid();
});

connect(ui->pwd_edit, &QLineEdit::editingFinished, this, [this](){
checkPassValid();
});


connect(ui->varify_edit, &QLineEdit::editingFinished, this, [this](){
checkVarifyValid();
});

//连接reset相关信号和注册处理回调
initHandlers();
connect(HttpMgr::GetInstance().get(), &HttpMgr::sig_reset_mod_finish, this,
&ResetDialog::slot_reset_mod_finish);

}

下面是检测逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
bool ResetDialog::checkUserValid()
{
if(ui->user_edit->text() == ""){
AddTipErr(TipErr::TIP_USER_ERR, tr("用户名不能为空"));
return false;
}

DelTipErr(TipErr::TIP_USER_ERR);
return true;
}


bool ResetDialog::checkPassValid()
{
auto pass = ui->pwd_edit->text();

if(pass.length() < 6 || pass.length()>15){
//提示长度不准确
AddTipErr(TipErr::TIP_PWD_ERR, tr("密码长度应为6~15"));
return false;
}

// 创建一个正则表达式对象,按照上述密码要求
// 这个正则表达式解释:
// ^[a-zA-Z0-9!@#$%^&*]{6,15}$ 密码长度至少6,可以是字母、数字和特定的特殊字符
QRegularExpression regExp("^[a-zA-Z0-9!@#$%^&*]{6,15}$");
bool match = regExp.match(pass).hasMatch();
if(!match){
//提示字符非法
AddTipErr(TipErr::TIP_PWD_ERR, tr("不能包含非法字符"));
return false;;
}

DelTipErr(TipErr::TIP_PWD_ERR);

return true;
}

bool ResetDialog::checkEmailValid()
{
//验证邮箱的地址正则表达式
auto email = ui->email_edit->text();
// 邮箱地址的正则表达式
QRegularExpression regex(R"((\w+)(\.|_)?(\w*)@(\w+)(\.(\w+))+)");
bool match = regex.match(email).hasMatch(); // 执行正则表达式匹配
if(!match){
//提示邮箱不正确
AddTipErr(TipErr::TIP_EMAIL_ERR, tr("邮箱地址不正确"));
return false;
}

DelTipErr(TipErr::TIP_EMAIL_ERR);
return true;
}

bool ResetDialog::checkVarifyValid()
{
auto pass = ui->varify_edit->text();
if(pass.isEmpty()){
AddTipErr(TipErr::TIP_VARIFY_ERR, tr("验证码不能为空"));
return false;
}

DelTipErr(TipErr::TIP_VARIFY_ERR);
return true;
}

void ResetDialog::AddTipErr(TipErr te, QString tips)
{
_tip_errs[te] = tips;
showTip(tips, false);
}

void ResetDialog::DelTipErr(TipErr te)
{
_tip_errs.remove(te);
if(_tip_errs.empty()){
ui->err_tip->clear();
return;
}

showTip(_tip_errs.first(), false);
}

显示接口

1
2
3
4
5
6
7
8
9
10
11
12
void ResetDialog::showTip(QString str, bool b_ok)
{
if(b_ok){
ui->err_tip->setProperty("state","normal");
}else{
ui->err_tip->setProperty("state","err");
}

ui->err_tip->setText(str);

repolish(ui->err_tip);
}

获取验证码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void ResetDialog::on_varify_btn_clicked()
{
qDebug()<<"receive varify btn clicked ";
auto email = ui->email_edit->text();
auto bcheck = checkEmailValid();
if(!bcheck){
return;
}

//发送http请求获取验证码
QJsonObject json_obj;
json_obj["email"] = email;
HttpMgr::GetInstance()->PostHttpReq(QUrl(gate_url_prefix+"/get_varifycode"),
json_obj, ReqId::ID_GET_VARIFY_CODE,Modules::RESETMOD);
}

初始化回包处理逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void ResetDialog::initHandlers()
{
//注册获取验证码回包逻辑
_handlers.insert(ReqId::ID_GET_VARIFY_CODE, [this](QJsonObject jsonObj){
int error = jsonObj["error"].toInt();
if(error != ErrorCodes::SUCCESS){
showTip(tr("参数错误"),false);
return;
}
auto email = jsonObj["email"].toString();
showTip(tr("验证码已发送到邮箱,注意查收"), true);
qDebug()<< "email is " << email ;
});

//注册注册用户回包逻辑
_handlers.insert(ReqId::ID_RESET_PWD, [this](QJsonObject jsonObj){
int error = jsonObj["error"].toInt();
if(error != ErrorCodes::SUCCESS){
showTip(tr("参数错误"),false);
return;
}
auto email = jsonObj["email"].toString();
showTip(tr("重置成功,点击返回登录"), true);
qDebug()<< "email is " << email ;
qDebug()<< "user uuid is " << jsonObj["uuid"].toString();
});
}

根据返回的id调用不同的回报处理逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void ResetDialog::slot_reset_mod_finish(ReqId id, QString res, ErrorCodes err)
{
if(err != ErrorCodes::SUCCESS){
showTip(tr("网络请求错误"),false);
return;
}

// 解析 JSON 字符串,res需转化为QByteArray
QJsonDocument jsonDoc = QJsonDocument::fromJson(res.toUtf8());
//json解析错误
if(jsonDoc.isNull()){
showTip(tr("json解析错误"),false);
return;
}

//json解析错误
if(!jsonDoc.isObject()){
showTip(tr("json解析错误"),false);
return;
}


//调用对应的逻辑,根据id回调。
_handlers[id](jsonDoc.object());

return;
}

这里实现发送逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
void ResetDialog::on_sure_btn_clicked()
{
bool valid = checkUserValid();
if(!valid){
return;
}

valid = checkEmailValid();
if(!valid){
return;
}

valid = checkPassValid();
if(!valid){
return;
}

valid = checkVarifyValid();
if(!valid){
return;
}

//发送http重置用户请求
QJsonObject json_obj;
json_obj["user"] = ui->user_edit->text();
json_obj["email"] = ui->email_edit->text();
json_obj["passwd"] = xorString(ui->pwd_edit->text());
json_obj["varifycode"] = ui->varify_edit->text();
HttpMgr::GetInstance()->PostHttpReq(QUrl(gate_url_prefix+"/reset_pwd"),
json_obj, ReqId::ID_RESET_PWD,Modules::RESETMOD);
}

注册、重置、登录切换

我们要实现注册、重置、登录三个界面的替换,就需要在MainWindow中添加SlotSwitchLogin2的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//从重置界面返回登录界面
void MainWindow::SlotSwitchLogin2()
{
//创建一个CentralWidget, 并将其设置为MainWindow的中心部件
_login_dlg = new LoginDialog(this);
_login_dlg->setWindowFlags(Qt::CustomizeWindowHint|Qt::FramelessWindowHint);
setCentralWidget(_login_dlg);

_reset_dlg->hide();
_login_dlg->show();
//连接登录界面忘记密码信号
connect(_login_dlg, &LoginDialog::switchReset, this, &MainWindow::SlotSwitchReset);
//连接登录界面注册信号
connect(_login_dlg, &LoginDialog::switchRegister, this, &MainWindow::SlotSwitchReg);
}

服务端响应重置

在LogicSystem的构造函数中增加注册逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//重置回调逻辑
RegPost("/reset_pwd", [](std::shared_ptr<HttpConnection> connection) {
auto body_str = boost::beast::buffers_to_string(connection->_request.body().data());
std::cout << "receive body is " << body_str << std::endl;
connection->_response.set(http::field::content_type, "text/json");
Json::Value root;
Json::Reader reader;
Json::Value src_root;
bool parse_success = reader.parse(body_str, src_root);
if (!parse_success) {
std::cout << "Failed to parse JSON data!" << std::endl;
root["error"] = ErrorCodes::Error_Json;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}

auto email = src_root["email"].asString();
auto name = src_root["user"].asString();
auto pwd = src_root["passwd"].asString();

//先查找redis中email对应的验证码是否合理
std::string varify_code;
bool b_get_varify = RedisMgr::GetInstance()->Get(CODEPREFIX + src_root["email"].asString(), varify_code);
if (!b_get_varify) {
std::cout << " get varify code expired" << std::endl;
root["error"] = ErrorCodes::VarifyExpired;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}

if (varify_code != src_root["varifycode"].asString()) {
std::cout << " varify code error" << std::endl;
root["error"] = ErrorCodes::VarifyCodeErr;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}
//查询数据库判断用户名和邮箱是否匹配
bool email_valid = MysqlMgr::GetInstance()->CheckEmail(name, email);
if (!email_valid) {
std::cout << " user email not match" << std::endl;
root["error"] = ErrorCodes::EmailNotMatch;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}

//更新密码为最新密码
bool b_up = MysqlMgr::GetInstance()->UpdatePwd(name, pwd);
if (!b_up) {
std::cout << " update pwd failed" << std::endl;
root["error"] = ErrorCodes::PasswdUpFailed;
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
}

std::cout << "succeed to update password" << pwd << std::endl;
root["error"] = 0;
root["email"] = email;
root["user"] = name;
root["passwd"] = pwd;
root["varifycode"] = src_root["varifycode"].asString();
std::string jsonstr = root.toStyledString();
beast::ostream(connection->_response.body()) << jsonstr;
return true;
});

在Mysql中新增CheckEmail和UpdatePwd函数

1
2
3
4
5
6
7
bool MysqlMgr::CheckEmail(const std::string& name, const std::string& email) {
return _dao.CheckEmail(name, email);
}

bool MysqlMgr::UpdatePwd(const std::string& name, const std::string& pwd) {
return _dao.UpdatePwd(name, pwd);
}

DAO这一层写具体的逻辑, 检测邮箱是否合理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
bool MysqlDao::CheckEmail(const std::string& name, const std::string& email) {
auto con = pool_->getConnection();
try {
if (con == nullptr) {
pool_->returnConnection(std::move(con));
return false;
}

// 准备查询语句
std::unique_ptr<sql::PreparedStatement> pstmt(con->prepareStatement("SELECT email FROM user WHERE name = ?"));

// 绑定参数
pstmt->setString(1, name);

// 执行查询
std::unique_ptr<sql::ResultSet> res(pstmt->executeQuery());

// 遍历结果集
while (res->next()) {
std::cout << "Check Email: " << res->getString("email") << std::endl;
if (email != res->getString("email")) {
pool_->returnConnection(std::move(con));
return false;
}
pool_->returnConnection(std::move(con));
return true;
}
}
catch (sql::SQLException& e) {
pool_->returnConnection(std::move(con));
std::cerr << "SQLException: " << e.what();
std::cerr << " (MySQL error code: " << e.getErrorCode();
std::cerr << ", SQLState: " << e.getSQLState() << " )" << std::endl;
return false;
}
}

更新密码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
bool MysqlDao::UpdatePwd(const std::string& name, const std::string& newpwd) {
auto con = pool_->getConnection();
try {
if (con == nullptr) {
pool_->returnConnection(std::move(con));
return false;
}

// 准备查询语句
std::unique_ptr<sql::PreparedStatement> pstmt(con->prepareStatement("UPDATE user SET pwd = ? WHERE name = ?"));

// 绑定参数
pstmt->setString(2, name);
pstmt->setString(1, newpwd);

// 执行更新
int updateCount = pstmt->executeUpdate();

std::cout << "Updated rows: " << updateCount << std::endl;
pool_->returnConnection(std::move(con));
return true;
}
catch (sql::SQLException& e) {
pool_->returnConnection(std::move(con));
std::cerr << "SQLException: " << e.what();
std::cerr << " (MySQL error code: " << e.getErrorCode();
std::cerr << ", SQLState: " << e.getSQLState() << " )" << std::endl;
return false;
}
}
<1…345…37>

370 posts
17 categories
21 tags
RSS
GitHub ZhiHu
© 2025 恋恋风辰 本站总访问量次 | 本站访客数人
Powered by Hexo
|
Theme — NexT.Muse v5.1.3