重点:怎样正确的使用QThread类(注:包括推荐使用QThread线程的新方法QObject::moveToThread)
背景描述:
以前,继承 QThread 重新实现 run() 函数是使用 QThread唯一推荐的使用方法。这是相当直观和易于使用的。但是在工作线程中使用槽机制和Qt事件循环时,一些用户使用错了。Qt 核心开发人员Bradley T. Hughes, 推荐使用QObject::moveToThread 把它们移动到线程中。不幸的是, 以用户反对这样使用。Olivier Goffart, 前Qt 核心开发人之一, 告诉这些用户你们不这样做就错了。最终这俩种用法我们都在QThread的文档中发现 。
QThread::run() 是线程的入口点
从Qt文档中我们可以看到以下内容:
A QThread instance represents a thread and provides the means to start() a thread, which will then execute the reimplementation of QThread::run(). The run() implementation is for a thread what the main() entry point is for the application.
Usage 1-0
在新的线程中执行一些代码,继承QThread 重新实现 run()函数接口。
For example
- #include <QtCore>
- class Thread : public QThread
- {
- private:
- void run()
- {
- qDebug()<<"From worker thread: "<<currentThreadId();
- }
- };
- int main(int argc, char *argv[])
- {
- QCoreApplication a(argc, argv);
- qDebug()<<"From main thread: "<<QThread::currentThreadId();
- Thread t;
- QObject::connect(&t, SIGNAL(finished()), &a, SLOT(quit()));
- t.start();
- return a.exec();
- }
结果输出如下:
From main thread: 0x15a8
From worker thread: 0x128c
Usage 1-1
正因QThread::run() 是线程的入口, 所以很容易的理解它们, 并不是所有的代码都在run()接口中被直接调用而不在工作线程中被执行。
接下来的例子中,成员变量 m_stop
在 stop() 和 run()都可被访问到。考虑到前者将在主线程执行,后者在工作线程执行,互斥锁或其它操作是有必要的。
- #if QT_VERSION>=0x050000
- #include <QtWidgets>
- #else
- #include <QtGui>
- #endif
- class Thread : public QThread
- {
- Q_OBJECT
- public:
- Thread():m_stop(false)
- {}
- public slots:
- void stop()
- {
- qDebug()<<"Thread::stop called from main thread: "<<currentThreadId();
- QMutexLocker locker(&m_mutex);
- m_stop=true;
- }
- private:
- QMutex m_mutex;
- bool m_stop;
- void run()
- {
- qDebug()<<"From worker thread: "<<currentThreadId();
- while () {
- {
- QMutexLocker locker(&m_mutex);
- if (m_stop) break;
- }
- msleep();
- }
- }
- };
- #include "main.moc"
- int main(int argc, char *argv[])
- {
- QApplication a(argc, argv);
- qDebug()<<"From main thread: "<<QThread::currentThreadId();
- QPushButton btn("Stop Thread");
- Thread t;
- QObject::connect(&btn, SIGNAL(clicked()), &t, SLOT(stop()));
- QObject::connect(&t, SIGNAL(finished()), &a, SLOT(quit()));
- t.start();
- btn.show();
- return a.exec();
- }
结果输出如下:
From main thread: 0x13a8
From worker thread: 0xab8
Thread::stop called from main thread: 0x13a8
你可以看到Thread::stop() 函数是在主线程中被执行的。
Usage 1-2 (错误的使用方式)
以上的例子很容易明白,但它不是那么直观当事件系统(或队列)中引进工作线程时。
例子如下, 我们应该做些什么,如果我们想在工作线程中周期性的做些动作?
- 在Thread::run()中创建一个QTimer
- 将超时信号连接到线程中的槽函数上
- #include <QtCore>
- class Thread : public QThread
- {
- Q_OBJECT
- private slots:
- void onTimeout()
- {
- qDebug()<<"Thread::onTimeout get called from? : "<<QThread::currentThreadId();
- }
- private:
- void run()
- {
- qDebug()<<"From worker thread: "<<currentThreadId();
- QTimer timer;
- connect(&timer, SIGNAL(timeout()), this, SLOT(onTimeout()));
- timer.start();
- exec();
- }
- };
- #include "main.moc"
- int main(int argc, char *argv[])
- {
- QCoreApplication a(argc, argv);
- qDebug()<<"From main thread: "<<QThread::currentThreadId();
- Thread t;
- t.start();
- return a.exec();
- }
乍看起来代码没什么问题。当线程开始执行时, 我们在当前线程的事件处理中启动了一个定时器。我们将 onTimeout()
连接到超时信号上。此时我们希望它在线程中执行?
但是执行结果如下:
From main thread: 0x13a4
From worker thread: 0x1330
Thread::onTimeout get called from?: 0x13a4
Thread::onTimeout get called from?: 0x13a4
Thread::onTimeout get called from?: 0x13a4
Oh, No!!! 它为什么在主线程中被调用了!
是不是很有趣?(接下来我们将要讨论这是为什么)
如何解决这个问题
为了使槽函数工作在线程中, 有人尝试在connect()函数中传入参数 Qt::DirectConnection
- connect(&timer, SIGNAL(timeout()), this, SLOT(onTimeout()), Qt::DirectConnection);
还有人尝试在线程构造函数中添加如下功能。
- moveToThread(this)
它们都会如期望的工作吗. 但是 …
第二个用法的是错误的,
尽管看起来它工作啦,但很令人费解,这不是QThread 设计的本意(QThread 中所有实现的函数是被创建它的线程来调用的,不是在线程中)
实际上,根据以上表述,第一个方案是错误的。onTimeout() 是线程对象的一个成员函数,会被创建它的线程来调用。
它们都是错误的使用方法?!我们该如何做呢?
Usage 1-3
因为没有一个线程类的成员是设计来被该线程调用的。所以如果我们想使用槽函数必须创建一个独立的工作对象。
- #include <QtCore>
- class Worker : public QObject
- {
- Q_OBJECT
- private slots:
- void onTimeout()
- {
- qDebug()<<"Worker::onTimeout get called from?: "<<QThread::currentThreadId();
- }
- };
- class Thread : public QThread
- {
- Q_OBJECT
- private:
- void run()
- {
- qDebug()<<"From work thread: "<<currentThreadId();
- QTimer timer;
- Worker worker;
- connect(&timer, SIGNAL(timeout()), &worker, SLOT(onTimeout()));
- timer.start();
- exec();
- }
- };
- #include "main.moc"
- int main(int argc, char *argv[])
- {
- QCoreApplication a(argc, argv);
- qDebug()<<"From main thread: "<<QThread::currentThreadId();
- Thread t;
- t.start();
- return a.exec();
- }
结果如下:
From main thread: 0x810
From work thread: 0xfac
Worker::onTimeout get called from?: 0xfac
Worker::onTimeout get called from?: 0xfac
Worker::onTimeout get called from?: 0xfac
问题解决啦!
尽管运行的很好,但是你会注意到,当工作线程中运行在事件循环 QThread::exec()
中时,在QThread::run() 函数接口中没有执行自身相关的事务。
所以我们是否可以将对象的创建从QThread::run()中移出, 此时, 槽函数是否依旧会被QThread::run()调用?
Usage 2-0
如果我们只想使用QThread::exec(), 默认情况下会被QThread::run() 调用, 那不需要子类化QThread。
- 创建一个工作对象
- 建立信号与槽的连接
- 将工作对象移至子线程中
- 启动线程
- #include <QtCore>
#include <QUdpSocket>- class Worker : public QObject
- {
- Q_OBJECT
public:
void Work()
{
}
private:
QUdpSocket *socket;- private slots:
- void onTimeout()
- {
- qDebug()<<"Worker::onTimeout get called from?: "<<QThread::currentThreadId();
- }
- };
- #include "main.moc"
- int main(int argc, char *argv[])
- {
- QCoreApplication a(argc, argv);
- qDebug()<<"From main thread: "<<QThread::currentThreadId();
- QThread t;
- QTimer timer;
- Worker worker;
- QObject::connect(&timer, SIGNAL(timeout()), &worker, SLOT(onTimeout()));
- timer.start();
- timer.moveToThread(&t);
- worker.moveToThread(&t);
- t.start();
- return a.exec();
- }
结果是:
From main thread: 0x1310
Worker::onTimeout get called from?: 0x121c
Worker::onTimeout get called from?: 0x121c
Worker::onTimeout get called from?: 0x121c
正如所预料的,槽函数没有在主线程中执行。
在此例中,定时器和工作对象都被移至子线程中,实际上,将定时器移至子线程中是没有必要的。
Usage 2-1
在上面的例子当中将 timer.moveToThread(&t);
这一行注释掉。
- int main(int argc, char *argv[])
- {
- QCoreApplication a(argc, argv);
- qDebug()<<"From main thread: "<<QThread::currentThreadId();
- QThread t;
- QTimer timer;
- Worker worker;
- QObject::connect(&timer, SIGNAL(timeout()), &worker, SLOT(onTimeout()));
- timer.start();
- // timer.moveToThread(&t);
- worker.moveToThread(&t);
- t.start();
- return a.exec();
- }
不同之处如下:
在上面的例子中,
- 信号
timeout()
是由子线程发送出去的。 - 定时器和工作对象是在同一线程当中的,它们的连接方式直接连接。
- 槽函数的调用和信号的触发是在同一线程中的。
在本例中,
- 信号
timeout()
是由主线程发出的。 - 定时器和工作对象是在不同的线程中,它们的连接方式队列连接。
- 槽函数是在子线程中被调用。
- 由于队列连接机制的存在,可以安全的将信号和槽函数在不同的线程中连接起来。如果所有的跨线程通信都通过队列连接方式,那么多线程的互斥防范机制将不在需要。
总结
- 子类化QThread ,实现 run()函数接口是很直观的,也存在很多不错的方法去子类化QThread,但是在工作线程中处理事件处理并不是一件简单的事。
- 在事件处理存在时使用对象时将它们移至线程中时很简单的,这样将事件处理和队列连接的细节给隐藏了。
QObject::thread()
可以查询一个QObject
的线程依附性,通过QThread::currentThread()可以查询当前运行所属线程,都非常有用。
- QThread *thread;
- thread=new QThread(this);
- qDebug()<<"1:"<<commUdpClient->thread();
- commUdpClient->moveToThread(thread);
- thread->start();
- qDebug()<<"2:"<<commUdpClient->thread();
输出:
说明对象commUdpClient确实发生所在线程的变化。但是值得注意的是,commUdpClient对象中的
- QUdpSocket *socket;
QUdpSocket *socket2;
QUdpSocket socket3; 前两个指针在构造函数里和槽函数里初始化,所属线程是有很大区别的。如下面代码所示,在构造函数里初始化socket2,意味着属于主线程,不管commUdpClient是否移到子线程,它还是属于主线程;而socket是在commUdpClient对象移到子线程后,再在槽函数中初始化,则属于子线程;socket3默认在构造函数里初始化,即使构造函数没有显式初始化,但是也属于主线程,其它如int、char等基础数据变量也是属于主线程的。所以在利用moveToThread方式使用多线程的情况下,commUdpClient类声明中类类型对象建议都声明为指针,然后都在某个槽函数中初始化,而基础数据变量如int、char等建议声明为private,如果需要改变则利用属性的方式get、set来改变,实在需要使用public,则需要加锁,不然会出错。

所以要求A的初始化过程放到某个槽函数中进行,包括connect的过程,然后由线程ThreadParent中调用A的对象C通过信号的方式触发该槽。A其它的槽要想在对象C中调用也需要通过信号的方式,如果通过直接调用A.XX()方式调用槽函数或者其它函数,则仍旧是在线程ThreadParent中运行,非在线程ThreadSon中运行。切记切记,具体看以下代码:
对象B的源码.h
- //CommUdpClient.h
- #include <QObject>
- #include <QUdpSocket>
- class CommUdpClient : public QObject
- {
- Q_OBJECT
- public:
- explicit CommUdpClient(QObject *parent = );
- ~CommUdpClient();
- QUdpSocket *socket;
QUdpSocket *socket2;
QUdpSocket socket3;- private:
- QHostAddress serverAddress;
- quint16 serverPort;
- public:
- //发送报文
- void send(const QByteArray &msg);
- signals:
- //接收完报文后触发的信号,用于上层触发相应的槽,获得报文
- void sigRevedMsg(QByteArray msg,const QStringList ¶s);
- public slots:
- //报文发送完成信号bytesWritten对应的槽
- void sltSent(qint64 bytes);
- //接收报文,对应信号readyRead的槽
- QByteArray sltReceive();
- //通讯参数设置,打开连接
- void sltOpenConnect(const QStringList ¶s);
- };
对象B的源码.CPP
- #include "commudpclient.h"
- #include <qstringlist.h>
- #include <QMetaEnum>
- #include <QDebug>
- #include <QThread>
- CommUdpClient::CommUdpClient(QObject *parent) :
- QObject(parent)
- {
- qDebug()<<"udp current thread 1:"<<QThread::currentThread();
- //错误
- //socket=new QUdpSocket;
- //connect(socket,SIGNAL(bytesWritten(qint64)),this,SLOT(sltSent(qint64)));
- //connect(socket,SIGNAL(readyRead()),this,SLOT(sltReceive()));
socket2=new QUdpSocket;
qDebug()<<"socket2 thread:"<<socket2->thread();- }
- CommUdpClient::~CommUdpClient()
- {
- if(socket!=NULL)
- {
- if(socket->isOpen())
- {
- socket->close();
- }
- delete socket;
- socket=NULL;
- }
- }
- void CommUdpClient::sltOpenConnect(const QStringList ¶s)
- {
- qDebug()<<"udp current thread 2:"<<QThread::currentThread();
- //正确
- socket=new QUdpSocket();//这里加this和不加this是有区别的
这是打印出来的socket的parent,是有区别的,不过这里的CommUdpClient也是顶层的,无parent的,不然是无法移动到子线程中的,会报错
,具体在对象B的源码中描述。
- connect(socket,SIGNAL(bytesWritten(qint64)),this,SLOT(sltSent(qint64)));
- connect(socket,SIGNAL(readyRead()),this,SLOT(sltReceive()));
//测试
qDebug()<<"socket thread:"<<socket->thread();
qDebug()<<"socket2 thread:"<<socket2->thread();
qDebug()<<"scoket3 thread:"<<socket3.thread();
qDebug()<<this<<", thread:"<<this->thread();- serverAddress.setAddress(paras[]);
- serverPort=paras[].toUShort();
- socket->bind(paras[].toUShort());
- }
- void CommUdpClient::send(const QByteArray &msg)
- {
qDebug()<<"udp send thread:"<<QThread::currentThread();- if(!serverAddress.isNull())
- {
- socket->writeDatagram(msg,serverAddress,serverPort);
- qDebug()<<"client send end";
- }
- }
- void CommUdpClient::sltSent(qint64 bytes)
- {
- }
- QByteArray CommUdpClient::sltReceive()
- {
- qDebug()<<"server start receive...";
- while (socket->hasPendingDatagrams()) {
- QByteArray datagram;
- datagram.resize(socket->pendingDatagramSize());
- QHostAddress sender;
- quint16 senderPort;
- socket->readDatagram(datagram.data(), datagram.size(),
- &sender, &senderPort);
- qDebug()<<datagram;
- QStringList sl;
- sl.append(sender.toString());
- sl.append(QString::number(senderPort));
- emit sigRevedMsg(datagram,sl);
- return datagram;
- }
- }
对象A的源码.h
- #ifndef COMMUNICATION_H
- #define COMMUNICATION_H
- #include <QObject>
- #include <QThread>
- #include <qstringlist.h>
- #include "commtcpclient.h"
- class Communication : public QObject
- {
- Q_OBJECT
- public:
- explicit Communication(QObject *parent = );
- virtual ~Communication();
- public:
- CommUdpClient *commUdpClient;
- QThread *thread;
- public:
- void readData();
- void writeData();
- signals:
- void sigThreadConnect(const QStringList ¶s);
- public slots:
- void sltRevedMsg(QByteArray msg,const QStringList ¶s);
- };
- #endif // COMMUNICATION_H
对象B的源码.cpp
- #include "communication.h"
- #include <QCoreApplication>
- #include <QDebug>
- #include <QTime>
- Communication::Communication(QObject *parent) :
- QObject(parent)
- {
- qDebug()<<"current thread:"<<QThread::currentThread();
- commUdpClient=new CommUdpClient();//这里不能加this,不然是不允许移到子线程中,报错
- connect(commUdpClient,SIGNAL(sigRevedMsg(QByteArray,QStringList)),this,SLOT(sltRevedMsg(QByteArray,QStringList)));
- connect(this,SIGNAL(sigThreadConnect(QStringList)),commUdpClient,SLOT(sltOpenConnect(QStringList)));
- thread=new QThread(this);
qDebug()<<"1:"<<commUdpClient->thread();- commUdpClient->moveToThread(thread);
qDebug()<<"2:"<<commUdpClient->thread();
thread->start();
qDebug()<<"3:"<<commUdpClient->thread();- }
- Communication::~Communication()
- {
- delete commUdpClient;
- thread->quit();
- delete thread;
- }
- void Communication::readData()
- {
- QString sz="192.168.1.186:1234:1235";
- QStringList sl=sz.split(':');
- emit sigThreadConnect(sl);//正确用法
- //commUdpClient->send("error");//不正确的用法,这个是直接在对象B的线程中运行的
- }
- void Communication::writeData()
- {
- qDebug()<<"xxxx";
- }
- void Communication::sltRevedMsg(QByteArray msg)
- {
- qDebug()<<"communication end,receive msg:"<<msg;
- }
- void Communication::sltRevedMsg(QByteArray msg, const QStringList ¶s)
- {
- }
远行结果:
- //commUdpClient->send("error");//不正确的用法,这个是直接在对象B的线程中运行的
具体的参考资料:http://blog.csdn.net/sydnash/article/details/7425947 一种使用QThread线程的新方法QObject::moveToThread
http://blog.csdn.net/lainegates/article/details/9701215
http://mobile.51cto.com/symbian-268690_1.htm
关于: QObject: Cannot create children for a parent that is in a different thread错误
参考:http://blog.chinaunix.net/uid-26808060-id-3355816.html
- class TcpComm:public QThread
- {
- Q_OBJECT
- public:
- TcpComm(const QString &iAddrStr, quint16 iPort);
- ~TcpComm();
- ........
- private:
- .......
- TcpClient*mTcpClient;
- };
- TcpComm::TcpComm(const QString &iAddrStr, quint16 iPort):mAddr(iAddrStr), mPort(iPort)
- {
- mIsStop = false;
- mTcpClient = new TcpClient();
- start();
- }
- 以上程序在运行时报QObject: Cannot create children for a parent that is in a different thread错误。
- 将原构造函数中的mTcpClient = new TcpClient();放到run()中问题解决。
- TcpComm::TcpComm(const QString &iAddrStr, quint16 iPort):mAddr(iAddrStr), mPort(iPort)
- {
- mIsStop = false;
- start();
- }
- void TcpComm::run()
- {
- mTcpClient = new TcpClient();
- ........
- }
- 查了查,原因应该是,在QThread中定义的所有东西都属于创建该QThread的线程。所以在构造函数中初始化的mTcpClient应该是属于父线程的,那么在run中调用时就属于跨线程调用。所以把mTcpClient放到run中初始化就属于线程的了,调用时就不会出现跨线程调用的问题。
另外:QThread中写的所有函数都应该在创建它的线程中调用,而不是开启QThread的线程
其它参考资料:http://blog.csdn.net/zhangbinsijifeng/article/details/52299926 qt中的线程 拥有权 一个对象属于哪个线程
http://blog.csdn.net/an505479313/article/details/50351745 QThread使用——关于run和movetoThread的区别
http://blog.csdn.net/dbzhang800/article/details/6554104 Qt 线程基础(QThread、QtConcurrent等)
重点:怎样正确的使用QThread类(注:包括推荐使用QThread线程的新方法QObject::moveToThread)的更多相关文章
- 重点:怎样正确的使用QThread类(很多详细例子的对比,注意:QThread 中所有实现的函数是被创建它的线程来调用的,不是在线程中)good
背景描述: 以前,继承 QThread 重新实现 run() 函数是使用 QThread唯一推荐的使用方法.这是相当直观和易于使用的.但是在工作线程中使用槽机制和Qt事件循环时,一些用户使用错了.Qt ...
- pyqt5-多线程QThread类
要实现多线程,我们要先继承QThread类并重新实现其中的run()函数,也就是说把耗时的操作放入run()函数中 import sys from PyQt5.QtCore import Qt, QT ...
- QThread::wait(),一直以来我以为它阻塞的是QThread对象,可是我现在明白,原来阻塞的是这个对象所在的线程(通常是主线程)——所有事情源于 QThread 的事件循环——如果使用继承QThread这一方法,QThread::quit()没有效果,因为这个线程根本就不需要事件循环
近日,使用QThread,一些问题百思不得其解,看过大牛的文章,恍然大悟啊. 原文 http://hi.baidu.com/dbzhang800/item/c14c97dd15318d17e1f46f ...
- 把jQuery的类、插件封装成seajs的模块的方法
这篇文章主要介绍了把jQuery的类.插件封装成seajs的模块的方法,需要的朋友可以参考下 注:本文使用的seajs版本是2.1.1 一.把Jquery封装成seajs的模块 define(func ...
- JVM如何加载一个类的过程,双亲委派模型中有哪些方法
1.类加载过程:加载.验证.准备.解析.初始化 加载 在加载阶段,虚拟机主要完成三件事: 1.通过一个类的全限定名来获取定义此类的二进制字节流. 2.将这个字节流所代表的静态存储结构转化为方法 ...
- SWF在线绘本批量制作高质量PDF的新方法(重点在批量制作)
SWF在线绘本批量制作高质量PDF的新方法(重点在批量制作) 2012-12-21 未来决定... http://www.ebama.net/thread-107643-1-1.html ...
- 22.编写一个类A,该类创建的对象可以调用方法showA输出小写的英文字母表。然后再编写一个A类的子类B,子类B创建的对象不仅可以调用方法showA输出小写的英文字母表,而且可以调用子类新增的方法showB输出大写的英文字母表。最后编写主类C,在主类的main方法 中测试类A与类B。
22.编写一个类A,该类创建的对象可以调用方法showA输出小写的英文字母表.然后再编写一个A类的子类B,子类B创建的对象不仅可以调用方法showA输出小写的英文字母表,而且可以调用子类新增的方法sh ...
- Java基础-接口.编写2个接口:InterfaceA和InterfaceB;在接口InterfaceA中有个方法void printCapitalLetter();在接口InterfaceB中有个方法void printLowercaseLetter();然 后写一个类Print实现接口InterfaceA和InterfaceB,要求 方法 实现输出大写英文字母表的功能,printLowerca
#34.编写2个接口:InterfaceA和InterfaceB:在接口InterfaceA中有个方法void printCapitalLetter():在接口InterfaceB中有个方法void ...
- Java基础-继承-编写一个Java应用程序,设计一个汽车类Vehicle,包含的属性有车轮个数 wheels和车重weight。小车类Car是Vehicle的子类,其中包含的属性有载人数 loader。卡车类Truck是Car类的子类,其中包含的属性有载重量payload。每个 类都有构造方法和输出相关数据的方法。最后,写一个测试类来测试这些类的功 能。
#29.编写一个Java应用程序,设计一个汽车类Vehicle,包含的属性有车轮个数 wheels和车重weight.小车类Car是Vehicle的子类,其中包含的属性有载人数 loader.卡车类T ...
随机推荐
- android调节屏幕亮度
一:只改变当前程序android屏幕亮度(1)方法:lp.screenBrightness 取值 0.0 -- 1.0 ※设定值(float)的范围,默认小于 0(系统设定).0.0(暗)-1.0(亮 ...
- fcntl函数用法详解
功能描述:根据文件描述词来操作文件的特性. #include <unistd.h> #include <fcntl.h> int fcntl(int fd, int cmd) ...
- Note for video Machine Learning and Data Mining——training vs Testing
Here is the note for lecture five. There will be several points 1. Training and Testing Both of th ...
- spring mvc 依赖包
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop&l ...
- java与数据库交互常用到的一些方法
下面我整理了一下java中常用的几个与数据库交互的常用方法,仅供参考: 1.执行SQL(dao层的实现类中) (1)SQL查询: //import org.hibernate.Query;//impo ...
- mysql 5.6 grant授权的时候出现问题
mysql> grant select on huamu_licai.* to 'read'@'%' identified by password 'Abcd1234';ERROR 1827 ( ...
- [na]思科产品选型pdf
以前做工程时候想起了设备选型时候用过的一份文档. 有个小伙伴今天问起思科设备选型,恰好google到了这份文档 https://www.cisco.com/web/CN/products/pdf/04 ...
- angular学习笔记(九)-css类和样式3
再来看一个选择li列表的例子: 点击li中的任意项,被点击的li高亮显示: <!DOCTYPE html> <html ng-app> <head> <tit ...
- web 安全问题(一):CSRF 攻击
什么是CSRF CSRF是怎么产生的 CSRF的攻击对象 CSRG的攻击手段 CSRF的防御措施 什么是CSRF 全称是(Cross Site Request Forgery)跨站请求伪造.也就是恶意 ...
- ny55 懒省事的小明
懒省事的小明 时间限制:3000 ms | 内存限制:65535 KB 难度:3 描述 小明很想吃果子,正好果园果子熟了.在果园里,小明已经将所有的果子打了下来,而 ...