项目中开发的问题

线程 & QThread使用 & 信号槽第五参数

1. 事件循环

对于文件发送这类需要花费大量资源的行为,若陷入死循环会占用大量的CPU资源,应当使用sleep(100)主动释放CPU:在一段时间后被添加到就绪队列中,等待切换到执行态(大于100ms)

在QT中,若在某个组件的槽函数(调用事件后的执行函数)中,执行耗时特别久的内容,会影响主事件循环,导致出现未响应。解决方法:使用多线程。

int main(int argc,char* argv[]){
    QApplication a(argc,argv);//初始化窗口系统,并使用命令行参数构造main中应用程序对象
    Dialog w;    //对话窗对象
    w.show();    //显示窗口
    return a.exec();    //进入主事件循环
}
​
static QApplication::exec(){
    while(1){    //事件循环
        //执行事件:比如事件响应 槽函数
    
        //return exit();
    }
}
​
void Dialog::on_pb_test_clicked(){
    //模拟发送文件
    while(1){   //未响应
        qDebug()<<"发文件";
        _sleep(100);
    }
}
​
2. 多线程

如何创建线程

1)CreateThread (WINAPI)ExitThread(会导致内存泄漏)

2)pthread_create(linux)pthread_join()

3)std::thread(c++线程对象)

4)_beginthreadex(C运行时库) _endthreadex(会回收C运行时库使用的内存块)

    _beginthreadex(0/*安全级别*/,
                   0/*线程栈大小,默认为1M*/,
                   &ThreadFunc/*线程函数*/,
                   (void*)this/*参数,传入当前类*/,
                   0/*创建即运行*/,
                   0);
​
//__stdcall 调用约定
unsigned __stdcall ThreadFunc (void *arg){
    //模拟发送文件
    while(1){
        qDebug()<<"发文件";
        _sleep(100);
    }
}

Ui::Dialog *ui;是私有的,如何使用:

  1. 通过公有get方法,获取ui

  2. 在Dialog类中声明友元

        friend unsigned __stdcall ThreadFunc (void *arg);
    ​
    unsigned __stdcall ThreadFunc (void *arg){
        //让按钮隐藏
        Dialog* p=(Dialog*)arg;
        p->ui->pb_show->hide();
    }

发现,程序崩溃了。原因:在另一个线程中,不允许操作主界面的控件。

解决方法:可以利用信号和槽传回主线程进行操作

信号和槽的条件:

  1. Q_OBJECT 宏

  2. connect连接信号和槽

  3. 发送信号和定义槽函数的类,需要是QObject的子类

5)QThread

6)QObject::moveToThread

3. QThread

使用QThread实现,满足信号与槽的条件

实现QThread的子类,可以发信号

class Dialog:public QDialog {
    Q_OBJECT
    MyThread* m_thread;
};
Dialog::Dialog(){
    qDebug()<<"main thread:"<<QThread::currentThreadId();   //获取当前线程HANDLE
    m_thread=new MyThread;
}
void Dialog::on_pb_test_clicked(){
    m_thread->start();  //->run()
}
class MyThread:public QThread{ //自定义线程类
    Q_OBJECT
    //定义线程函数
    void run(){//重写父类的虚函数
        qDebug()<<"this thread:"<<QThread::currentThreadId();
    }
};

执行start()开启新的线程,新线程调用run()开启线程的执行。run()函数结束,线程退出

如果线程正在运行(run()正在执行),start()调用后什么也不做

//实现用另一个线程隐藏控件
class MyThread : public QThread{
    Q_OBJECT
signals:
    void SIG_toHide();
public:
    void run(){
        //隐藏控件
        Q_EMIT SIG_toHide();
    }
};
class Dialog : public QDialog{
    Q_OBJECT
private slots:
    void slot_toHide();
};
Dialog::Dialog(){
    m_thread=new MyThread;
    connect(m_thread,SIGNAL(SIG_toHide()),this,SLOT(slot_toHide()));
}
Dialog::~Dialog(){
    if(m_thread->isRunning()){
        m_thread->quit();
        m_thread->wait(100);
        if(m_thread->isRunning()){
            m_thread->terminate();
            m_thread->wait(100);
        }
        delete m_thread;
    }
}
void Dialog::slot_toHide(){
    qDebug()<<__func__<<":"<<QThread::currentThreadId();
    ui->pb_show->hide();
}

image-20240729103450330

4. connect
    QObject::connect(const QObject *sender, /*信号发出者*/
                     const char *signal, /*信号*/
                     const QObject *receiver, /*信号接收者*/
                     const char *method, /*槽函数*/
                     Qt::ConnectionType type = Qt::AutoConnection 
                     /*连接方法,默认自动连接*/
                    );

image-20240728194845722

connect(m_thread,SIGNAL(SIG_toHide()),this,SLOT(slot_toHide()),
            (Qt::ConnectionType)(Qt::QueuedConnection | Qt::UniqueConnection));

DirectConnection:在同一个线程里直接连接,效果等同于直接调用函数

QueuedConnection:不在一个线程里执行,发送信号后马上执行后续代码

BlockingQueuedConnection:不在一个线程里执行,等待槽函数返回后执行后续代码

5. moveToThread
  1. QObject子类定义

  2. 创建一个QThread对象

  3. 子类对象调用moveToThread,Qthread对象调用start()开启线程

  4. 定义信号槽,接收到信号,就让子类的槽函数执行(在移动到的目标线程执行)

class WorkObject : public QObject{
    Q_OBJECT
public slots:
    void slot_thisWork(){
        qDebug()<<__func__<<":"<<QThread::currentThreadId();
    }
};
class Dialog : public QDialog{
    Q_OBJECT
signals:
    void SIG_toDoWork();
private:
    WorkObject* m_worker;
    QThread* m_workerThread;
};
Dialog::Dialog(){ 
    m_worker=new WorkObject;
    m_workerThread=new QThread;
    m_worker->moveToThread(m_workerThread);
    
    connect(this,SIGNAL(SIG_toDoWork()),m_worker,SLOT(slot_thisWork()));
    
    m_workerThread->start();
}
Dialog::~Dialog(){
    delete m_worker;
    if(m_workerThread->isRunning()){
        m_workerThread->quit();
        m_workerThread->wait(100);
        if(m_workerThread->isRunning()){
            m_workerThread->terminate();
            m_workerThread->wait(100);
        }
        delete m_workerThread;
    }
}
void Dialog::on_pb_todoWork_clicked(){
    qDebug()<<__func__<<"main:"<<QThread::currentThreadId();    
    Q_EMIT SIG_toDoWork();
}

image-20240729103610461

使用moveToThread,不会像QThread一样每次start()创建一个新线程,而是一直一个线程。可以让函数到另一个线程中执行。

class Dialog{
public:
    static Dialog* m_pThis;
};
Dialog* Dialog::m_pThis=nullptr;
Dialog::Dialog{
    m_pThis=this;
    //m_workerThread->start();
    SendFile(); //主线程
}
void WorkObject::slot_thisWork(){
    qDebug()<<__func__<<":"<<QThread::currentThreadId();
    Dialog::m_pThis->SendFile();    //子线程
}

image-20240729104626044

优点:可以将一段要执行的代码,放到另一个线程执行,同时一直在一个线程里执行。QThread通过run()函数,也可以将一段要执行的代码,放到另一个线程执行,但是每次start()都会创建一个新的线程,相比moveToThread效率低,线程开销大,耗费额外的空间。

C++11线程 & 3种锁

1. C++11线程 std::thread

thread对象要结合join()或detach()使用,避免线程未结束线程对象生命周期就结束了

#include <thread>

int add(int a,int b){	//线程函数没有要求格式,比较灵活
    qDebug()<<"a+b="<<a+b;
    _sleep(1000);
    qDebug()<<"end";
    return a+b;
}

	std::thread th(&add,3,4);   //线程函数以参数形式传入

	_sleep(100);
    qDebug()<<"before";
    th.join();
	//th.detach();
    qDebug()<<"after";

join()阻塞当前线程,直到线程函数退出

image-20240729133948119

detach()将线程对象与线程函数分离,线程不依赖线程对象管理

image-20240729133918327

join()和detach(),两者必选其一,不然线程对象的回收会影响线程的回收,导致程序崩溃

class AA{
    AA(){
        //在构造函数中创建线程
        //如果使用_beginthreadex,线程函数需是静态的
        std::thread th(&threadFun,this);    //类成员函数,第一个参数是隐藏的this
        th.detach();
    }
    void threadFun(){
        while(!m_isQuit){
            _sleep(100);
            qDebug()<<"do some work";
        }
    }
private:
    bool m_isQuit=false;
};
2. 线程同步

解决线程并发引起的数据问题

并发:同一时间间隔内,程序交替执行

线程同步:同一时间,只允许一个线程可以读写某资源

方式:锁、关键段、读写锁、原子访问、信号、事件

2.1 互斥锁
#include <mutex>
std::mutex mtx;

	mtx.lock();
	//访问全局资源
	mtx.unlock();

RAII(Resource Access Is Initializer)资源获取即初始化

例如:STL容器对象,开辟的空间在堆区,对象空间看定义的位置

lock_guard、unique_lock 管理互斥锁,遵循RAII,让互斥锁使用更方便更安全(避免死锁)

{
    std::lock_guard<std::mutex> lck(mtx);   //在初始化lck时,会对互斥锁上锁
}//lck生命周期结束,自动释放锁

unique_lock 可以在中途释放锁。lock_guard可以借助{}实现(lock_guard耗费的资源更少、更快)

	std::unique_lock<std::mutex> unlck(mtx);
	unlck.unlock();

粒度:锁定代码的长度。锁定的代码长度越长,锁的粒度越大,影响并发的效率

2.2 条件变量
#include <condition_variable>
std::condition_variable con_var;
​
    wait(/*一个已经上锁的互斥锁*/,
         /*可调用对象(函数指针、仿函数、bind、lamba表达式),若返回值是false,会通知无效仍阻塞在wait(),避免误通知*/);   
    //阻塞当前线程,直到收到通知
    //当调用wait()时,释放互斥锁,阻塞当前线程,将线程放入条件变量等待的容器中
    //当收到通知时,获取互斥锁,执行后续代码
    notify_one; //通知一个
    notify_all;//通知所有
std::condition_variable con_var;
​
void ConditionFun(){
    std::unique_lock<std::mutex> unlck(mtx);
    qDebug()<<"before wait";
    con_var.wait(unlck);
    qDebug()<<"after wait";
}
​
int main(){
    std::thread th[3];
       for(int i=0;i<3;i++){
       th[i]=std::thread(&ConditionFun);
    }
    for(int i=0;i<3;i++){
        th[i].detach();
    }
}
​
extern std::condition_variable con_var; //链接到外部同名变量
void Dialog::on_pb_notify_clicked(){
    //通知条件变量
    con_var.notify_one();
}
bool check(){
    return false;
}

	con_var.wait(unlck,&check);	//返回值是false,通知失效

注意一定是wait()之后才能收到通知,不然通知就失效了

2.3 原子访问

原子访问仅对某个变量修改值时锁定,速度相对其他同步方式更快

#include <atomic>
std::atomic<int> count(0);
	count++;	//加锁的方式进行++,注意:++ --操作是原子操作,可以保证线程安全
	count--;	//枷锁的方式进行--
	count.load();	//读值
	//count=count+10;	//ERROR:非原子操作,线程不安全
std::atomic<int> cnt(0);

void AtomicFunc(){
    for(int i=0;i<100;i++){
        _sleep(10);
        cnt++;
        _sleep(10);
    }
}

int main(){
    std::thread th[3];
    for(int i=0;i<3;i++){
        th[i]=std::thread(&AtomicFunc);
    }
    for(int i=0;i<3;i++){
        th[i].join();
    }
    qDebug()<<"count="<<cnt.load();
}

C++11 lambda表达式

在函数里定义另一个函数,例如:sort()

bool comp(int a,int b){ //comp(a,b)为真,保持顺序,从大到小
    return a>b;
}
​
    vector<int> vec={1,3,5,2,4,6};
    sort(vec.begin(),vec.end(),&comp);  //sort默认为从小到大
    //数量较少使用插入排序,大量数据使用快排,同时为了避免出现因为数据集导致快排退化、会使用堆排序辅助排序

可以使用lambda表达式定义一个匿名函数,在函数定义里完成对另一个函数的定义

    sort(vec.begin(),vec.end(),
        [] (int a,int b) {return a>b;});    //作为匿名函数使用
    auto 函数名=lambda表达式; //通过函数名,调用函数

image-20240729152806296

  1. 捕获列表:捕获(lambda表达式)外面的局部的变量,以及this指针。静态和全局的变量不用捕获,直接能用

    [ = ] 捕获外面的非静态局部的全部变量的副本,值传递,以及this指针

    [ & ] 捕获外面的非静态局部的全部变量的引用,引用传递,以及this指针

    [ val ] 捕获val变量,值传递

    [ &val ] 捕获val的引用,引用传递

    [ this ] 捕获this指针

    image-20240729153332032

    class AA{
    public:
        AA(){
            auto f=[this](){  cout<<"this:"<<this<<endl;  };
            f();
        }
    };
    
    connect(ui->pushButton,&QPushButton::clicked,
            this,[this](){ this->ui->pushButton->setText("改变名字");});
    
  2. 参数列表:与函数的参数列表是一样,可省。但有mutable修饰时不可省

  3. mutable:可变的。没有mutable修饰时是一个特殊的常函数,不可以修改非引用的值(副本不可改)

    image-20240729153620322

  4. 返回值:可省,自动匹配返回值类型

  5. 函数体:与函数写法一致

C++11 智能指针

C++11现代化C++,让代码更容易简洁

导致内存泄漏的原因:

  1. 堆区没有手动释放空间

  2. 父类没有虚析构,回收父类只调用了父类析构

  3. 文件

  4. 网络套接字socket

  5. 资源句柄HANDLE(线程、窗口)

把指针变成对象,在生命周期结束时,通过析构函数自动回收空间

#include <memory>
    auto_ptr    
    unique_ptr  
    shared_ptr  
    weak_ptr    
1. auto_ptr

为什么使用指针?

利用指针实现地址传递,间接访问修改空间值

#include <memory>
​
class AAA{
public:
    AAA(string _str):m_str(_str){
        cout<<"AAA::AAA(str):"<<m_str<<endl;
    }
    ~AAA(){
        cout<<"AAA::~AAA()"<<endl;
    }
    void say(){
        cout<<"say:"<<m_str<<endl;
    }
private:
    string m_str;
};
​
void autoFunc(std::auto_ptr<AAA> apt  /* =apt2 */){  //apt2在传参时发生拷贝构造,apt2为空
}
​
int main()
{
    {
        cout<<"auto ptr"<<endl;
        //创建 智能指针接收创建的堆空间
        std::auto_ptr<AAA> apt1(new AAA("auto_ptr1"));
        std::auto_ptr<AAA> apt2;
        apt2.reset(new AAA("auto_ptr2"));   //清空已有的创建新的
        std::auto_ptr<AAA> apt3=apt1;   //拷贝构造,不允许两个指针指向同一空间,原指针赋空
        //std::auto_ptr<AAA> apt3(apt1);
        //使用
        apt2.get()->say();
        apt2->say();
​
        //判断
        if(apt1.get()==nullptr){    //拷贝构造,原指针为空
            cout<<"apt1 null point"<<endl;
        }
​
        autoFunc(apt2); //apt2变为空指针
        //apt2->say();    //ERROR:apt2为空
        
        //回收
        //reset()
    }
    return 0;
}

C++会避免类型转换,在我们不知情的情况下创建对象,发生拷贝构造,把空间转移了

2. unique_ptr

唯一所有权的智能指针,某一个空间只有一个智能指针持有。不允许系统未经用户允许就发生空间转移,需要手动使用std::move()

    //AAA(const AAA &a)=delete;    //删除拷贝构造,禁止外部调用C++自动生成的函数
​
{
   cout<<"unique ptr"<<endl;
   //创建 智能指针接收创建的堆空间
   std::unique_ptr<AAA> upt1(new AAA("unique_ptr1"));
   std::unique_ptr<AAA> upt2;
   upt2.reset(new AAA("unique_ptr2"));   //清空已有的创建新的
   //std::unique_ptr<AAA> upt3=upt1;   //ERROR:使用了一个删除的函数,unique_ptr禁止了拷贝构造和拷贝赋值
   std::unique_ptr<AAA> upt3;
   //upt3=upt1;  //ERROR:使用了一个删除的函数,unique_ptr禁止了赋值重载运算符
   upt3=std::move(upt1);  //使用转移语句实现空间转移,避免重复资源,使用权传递
   upt3->say();
   if(upt1.get()==nullptr){
        cout<<"upt1 null point"<<endl;
   }
​
}
3. shared_ptr

共享使用权的智能指针,允许多个智能指针指向同一个空间。使用引用计数,实现回收

特性:

  1. 引用计数:指向相同空间,引用计数+1;智能指针回收,引用计数-1;引用计数归0,空间回收

  2. 引用计数+1 -1是线程安全的

  3. 使用use_count()查看引用计数

{
    //创建一个shared_ptr对象
    shared_ptr<AAA> spt1(new AAA("shared_ptr1"));
    shared_ptr<AAA> spt2=std::make_shared<AAA>("shared_ptr2");
    shared_ptr<AAA> spt3;
    spt3.reset(new AAA("shared_ptr3"));
    {
        shared_ptr<AAA> spt4=spt1;	//指向同一个,共享
        cout<<"spt1 use_count:"<<spt1.use_count()<<endl;    //2
        spt4->say();
    }
    spt1->say();
    cout<<"spt1 use_count:"<<spt1.use_count()<<endl;    //1
}

问题:循环引用

class BBBB;
class AAAA{
public:
    AAAA(){
        cout<<"AAAA::AAAA()"<<endl;
    }
    ~AAAA(){
        cout<<"AAAA::~AAAA()"<<endl;
    }
    shared_ptr<BBBB> spb;
};
class BBBB{
public:
    BBBB(){
        cout<<"BBBB::BBBB()"<<endl;
    }
    ~BBBB(){
        cout<<"BBBB::~BBBB()"<<endl;
    }
    shared_ptr<AAAA> spa;
};

{
    shared_ptr<AAAA> sa=shared_ptr<AAAA>(new AAAA);
    shared_ptr<BBBB> sb=shared_ptr<BBBB>(new BBBB);

    sa->spb=sb;
    sb->spa=sa;
    cout<<"A use count:"<<sa.use_count()<<endl;
    cout<<"B use count:"<<sb.use_count()<<endl;
	//~BBBB() 	生命周期结束,没有调用析构进行回收
    //~AAAA()	生命周期结束,回收sb sa,但spb spa与空间共存,一直占用空间,导致引用计数不归零,无法回收空间
}
//回收顺序——函数栈FILO

image-20240801093100927

4. weak_ptr

不使用使用权的智能指针,和shared_ptr配合使用,单独不可以指向空间。

weak_ptr访问空间,需要使用lock()函数,获取shared_ptr然后才能获得空间。

指向shared_ptr的空间,空间的引用计数不加1。弱引用,解决了循环引用。

weak_ptr的销毁不会影响引用计数。

expire()方法,可以用于判断引用计数是否为0。若为真,代表引用计数为0,空间回收

class BBBB{
public:
    BBBB(){
        cout<<"BBBB::BBBB()"<<endl;
    }
    ~BBBB(){
        cout<<"BBBB::~BBBB()"<<endl;
    }
    //shared_ptr<AAAA> spa;	//将两者中任意一个shared_ptr改为weak_ptr即可解决循环引用
    weak_ptr<AAAA> spa;	//指向空间,引用计数不加1
};
	{
        shared_ptr<AAAA> sa=shared_ptr<AAAA>(new AAAA);
        shared_ptr<BBBB> sb=shared_ptr<BBBB>(new BBBB);

        sa->spb=sb;
        sb->spa=sa;
        sb->spa.lock()->spb;
        cout<<"A use count:"<<sa.use_count()<<endl; //1
        cout<<"B use count:"<<sb.use_count()<<endl; //2
        {
            weak_ptr<BBBB> wpb=sb;
            cout<<"B use count:"<<sb.use_count()<<endl; //2
            cout<<wpb.expired()<<endl;  //0
        }
        cout<<"B use count:"<<sb.use_count()<<endl; //2
    }

image-20240801093551733

image-20240801093604833

A的引用计数为1,生命周期结束,回收sb sa后,A引用计数归零,先回收A空间。spb回收,B引用计数归0,B空间回收。

手写智能指针 实现线程安全的shared_ptr
#include <iostream>
#include<atomic>

using namespace std;

class AAA{
public:
    AAA(string _str):m_str(_str){
        cout<<"AAA::AAA(str):"<<m_str<<endl;
    }
    AAA(const AAA &a)=delete;    //删除拷贝构造
    ~AAA(){
        cout<<"AAA::~AAA()"<<endl;
    }
    void say(){
        cout<<"say:"<<m_str<<endl;
    }
private:
    string m_str;
};

template<class T>
struct Node{    //sA1 sA2如何共用ptr和count?指向相同的Node地址
    Node(T* _ptr){
        //接收外部空间
        ptr=_ptr;
        nCount=1;
    }
    ~Node(){
        //回收空间
        delete ptr;
        nCount=0;
    }
    T* ptr;
    //int nCount;
    atomic<int> nCount;   //原子访问
};

template<class T>   //写typename或class没区别
class MyShared_ptr{
public:
    //创建,是第一个指向空间的
    MyShared_ptr(T* _ptr){
        m_ptr_count=new Node<T>(_ptr);
    }
    //拷贝构造,已有对象初始化该对象
    MyShared_ptr(const MyShared_ptr& _ptr){
        m_ptr_count=_ptr.m_ptr_count;
        m_ptr_count->nCount++;
    }
    //回收,触发引用计数-1,归0 回收
    ~MyShared_ptr(){
        if(m_ptr_count){
            m_ptr_count->nCount--;
            if(m_ptr_count->nCount==0){
                delete m_ptr_count;
                m_ptr_count=nullptr;
            }
        }
    }
    //赋值重载操作符,返回值为当前对象
    const MyShared_ptr& operator =(const MyShared_ptr& _ptr){
        if(&_ptr==this)
            return *this;
        //原来的空间的引用计数-1,归0回收
        if(m_ptr_count){
            m_ptr_count->nCount--;
            if(m_ptr_count->nCount==0){
                delete m_ptr_count;
            }
            //新空间引用计数+1
            m_ptr_count=_ptr.m_ptr_count;
            m_ptr_count->nCount++;
        }
        return *this;    //this为当前对象的地址
    }
    //引用计数 指向相同空间是共用的
    int use_count(){
        if(m_ptr_count)
            return m_ptr_count->nCount.load();
        return 0;
    }
    //get方法
    T* get(){
        if(m_ptr_count)
            return m_ptr_count;
        return nullptr;
    }

private:
    Node<T>* m_ptr_count; //指针成员,采取浅拷贝,为了共用
};

int main(){
    {
        MyShared_ptr<AAA> spa(new AAA("my shared ptr a"));
        {
            MyShared_ptr<AAA> spb=(new AAA("my shared ptr b"));
            spb=spa;
            cout<<"a use count:"<<spa.use_count()<<endl;
        }
        cout<<"a use count:"<<spa.use_count()<<endl;
    }
    getchar();
    return 0;
}

C++11 chrono时间库

1. 精度ratio
#include <chrono>

template <intmax_t N, intmax_t D = 1> class ratio;
//其中N表示分子,D表示分母,默认用秒表示的时间单位。
typedef ratio<60, 1> minute;	//60s
typedef ratio<1, 1> second;	//1s
typedef ratio<1,1000> milli;  //1/1000s
2. duration 时间间隔
struct duration{
    typedef _Rep rep;    //数值
    typedef _Period period; //单位
};
	duration_cast<转换的目标类型>(原值).count();	
	//截断转换时间,成员函数count()返回单位时间的数量

用std::chrono::duration 表示一段时间

std::this_thread::sleep_for(std::chrono::milliseconds(2000));	//线程休眠
//this_thread 当前线程	
//sleep_for(chrono的时间间隔)休眠一段时间,使用chrono的休眠,增加易读性
//milliseconds 毫秒 2000ms->2s
typedef duration<int64_t, milli> milliseconds;
	std::chrono::duration<2000,ratio<1,1000>>
3. 时间点

Epoch Time:1970-01-01 00:00:00 UTC UNIX系统认为Epoch Time为时间纪元,也就是说在UNIX系统中,一切的时间会以Epoch Time为一个基点,然后通过经过秒数再来计算现如今的时间(UNIX时间戳)。

UTC:世界统一时间,由原子钟提供

GMT:格林威治时间,由本初子午线确定,00:00:00对应北京时间08:00:00

template <class Clock, class Duration = typename Clock::duration>
class time_point;
duration time_since_epoch();	//用于获取当前时间点距离时间戳的时间长度duration

std::chrono::time_point 表示一个具体时间,第一个模板参数Clock用来指定所要使用的时钟,第二个模板函数参数用来表示时间的计量单位(特化的std::chrono::duration<> )。time_point也就是距离时间戳(epoch)的时间长度(duration)

	//时间点
    std::chrono::system_clock::time_point tp=std::chrono::system_clock::now();
	//获取当前时间点
    time_t t_t=std::chrono::system_clock::to_time_t(tp);
    tm* t=std::localtime(&t_t);
    //获取毫秒
    auto du=tp.time_since_epoch();  //获取unix时间戳
	//转换 duration_cast<转换的目标类型>(原值)
    int ms=chrono::duration_cast<chrono::milliseconds>(du).count();
    ms=ms%1000;
    //打印当前时间
    printf("%d-%02d-%02d %02d:%02d:%02d %03d\n",
           t->tm_year+1900,
           t->tm_mon+1,
           t->tm_mday,
           t->tm_hour,
           t->tm_min,
           t->tm_sec,
           ms);
    return 0;
4. 时钟(代表当前系统的时间)
4.1 system_clock系统时钟

获取当前系统时间的时钟,系统时间可以改变。不是连续的,一般不用于计算时间间隔

4.2 steady_clock稳定时钟

不受系统时间影响,单调递增,用于计算时间间隔

4.3 high_resolution_clock高分辨率时钟

system_clock或steady_clock的别名,由系统决定。提供高分辨率的时间计算

//使用稳定时钟计算一段时间间隔
	std::chrono::steady_clock::time_point begin=chrono::steady_clock::now();
    std::this_thread::sleep_for(chrono::seconds(1));
    std::chrono::steady_clock::time_point end=chrono::steady_clock::now();
    //两个时间点的差——>时间间隔
    auto du=end-begin;  //duration类型
    //转换为ms单位
    int diff=chrono::duration_cast<chrono::milliseconds>(du).count();
    cout<<"diff:"<<diff<<endl;

C++11 线程安全的单例模式

#include <mutex>	
	call_once(once_flag& flag,函数, 参数...);	//只执行一次的函数
//执行一次后,自动将flag反转,使函数只执行一次

单例模式的优点:

  1. 保证一个类只有一个对象,避免在不知情的情况下创建对象(类型转换),造成内存泄漏等问题

  2. 提供全局的调用点,方便编程

  3. 提供延迟初始化,并且是线程安全的,可以避免在程序刚运行时初始化

为什么需要是单例?

1)避免多个不同引起歧义,相同需要同步。例如:任务管理器,打印机任务队列、线程池、日志、管理者

2)避免在不知情的情况下创建对象,造成内存泄露

实现:

1)将析构、构造、拷贝构造私有化

2)提供静态公有到的获取对象的方法

//简易版本:使用静态存储区,保证线程安全
class Singleton{
private:
    Singleton()=default;    //使用默认的构造
    ~Singleton()=default;
    Singleton(const Singleton& s)=delete;   //禁用拷贝构造
    Singleton& operator =(const Singleton& s)=delete;
public:
    static Singleton* getInstance(){
        static Singleton s;
        return &s;
    }
};
//问题:静态存储区生命周期为整个程序运行,无法主动回收
1. 饿汉式
//C++11版本 使用堆区
class Singleton{
private:
    Singleton()=default;    //使用默认的构造
    ~Singleton()=default;
    Singleton(const Singleton& s)=delete;   //禁用拷贝构造
    Singleton& operator =(const Singleton& s)=delete;
    
    static Singleton* m_p;  //在.cpp文件中赋值,避免重定义
public:
    static Singleton* getInstance(){
        return m_p;
    }
    static void destroyInstance(){  //饿汉式,在结束手动调用回收
        if(m_p){
            delete m_p;
            m_p=nullptr;
        }  
    }
};
Singleton* Singleton::m_p=new Singleton;    //饿汉式,起始就要给对象
2. 懒汉式
//C++11版本 使用堆区
class Singleton{
private:
    Singleton()=default;    //使用默认的构造
    ~Singleton()=default;
    Singleton(const Singleton& s)=delete;   //禁用拷贝构造
    Singleton& operator =(const Singleton& s)=delete;
    
    static Singleton* m_p;  //在.cpp文件中赋值,避免重定义
    static once_flag flag;
    static std::mutex m_mutex;
public:
    static Singleton* getInstance(){
        //懒汉式,需要时手动创建对象
        /* 线程安全:call_once()
         * if(!m_p){
            //m_p=new Singleton;    //不是线程安全的
            call_once(flag,[](){Singleton::m_p=new Singleton});
        }*/
        //双层判断 double_check,解决需要等锁造成的效率低下
        if(!m_p){   
            std::lock_guard<std::mutex> lck(m_mutex);
            if(!m_p){
                m_p=new Singleton;
            }  
        }
        return m_p;
    }
    static void destroyInstance(){  //释放资源
        if(m_p){
            std::lock_guard<std::mutex> lck(m_mutex);
            if(m_p){
                delete m_p;
            	m_p=nullptr;
            }
        }
    }
};

Singleton* Singleton::m_p=nullptr;  //懒汉式,啥时候用啥时候给
once_flag Singleton::flag;
std::mutex Singleton::m_mutex;
3. 智能指针封装
class Singleton{
private:
    Singleton()=default;    //使用默认的构造
    Singleton(const Singleton& s)=delete;   //禁用拷贝构造
    Singleton& operator =(const Singleton& s)=delete;

    static std::unique_ptr<Singleton> m_pS;
    static once_flag flag;
    static std::mutex m_mutex;
public:
    ~Singleton()=default;
    static Singleton& getInstance(){
        //双层判断
        if(!m_pS.get()){
            std::lock_guard<std::mutex> lck(m_mutex);
            if(!m_pS.get()){
                m_pS.reset(new Singleton);
            }
        }
        return *(m_pS.get());
    }
    static void destroyInstance(){ 
        if(m_pS.get()!=nullptr){
            m_pS.reset();
        }
    }
};

std::unique_ptr<Singleton> Singleton::m_pS;
once_flag Singleton::flag;
std::mutex Singleton::m_mutex;
4. 定义销毁器解决智能指针实现单例析构私有无法访问
#include <iostream>
#include <mutex>
#include <memory>

using namespace std;

class Singleton{
private:
    //私有构造函数,防止外部创建实例
    Singleton(){
        cout<<"Singleton created"<<endl;
    }
    //私有析构函数
    ~Singleton(){
        cout<<"Singleton destoryed"<<endl;
    }
    //解决方法:unique_ptr自定义一个销毁器deleter
    //定义内容结构:内部类结构体是外部类的友元
    struct deleter{
        void operator ()(Singleton *p)const{
            delete p;
        }
    };

    static std::unique_ptr<Singleton,deleter> instance;
    static std::mutex m_mutex;
public:
    static Singleton& getInstance(){
        if(!instance.get()){
            std::lock_guard<std::mutex> lck(m_mutex);
            if(!instance.get()){
                instance.reset(new Singleton);
            }
        }
        return *instance;
    }
    static void destroyInstance(){
        instance.reset();
    }
    void doSomething(){
        cout<<"Doing something"<<endl;
    }
};
std::unique_ptr<Singleton,Singleton::deleter> Singleton::instance;
std::mutex Singleton::m_mutex;

int main()
{
    //获取单例实体并调用方法
    Singleton& singleton=Singleton::getInstance();
    singleton.doSomething();
    cout<<"addr:"<<&Singleton::getInstance()<<endl;
    cout<<"addr:"<<&Singleton::getInstance()<<endl;
    
    //delete &Singleton::getInstance();
    return 0;
}

实现日志

日志:存消息 警告 错误信息(同时记录时间),以备以后出现问题分析问题定位问题

  1. 创建文件保存(不可以只记录输出结果,而不写入文件中持久化保存)

不把所有日志存在一个文件中,不好查,不稳定,存在隐患

达到设定时间间隔就关闭文件,开启新文件;设置行数,达到预设行数(计数器),切换文件

日志文件命名:year-month-day_hour_min_sec_ms.log,精确到ms,保证文件切换时不会重名

  1. 文件写入同步

fwrite写入文本之后,把文本写入缓冲区,文件内容同步fflush,才能把文本写到文件中

fflush(int fd);

现代化操作系统,为了避免反复中断(开销大),会有一个缓冲区,在写入或发送数据时 ,先写入缓冲区,等待 缓冲区满 或 超时后,才会把数据同步完成写入或发送。

  1. 写入日志实现

多线程并发写(锁)会导致乱序。在处理流程里写文件,需要等待文件IO,会导致处理流程延迟增大。

可以使用另一个线程写入文本,不影响处理流程。

使用队列存储,采用特殊的生产者消费者模型——单一消费者

【拓展:异步双缓冲日志】

  1. 生产者消费者模型

(1)组成部分:若干生产者、若干消费者 /管理者/ 同步队列(阻塞队列)

(2)成员任务:生产者负责生产任务,添加到同步队列中。消费者监听同步队列,有任务从队列中拉取任务。

(3)阻塞:当队列满,生产者阻塞。当队列为空,消费者阻塞。/*管理者负责调配生产者和消费者的个数。*/

队列实现:deque、环形队列

线程控制方法:条件变量和互斥锁

  1. 日志是一个单例

//CMyLog.h

#ifndef CMYLOG_H
#define CMYLOG_H

#include <iostream>
#include <string.h>
using namespace std;
#include <deque>
#include <mutex>
#include <condition_variable>
#include <chrono>
#include <thread>

//日志宏的实现
enum ENUM_LOG_TYPE{
    info =0,
    warning,
    error
};
#define WRITE_LOG(a,b) CMyLog::getInstance()->writeLog(a,b);

//1.实现单例
//2.实现框架
class CMyLog{
public:
    static CMyLog* getInstance(){
        static CMyLog log;
        return &log;
    }
    //写日志 信息 警告
    void writeLog(int type,string content){
        //格式化字符串 todo
        string str=content;
        {
            //加锁
            std::lock_guard<std::mutex> lck(m_mutex);
            //添加日志到队列
            m_queue.emplace_back(str);
            //解锁
        }
        //条件变量 通知
        m_conVar.notify_one();
    }
private:
    CMyLog(){
        init();
        //创建线程
        std::thread th(&CMyLog::writeThread,this);
        th.detach();
    }
    ~CMyLog(){
        //退出标志为真
        m_quitFlag=true;
        //条件变量 通知
        m_conVar.notify_one();
        std::this_thread::sleep_for(chrono::microseconds(100));
    }

    CMyLog(const CMyLog& log)=delete;
    CMyLog& operator=(const CMyLog& log)=delete;

    void init(){
        //赋初值
        m_file=fopen("./log.log","w");
        m_quitFlag=false;
        //行数计数 todo
        //文档打开时间 todo
    }

    void writeThread(){
        while(!m_quitFlag){
            //加锁:lockguard RAII 自动解锁
            std::unique_lock<std::mutex> ulck(m_mutex);
            //循环避免错误通知
            while(!m_quitFlag && m_queue.empty()){
                //条件变量等通知
                m_conVar.wait(ulck);
            }
            //队列非空
            if(!m_queue.empty()){
                //队列中取出数据
                string res=m_queue.front();
                m_queue.pop_front();
                //判断是否切换文件 todo
                //写文件
                fwrite(res.c_str(),1,strlen(res.c_str()),m_file);
                //同步文件
                fflush(m_file);
            }
        }
        //关闭文件
        fclose(m_file);
    }
private:
    //日志文件指针
    FILE* m_file;
    //队列
    std::deque<string> m_queue;
    //互斥锁
    std::mutex m_mutex;
    //条件变量
    std::condition_variable m_conVar;
    //线程退出标志
    bool m_quitFlag;
    //行数计数 todo
    //文档打开时间 todo

};

#endif // CMYLOG_H

获取时间

struct MyTime{
    int year;
    int month;
    int day;
    int hour;
    int min;
    int sec;
    unsigned int msec;
    static MyTime getCurTime(){
        MyTime curTime;
        //时间点
        std::chrono::system_clock::time_point tp=std::chrono::system_clock::now();  //获取当前时间点
        time_t t_t=std::chrono::system_clock::to_time_t(tp);
        tm* t=std::localtime(&t_t);
        //获取毫秒
        auto du=tp.time_since_epoch();  //获取unix时间戳(返回值是时间间隔duration)
        int ms=chrono::duration_cast<chrono::milliseconds>(du).count();
        ms=ms%1000;
        //返回当前时间
        curTime.year=t->tm_year+1900;
        curTime.month=t->tm_mon+1;
        curTime.day=t->tm_mday;
        curTime.hour=t->tm_hour;
        curTime.min=t->tm_min;
        curTime.sec=t->tm_sec;
        curTime.msec=ms;
        return curTime;
    }
};

//写日志 信息 警告
    void writeLog(int type,string content){
        MyTime curTime=MyTime::getCurTime();
        char buf[100]="";
        sprintf(buf,"[%04d_%02d_%02d %02d:%02d:%02d %03d]:",
                curTime.year,curTime.month,curTime.day,
                curTime.hour,curTime.min,curTime.sec,
                curTime.msec);
        //格式化字符串
        string str;
        switch (type) {
        case info:
            str="info ";
            break;
        case warning:
            str="warning ";
            break;
        case error:
            str="error ";
            break;
        default:
            str="default";
            break;
        }
        str+=buf;
        str+=content;

        {
            //加锁
            std::lock_guard<std::mutex> lck(m_mutex);
            //添加日志到队列
            m_queue.emplace_back(str);
            //解锁
        }
        //条件变量 通知
        m_conVar.notify_one();
        /*先解锁再发条件变量通知
         *这样writeThread解除阻塞并重新加锁,保证writeLog阻塞住
         *这时候通知一定能接收到
         */
    }

切换文件

#define _MAX_LINE_COUNT 50
void init(){
        //文档打开时间
        m_openTime=MyTime::getCurTime();
        //赋初值
        char buf[100]="";
        sprintf(buf,"./%04d_%02d_%02d_%02d_%02d_%02d_%03d.log",
                m_openTime.year,m_openTime.month,m_openTime.day,
                m_openTime.hour,m_openTime.min,m_openTime.sec,
                m_openTime.msec);
        m_file=fopen(buf,"w");  //build路径
        m_quitFlag=false;
        //行数计数
        m_count=0;
    }
void writeThread(){
        while(!m_quitFlag){
            //加锁:lockguard RAII 自动解锁
            std::unique_lock<std::mutex> ulck(m_mutex);
            //循环避免错误通知
            while(!m_quitFlag && m_queue.empty()){
                //条件变量等通知
                m_conVar.wait(ulck);
            }
            //队列非空
            if(!m_queue.empty()){
                //队列中取出数据
                string res=m_queue.front();
                m_queue.pop_front();
                //判断是否切换文件
                MyTime curTime=MyTime::getCurTime();
                if(m_count>=_MAX_LINE_COUNT || curTime.day>m_openTime.day ||
                        curTime.month>m_openTime.month || curTime.year>m_openTime.year){
                    fclose(m_file);
                    init();
                }
                //写文件
                fwrite(res.c_str(),1,strlen(res.c_str()),m_file);
                //同步文件
                fflush(m_file);
                m_count++;
            }
        }
        //关闭文件
        fclose(m_file);
    }

搜索小例子

1)搜狗接口比较简单,替换query即可实现搜索,"搜狗搜索引擎 - 上网从搜狗开始雾霾&pid=AWNb5-7772"

2)从返回的数据中可以发现,data-url="海纳各领域知识,新一代百科全书 - 搜狗百科....可以得到雾霾词条的百科页面

3)词条的内容就是以 <div id='lemmaPage'> <div id='j-shareAbstract'>开始的一段文本, 直到</div>结束 那么剩下的就是根据正则表达式就可以筛选出最终的结果.

img

img编辑

用途:

  1. 可以搜索数据

  2. 获取天气

  3. 开发新闻控件

  4. 实现http服务器,客户端通过http请求完成通信

(一)封装NetRequest

#.pro
include(./NetRequest/NetRequest.pri)
INCLUDEPATH += ./NetRequest/
#.pri
QT +=network
//NetRequest.h
#ifndef NETREQUEST_H
#define NETREQUEST_H

#include <QObject>
#include<QNetworkAccessManager>
#include<QNetworkRequest>
#include<QNetworkReply>

class NetRequest : public QObject
{
    Q_OBJECT
signals:
    void SIG_dealResponse(int code, QByteArray& bt);    //不能多线程处理,使用&避免拷贝
public:
    explicit NetRequest(QObject *parent = nullptr);
    ~NetRequest();
    void sendGetRequest(QString url);
public slots:
    void slot_replyFinished(QNetworkReply* reply);
private:
    //这里使用的类QNetworkAccessManager 用于管理网络连接, QNetworkRequest 用来实现请求
    QNetworkAccessManager *network_manager  ; //发送请求
    QNetworkRequest *network_request;

};

#endif // NETREQUEST_H
//NetRequest.cpp
#include "netrequest.h"

NetRequest::NetRequest(QObject *parent) : QObject(parent)
{
    //创建类的对象
    network_manager = new QNetworkAccessManager();
    network_request = new QNetworkRequest();
    //写请求后得到回复的处理
    connect(network_manager, SIGNAL(finished(QNetworkReply*)),
            this, SLOT(slot_replyFinished(QNetworkReply*)) );
}

NetRequest::~NetRequest()
{
    network_manager->deleteLater();
    delete network_request;
}

void NetRequest::sendGetRequest(QString url)
{
    network_request->setUrl(QUrl(url));   // url是Qstring 类型, 表示要请求的url链接
    network_manager->get(*network_request);
}

void NetRequest::slot_replyFinished(QNetworkReply *reply)
{
    //无错误处理
    if(reply->error() == QNetworkReply::NoError)
    {
        QByteArray bytes = reply->readAll();  //获取字节
        //发送信号 处理数据
        Q_EMIT SIG_dealResponse(200,bytes); //用200表示成功
    }else{
        //处理错误
        qDebug()<<"处理错误";
        //发送信号
        QByteArray bt;
        Q_EMIT SIG_dealResponse(-1, bt ); //用-1表示失败

    }
    reply->deleteLater();
}

(二)发送请求

重点部分:发送请求、获取回复

包含头文件 
#include<QNetworkAccessManager> 
#include<QNetworkRequest> 
#include<QNetworkReply> 
我们需要用到的类为 
QNetworkAccessManager *network_manager  ; //发送请求搜狗搜索 
QNetworkRequest *network_request; 
//这里使用的类QNetworkAccessManager 用于管理网络连接, QNetworkRequest 用来实现请求 
创建类的对象 
network_manager = new QNetworkAccessManager(); 
network_request = new QNetworkRequest(); 
写请求后得到回复的处理 
connect(network_manager, SIGNAL(finished(QNetworkReply*)), 
this, SLOT(slot_replyFinished(QNetworkReply*)) ); 
发送url请求 
network_request->setUrl(QUrl(url));   // url是Qstring 类型, 表示要请求的url链接 
network_manager->get(*network_request); 
 
通过上面的步骤可以将请求发送, 一旦对方给出请求回复, 那么会发出finish信号, 进而有connect的槽函数
slot_replyFinished(QNetworkReply*)进行处理.

(三)解析,二次请求

void MainWindow::slot_dealResponse(int code, QByteArray &bt)
{
    if(code>0){
        qDebug()<<"搜狗搜索请求成功";
//        QFile file("./test.html");
//        if(file.open(QIODevice::WriteOnly)){
//            file.write(bt);
//            file.close();
//        }
        //解析相应数据 data-url="https://baike.sogou.com/
        QString result(bt);  //转化为字符串
        //提取
        int idx = result.indexOf( QRegExp("data-url=\"https*://baike.sogou.com") );
        QString str = result.mid( idx );

        int beginIdx = str.indexOf(QRegExp("https*://") );
        int endIdx = str.indexOf("\">");
        str = str.mid( beginIdx   , endIdx - beginIdx );

        qDebug() << str;
        //发送第二个链接请求
        m_request2->sendGetRequest(str);
    }
    else{
        qDebug()<<"搜狗搜索请求失败";
    }
}

(四)提取想要的内容到界面

void MainWindow::slot_getBaikeRes(int code, QByteArray &bt)
{
    if(code>0){
        qDebug()<<"百科请求成功";
        //解析,提取想要的内容到界面
        QString result(bt);  //转化为字符串
        //提取  j-shareAbstract
        int idx = result.indexOf( QRegExp("<div id=\"j-shareAbstract\" style=\"display:none\">") );
        QString str = result.mid( idx );
        int beginIdx = QString("<div id=\"j-shareAbstract\" style=\"display:none\">").length();
        int endIdx = str.indexOf("</div>");
        str = str.mid( beginIdx ,  endIdx - beginIdx );
        qDebug() << str;
        ui->te_content->setText(str);
    }
    else{
        qDebug()<<"百科请求失败";
    }
}

邮箱验证

image-20240804150341141

(一)获取QQ邮箱授权码

[获取QQ邮箱授权码]  https://blog.csdn.net/xiaowei1028/article/details/118305788 

(二)导入MailApi

 

(三)服务器

    CSmtpMail m_mai;
    std::map<std::string,int> m_mapMailToCode;
{
    //服务器收到信息
    //服务器 生成一个随机验证码,同时将用户信息和验证码绑定,map
    int code=123678;
    m_mapMailToCode[mail.toStdString()]=code;    //map中key为邮箱,value为验证码
    //发送邮件 通过 SMTP协议,利用QQ邮箱发送
    m_mai.SendMail(mail.toStdString(),to_string(code));
}
{
    //服务器:根据信息,查map,找到对应验证码,比对:一致,返回验证成功
    //服务器解析
    if(m_mapMailToCode[mail.toStdString()]>0){
        int code=m_mapMailToCode[mail.toStdString()];
        if(recvCode.toInt()==code){
            QMessageBox::about(this,"info","验证成功");
        }
        else{
            QMessageBox::about(this,"验证失败","验证码错误");
        }
    }
    else{
        QMessageBox::about(this,"tip","未知邮箱");
    }
}

(四)客户端

void Dialog::on_pb_getcode_clicked()    //获取验证码
{
    //获取信息
    QString tel=ui->lb_tel->text();
    QString mail=ui->le_mail->text();
    //过滤todo
    //手机和邮箱发送到服务器
}
void Dialog::on_pb_commit_clicked()
{
    //用户将邮件收到的验证码填入界面
    QString mail=ui->le_mail->text();
    QString recvCode=ui->le_code->text();
    //提交验证码和信息
}
SMTP的架构与连接
SMTP架构

image-20240804161522595

image-20240804161532659

SMTP端口与服务器

25端口

SMTP连接方式

image-20240804161723904

image-20240804161733427

1)建立会话EHELO

image-20240804161851998

image-20240804161903152

    WSADATA WSAData;
    struct sockaddr_in their_addr = { 0 };
    WSAStartup(MAKEWORD(2, 2), &WSAData);
    memset(&their_addr, 0, sizeof(their_addr));

    their_addr.sin_family = AF_INET;
    their_addr.sin_port = htons(m_serverPort);    // 一般是25端口不需要改
    hostent* hptr = gethostbyname(m_smtpServer.c_str());          // 端口和服务器
    memcpy(&their_addr.sin_addr.S_un.S_addr, hptr->h_addr_list[0], hptr->h_length);
    printf("IP of %s is : %d:%d:%d:%d\n",
        m_smtpServer.c_str(),
        their_addr.sin_addr.S_un.S_un_b.s_b1,
        their_addr.sin_addr.S_un.S_un_b.s_b2,
        their_addr.sin_addr.S_un.S_un_b.s_b3,
        their_addr.sin_addr.S_un.S_un_b.s_b4);

    // 连接邮件服务器,如果连接后没有响应,则2ms后重新连接
    sockfd = ConnectServer((struct sockaddr *)&their_addr);
    memset(rbuf, 0, 1500);
    while (recv(sockfd, rbuf, 1500, 0) == 0)
    {
        cout << "reconnect..." << endl;
        Sleep(2);
        sockfd = ConnectServer((struct sockaddr *)&their_addr);
        memset(rbuf, 0, 1500);
    }

    cout << rbuf << endl;

    // EHLO
    sprintf_s(buf, 1500, "EHLO HYL-PC\r\n");
    send(sockfd, buf, strlen(buf), 0);
    memset(rbuf, 0, 1500);
    recv(sockfd, rbuf, 1500, 0);
    cout << "EHLO REceive: " << rbuf << endl;

2)身份认证AUTH

image-20240804162118627

image-20240804162209908

 // AUTH LOGIN
    sprintf_s(buf, 1500, "AUTH LOGIN\r\n");
    send(sockfd, buf, strlen(buf), 0);
    memset(rbuf, 0, 1500);
    recv(sockfd, rbuf, 1500, 0);
    cout << "Auth Login Receive: " << rbuf << endl;

    // USER
    sprintf_s(buf, 1500, m_emailSenderAddr.c_str());//你的邮箱账号
    memset(login, 0, 128);
    EncodeBase64(login, buf, strlen(buf));	//base64编码
    sprintf_s(buf, 1500, "%s\r\n", login);
    send(sockfd, buf, strlen(buf), 0);
    cout << "Base64 UserName: " << buf << endl;
    memset(rbuf, 0, 1500);
    recv(sockfd, rbuf, 1500, 0);
    cout << "User Login Receive: " << rbuf << endl;

    // PASSWORD
    sprintf_s(buf, 1500, m_emailSenderPasswd.c_str());//你的邮箱密码  授权码
    memset(pass, 0, 128);
    EncodeBase64(pass, buf, strlen(buf));
    sprintf_s(buf, 1500, "%s\r\n", pass);
    send(sockfd, buf, strlen(buf), 0);
    cout << "Base64 Password: " << buf << endl;

    memset(rbuf, 0, 1500);
    recv(sockfd, rbuf, 1500, 0);
    cout << "Send Password Receive: " << rbuf << endl;

image-20240804162351001

3)发送邮件信封MAIL FROM、RCPT TO

   // MAIL FROM
    sprintf_s(buf, 1500, "MAIL FROM: <%s>\r\n",m_emailSenderAddr.c_str());  //此处要和发邮件的邮箱保持一致
    send(sockfd, buf, strlen(buf), 0);
    memset(rbuf, 0, 1500);
    recv(sockfd, rbuf, 1500, 0);
    cout << "set Mail From Receive: " << rbuf << endl;

    // RCPT TO 第一个收件人
    sprintf_s(buf, 1500, "RCPT TO:<%s>\r\n", m_emailReceiverAddr.c_str() );
    send(sockfd, buf, strlen(buf), 0);
    memset(rbuf, 0, 1500);
    recv(sockfd, rbuf, 1500, 0);
    cout << "Tell Sendto Receive: " << rbuf << endl;

4)发送邮件内容DATA

    // DATA 准备开始发送邮件内容
    sprintf_s(buf, 1500, "DATA\r\n");
    send(sockfd, buf, strlen(buf), 0);
    memset(rbuf, 0, 1500);
    recv(sockfd, rbuf, 1500, 0);
    cout << "Send Mail Prepare Receive: " << rbuf << endl;

    // 发送邮件内容,\r\n.\r\n内容结束标记
    sprintf_s(buf, 1500, "%s\r\n.\r\n", body);
    send(sockfd, buf, strlen(buf), 0);
    memset(rbuf, 0, 1500);
    recv(sockfd, rbuf, 1500, 0);
    cout << "Send Mail Receive: " << rbuf << endl;

5)关闭会话QUIT

    // QUIT
    sprintf_s(buf, 1500, "QUIT\r\n");
    send(sockfd, buf, strlen(buf), 0);
    memset(rbuf, 0, 1500);
    recv(sockfd, rbuf, 1500, 0);
    cout << "Quit Receive: " << rbuf << endl;

image-20240804162618041

每个邮件就是一个连接(短连接)

JSON实现

Java Script Object Notation,js对象表示法。是一对一对键值对形成的字符串

JSON是一种数据封装,可以用于网络传输

{
    "key":"value",
    "hobby":["唱","跳","rap","篮球"],
    "data":[
        {
            "name":"张三",
            "age":15
        },
        {
            "name":"李四",
            "age":18
        },
    ],
    "status":true,
    "num":null
}	
key值只能是字符串
value值可以是bool、数组(多个JSON对象,或多个字符串)、字符串、整型、JSON对象、空值

QT可以使用QT自带的JSON去实现,linux可以使用开源的json.c

JSON相比结构体的优势?

  1. 结构体有字节对齐问题,在不同的环境下可能会不同(pc、web、C、java)

  2. 结构体无法对空值进行描述,JSON可以避免一些异常情况(null 0)

JSON使用需要注意的事情:

  1. 会传输多余的数据

  2. 传输文件内容、音频视频等会带有0的数据,会产生问题(字符串遇到\0结束)

    对有0的数据,可以通过base64编码

http请求,protobuf

添加共享库:

打开共享库路径配置文件

sudo vi /etc/ld.so.conf

最后一行添加lib路径

image-20240808155831900

更新共享库加载路径

sudo ldconfig -v

网络抓包 异常处理

wireshark

[wireshark抓包及常用协议分析]  https://blog.csdn.net/qq_30036471/article/details/102776661 

异常处理

遇到问题,返回错误码

try{
    if(...)
        throw std::runtime_error("xxx error");
}catch(std::exception &exp){
    cout<<exp.what()<<endl;	//查看错误打印
}

这样就可以在遇到问题时,不用跳出程序也能得到错误信息

class MyException:public std::exception{
public:
    MyException(const std::string &str):m_str(str){}
    virtual ~MyException(){}
    virtual const char* what() const noexcept{
        return m_str.c_str();
    }
    std::string m_str;
};
void Dialog::on_pb_equal_clicked()
{
    int num1=ui->le_1->text().toInt();
    int num2=ui->le_2->text().toInt();
    int res=0;

    try {
        if(num2==0){
            //throw std::runtime_error("除数不能为0");
            throw MyException("除数不能为0");
        }
        res=num1/num2;
        ui->le_res->setText(QString::number(res));
    }  catch (std::exception& exp) {
        qDebug()<<exp.what();
    }
    qDebug()<<"quit";
}

断言

	assert();	//断言,参数是一个判断语句。如果判断为假,会中止程序,弹出弹窗提示错误
    int num1=ui->le_1->text().toInt();
    int num2=ui->le_2->text().toInt();
    assert(num2!=0);    //为假,弹窗中止程序
    qDebug()<<"ignore";
    int res=num1/num2;
    ui->le_res->setText(QString::number(res));

异常处理与使用错误码有什么区别?

  1. 异常处理更专业,可以简化代码流程,便于开发

  2. 异常处理可以嵌套,通过另一个函数throw错误,简化代码流程

    平时我们的代码是通过返回错误码以及errno配合使用,代码较复杂

int call(int num1,int num2){
    int res=0;
    if(num2==0){
        throw MyException("除数不能为0");
    }
    res=num1/num2;
    return res;
}
{
	int num1=ui->le_1->text().toInt();
    int num2=ui->le_2->text().toInt();
    int res=0;
	try{
        res=call(num1,num2);
        ui->le_res->setText(QString::number(res));
    }catch (std::exception& exp) {
        qDebug()<<exp.what();
    }
}
内存泄漏

导致内存泄漏的方式

  1. 申请的堆空间没有主动回收

  2. 打开的文件没有关闭

  3. 打开的网络连接没有close()

  4. 句柄没有关闭

  5. 资源未关闭,包括ffmpeg库 SDL库 网络库 等没有关闭

  6. 线程没有退出

内存泄漏检测

valgrind

sudo apt-get install valgrind
g++ -std=c++11 xx.cpp -g -o app
valgrind --leak-check=full --show-leak-kinds=all ./app
#include <iostream>
#include <stdlib.h>
#include <stdio.h>
using namespace std;
int main(){
	int *p=new int[10];
	FILE* pFile=fopen("test.txt","w");
	fwrite("hello",1,5,pFile);
}

image-20240814103841212

核心转储

linux下core dump文件,在发生核心转储时,gdb调试可以把段错误存储在核心错误文件

#include <iostream>
using namespace std;
int main(){
	int* p =nullptr;
	*p=10;
}
g++ -std=gnu++11 xx.cpp -g -o app
gdb ./app
#当发生段错误,可以使用命令将错误保存在文件中
gcore	#或:generate-core-file
#退出后,可以查看保存的文件
gdb -q ./app ./core.线程号

image-20240814105031235

在当前目录生成核心转储文件

ulimit -c unlimited	#发生段错误会生成一个core文件

image-20240814105144138

加解密

IM及时通讯软件,聊天的内容需要加密

不加密,任何人都可以拦截

如何加密?

密码:MD5(信息摘要算法),将明文转换为密文(不可逆),通过哈希算法,转换为128的二进制数,平时使用32位16进制的字符串来表示。写入数据库中,即使数据库被攻破,也不会立即直到内容是什么。但是可以利用截获的包,直接登录

HTTPS加密流程

1. AES对称加密

双方使用一个约定好的随机生成的密钥,后续的通信中,信息发送方都使用密钥对信息加密,而信息接收方通过同样的密钥对信息解密。

但是只要密钥泄漏或被破解,中间人就能解密通信了

2. RAS非对称加密

明文既可以用公钥加密,用私钥解密;也可以用私钥加密,用公钥解密。

一般来说,双方公开双方的公钥,使用对方的公钥进行明文加密,收到信息后用自己的私钥进行解密,就可以安全的进行信息通信。但是RSA的过程十分的缓慢,所以还是要用对称进行通信

方法:

A发布公钥A,B收到公钥A后,生成一个用于对称加密的密钥key,用公钥A对密钥key加密,将密文发给A。A收到密文后,用私钥A解密,就能得到密钥key。这样双方就可以用密钥key进行对称加密通信了

但是如果中间人把公钥A,换成了自己的公钥C,发给B。B发回消息就能被中间人用私钥C解密,获取密钥key,然后再用公钥A加密伪装发给A。这样双方都不知情的情况下,中间人得到了密钥key

3.CA机构(权威的证书颁发机构)

img

比对两次前面是否一致,一致证明没有发生篡改。证书是三方机构保证的,假证书无法通过验证

1.作为服务端的小红,首先把自己的公钥发给证书颁发机构,向证书颁发机构申请证书

2.证书颁发机构自己也有一对公钥私钥。机构利用自己的私钥来加密Key1,并且通过服务端网址等信息生成一个证书签名,证书签名同样经过机构的私钥加密。证书制作完成后,机构把证书发送给了服务端小红

3.当小灰向小红请求通信的时候,小红不再直接返回自己的公钥,而是把自己申请的证书返回给小灰。

4.小灰收到证书以后,要做的第一件事情是验证证书的真伪。需要说明的是,各大浏览器和操作系统已经维护了所有权威证书机构的名称和公钥。所以小灰只需要知道是哪个机构颁布的证书,就可以从本地找到对应的机构公钥,解密出证书签名。

接下来,小灰按照同样的签名规则,自己也生成一个证书签名,如果两个签名一致,说明证书是有效的。

验证成功后,小灰就可以放心地再次利用机构公钥,解密出服务端小红的公钥Key1

5.像之前一样,小灰生成自己的对称加密密钥Key2,并且用服务端公钥Key1加密Key2,发送给小红。

6.最后,小红用自己的私钥解开加密,得到对称加密密钥Key2。于是两人开始用Key2进行对称加密的通信。

image-20240818162658819

SSL/TLS 协议

C/S下的加解密

C/S 和 B/S架构不同:

  1. 可以通过官网下载APP,有文件的验证(MD5 或 SHA,信息一致性验证),确保APP一定是官方的

  2. 程序运行时,可以携带数据。CA证书保证客户端收到的服务器公钥一定来自服务器,但是只要通过程序运行携带数据,就可以直接获取公钥,而不需要网络传输获取

C/S架构加密,不需要CA,直接从步骤5开始,完成加密流程:

  1. 通过随机数,生成AES加密的密钥key

  2. 通过服务器公钥,key加密后传给服务器

  3. 服务器通过非对称的私钥解密,得到AES加密的密钥key

  4. 此时,客户端服务器就可以通过AES密钥完成加密通信了

为什么还需要非对称加密?

AES是随机固定的。若AES是动态的,例如通过时间戳换算得到,这样的密钥是可以通过反编译破解的。需要非对称把密文保护起来。此时服务器的公钥和私钥是固定的,想要破解需要花费很多时间,此时可以定期更新服务器的RSA密钥对,通过版本更新变换,旧版本软件作废。

代码实现

使用了cryptopp开源的加解密库

windows下有编译好的,5.12.11版本mingw32,能直接包含.pri使用

linux下

让共享库生效

image-20240818175209208

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值