简单的TCP邮箱程序
**教学与实践目的:**学会网络邮件发送的程序设计技术。
1.SMTP协议
-
邮件传输协议包括
SMTP(简单邮件传输协议,RFC821)
及其扩充协议MIME
; -
邮件接收协议包括
POP3
和功能更强大的IMAP
协议。 服务邮件发送的服务器其端口为 25(如果开启 ssl 一般使用 465 端口,目前QQ邮箱已经强制必须使用加密连接方式,所以以下实验使用465
端口), 服务邮件接收的服务器端口为 110(如果开启 SSL 一般使用 995 端口)。
SMTP协议解读以及如何使用SMTP协议发送电子邮件 - 一只会铲史的猫 - 博客园
-
SMTP(简单邮件传输协议):
- 命令有序性:SMTP的命令需要按照特定的顺序执行,以完成邮件的发送任务。每个命令都有其特定的作用,并且必须按照正确的顺序组合使用。
- 请求应答模式:SMTP遵循请求应答式协议,客户端发送命令后,服务器会返回相应的响应。这种模式确保了命令的执行和结果的确认。
- 响应格式:SMTP的响应格式通常包括一个三位数字的响应码,后面跟着响应描述,这与HTTP协议的响应格式相似。
-
POP3(邮局协议第三版):
- 命令独立性:POP3的命令如LIST、STAT、UIDL、TOP、RETR、DELE等,都可以独立使用,每个命令都有其特定的功能,如查看邮件列表、获取邮件内容等。
- 数据流处理:在接收邮件时,POP3需要以流的方式处理数据,因为邮件数据可能不是一次性完整发送,而是分批次到达。
-
Socket编程中的差异:
- 发送数据的简单性:在socket编程中,发送数据(如SMTP)相对简单,因为发送方可以按照自己的节奏发送数据,不需要考虑接收方的状态。
- 接收数据的复杂性:接收数据(如POP3)需要判断数据是否完全接收,处理数据流的完整性,这比发送数据要复杂。
-
请求应答式协议:
- SMTP和HTTP:SMTP和HTTP都是请求应答式协议,客户端发送请求后,服务器返回响应。
- HTTP的Keep-Alive:HTTP协议在设置为Keep-Alive时,可以进行多次请求和响应的交互,否则通常只有一次交互机会。
2.第三方邮箱设置
邮箱设置一定要开启 smtp/pop3
服务(以 QQ邮箱为例,在[邮箱设置]中 的[账户]中开启相关服务(获取授权码
)
3.本机设置telnet支持
- 首先打开电脑的控制面板
- 点击程序–>启用或关闭windows程序
- 启用Telnet客户端
- 将telent这个服务勾选上然后点击确定。
- 测试telent是否可用。打开cmd命令窗口,输入telnet
smtp.h
#ifndef SMTP_H
#define SMTP_H
#include<QByteArray>
#include<QString>
#include<QTcpSocket>
class Smtp
{
public:
Smtp(QByteArray username,QByteArray password);
~Smtp();
void SendData(QByteArray sendIp,QByteArray s_Title,QByteArray s_Content);
QString WaitAndReadData();
private:
QByteArray m_UserName="";
QByteArray m_Password="";
QTcpSocket * m_pSocket=nullptr;
QString m_ReceiverData="";
};
#endif // SMTP_H
smtp.cpp
#include "smtp.h"
#include<QDebug>
Smtp::Smtp(QByteArray username,QByteArray password)
{
if(username.contains("@163"))
{
m_UserName= username;
m_Password = password;
}
else
{
qDebug()<<"Error";
}
}
void Smtp::SendData(QByteArray sendIp, QByteArray s_Title, QByteArray s_Content)
{
m_pSocket=new QTcpSocket();
m_pSocket->connectToHost("smtp.163.com",25,QTcpSocket::ReadWrite); //连接163邮箱
m_pSocket->waitForConnected(1000);
WaitAndReadData();
m_pSocket->write("helo localhost\r\n");
WaitAndReadData();
m_pSocket->write("auth login\r\n");
WaitAndReadData();
m_pSocket->write(m_UserName.toBase64()+"\r\n"); //写入用户名
WaitAndReadData();
m_pSocket->write(m_Password.toBase64()+"\r\n"); //写入密码
WaitAndReadData();
m_pSocket->write("mail from: <"+m_UserName+">\r\n"); //发送的邮箱
WaitAndReadData();
m_pSocket->write("rcpt to: <"+sendIp+">\r\n"); //接收的邮箱
WaitAndReadData();
m_pSocket->write("data\r\n"); //开始写入
WaitAndReadData();
m_pSocket->write("from:<"+m_UserName+">\r\n"); //发送名称
WaitAndReadData();
m_pSocket->write("to:<"+sendIp+">"); //接受名称
WaitAndReadData();
m_pSocket->write("data\r\n");
WaitAndReadData();
m_pSocket->write("Subject:"+s_Title+"\r\n"); //标题
m_pSocket->write("\r\n");
m_pSocket->write(s_Content.append("\r\n")); //内容
m_pSocket->write(".\r\n");
WaitAndReadData();
m_pSocket->write("quit\r\n");
m_pSocket->disconnect();
}
QString Smtp::WaitAndReadData()
{
m_pSocket->waitForReadyRead(1000);
m_ReceiverData = m_pSocket->readAll();
return m_ReceiverData;
}
Smtp::~Smtp()
{
delete m_pSocket;
}
4.测试
Smtp smtp("邮箱名称","授权码"); //邮箱和密码都要用自己的 //注意是授权码,不是你登录邮箱的密码
smtp.SendData("aaa@qq.com","你好","这是一个测试程序");
安全的SSL邮箱程序
1. 使用QSslSocket设置参数
#include <QSslSocket>
#include <QSslCertificate>
#include <QSslKey>
#include <QTcpSocket>
#include <QHostAddress>
#include <QIODevice>
#include <QApplication>
#include <QDebug>
class SSLClient : public QObject {
Q_OBJECT
public:
SSLClient(const QString &host, quint16 port, QObject *parent = nullptr) : QObject(parent) {
QSslSocket *socket = new QSslSocket(QSsl::SslClientMode, this);
connect(socket, &QSslSocket::encrypted, this, &SSLClient::onEncrypted);
connect(socket, &QSslSocket::readyRead, this, &SSLClient::onReadyRead);
connect(socket, &QSslSocket::sslErrors, this, &SSLClient::onSslErrors);
connect(socket, &QSslSocket::connected, this, &SSLClient::onConnected);
socket->connectToHostEncrypted(host, port);
}
private slots:
void onConnected() {
qDebug() << "Connected to the server";
}
void onEncrypted() {
QSslSocket *socket = qobject_cast<QSslSocket *>(sender());
if (socket) {
QSslConfiguration config = socket->sslConfiguration();
// 设置SSL/TLS协议
config.setProtocol(QSsl::TlsV1_2);
// 设置验证模式
config.setPeerVerifyMode(QSslSocket::VerifyPeer);
// 设置验证深度
config.setPeerVerifyDepth(2);
// 设置加密套件
config.setCiphers(QSslConfiguration::supportedCiphers());
// 加载本地证书和私钥(如果需要)
// config.setLocalCertificate(QSslCertificate("path/to/certificate.pem", QSsl::Pem));
// config.setPrivateKey(QSslKey("path/to/private_key.pem", QSsl::Rsa, QSsl::Pem));
socket->setSslConfiguration(config);
}
}
void onReadyRead() {
QSslSocket *socket = qobject_cast<QSslSocket *>(sender());
if (socket) {
QByteArray data = socket->readAll();
qDebug() << "Received:" << data;
}
}
void onSslErrors(const QList<QSslError> &errors) {
QSslSocket *socket = qobject_cast<QSslSocket *>(sender());
if (socket) {
foreach (const QSslError &error, errors) {
qDebug() << "SSL Error:" << error.errorString();
}
socket->ignoreSslErrors();
}
}
};
2. 检查是否连接成功
在Qt中,使用QSslSocket
类时,你可以通过几种方式来检查SSL连接是否已经成功建立:
-
使用信号:
QSslSocket
提供了几个信号,可以用来确定连接的状态。-
encrypted()
:当连接被加密时发出,表示SSL握手已经完成,并且数据传输现在是加密的。 -
connected()
:当底层的TCP连接建立时发出,但此时SSL握手可能还没有完成。 -
sslErrors()
:当SSL握手过程中出现错误时发出,你可以通过这个信号来检查是否有错误发生。
-
-
检查状态:
使用QSslSocket
的state()
方法可以获取当前的连接状态。QAbstractSocket::SocketState
枚举值可以告诉你连接是否已经连接、正在连接、关闭等。 -
检查错误:
使用QSslSocket
的error()
方法可以获取最后一个错误。如果error()
返回QAbstractSocket::NoError
,则表示没有错误发生。
下面是一个简单的例子,展示了如何使用这些方法来检查SSL连接是否成功:
#include <QSslSocket>
#include <QDebug>
// 假设你已经有了一个QSslSocket对象叫做sslSocket
// 连接信号
connect(sslSocket, &QSslSocket::encrypted, this, []() {
qDebug() << "SSL connection established";
});
connect(sslSocket, &QSslSocket::sslErrors, this, [](QSslSocket *socket, const QList<QSslError> &errors) {
foreach (const QSslError &error, errors) {
qDebug() << "SSL error:" << error.errorString();
}
});
// 检查状态
if (sslSocket->state() == QAbstractSocket::ConnectedState) {
if (sslSocket->error() == QAbstractSocket::NoError) {
qDebug() << "SSL connection is up and running without errors";
} else {
qDebug() << "SSL connection has errors";
}
}
// 检查是否加密
if (sslSocket->isEncrypted()) {
qDebug() << "The connection is encrypted";
} else {
qDebug() << "The connection is not encrypted";
}
在这个例子中:
- 我们连接了
encrypted()
信号,当SSL连接建立时,会在控制台输出消息。 - 我们连接了
sslErrors()
信号,如果有SSL错误发生,会在控制台输出错误信息。 - 我们使用
state()
方法检查当前的连接状态,并且使用error()
方法检查是否有错误发生。 - 我们使用
isEncrypted()
方法检查连接是否已经加密。
界面布局
layoutstretch
在Qt Creator中,layoutStretch
是用于控制布局中各个元素(小部件或子布局)的拉伸系数(stretch factor)的属性。拉伸系数决定了元素在父布局中分配多余空间的比例。如果拉伸系数为0,则元素将保持其最小大小,而大于0的拉伸系数会让元素能够拉伸占据更多空间。
具体来说,layoutStretch
属性可以在Qt Designer中直接设置,或者通过代码来调整。在Qt Designer中,当你选中一个布局器(比如水平布局器QHBoxLayout
或垂直布局器QVBoxLayout
),你可以在属性编辑栏中找到layoutStretch
属性。这个属性允许你为布局中的每个元素设置一个拉伸系数,这些系数决定了在布局中的元素如何随着父容器大小的变化而变化。
例如,如果你有一个水平布局器中包含三个按钮,并且你将layoutStretch
设置为1,2,3
,那么当布局中的总空间需要分配时,第一个按钮将获得1份空间,第二个按钮获得2份空间,第三个按钮获得3份空间。这样,第三个按钮将占据比第一个和第二个按钮更多的空间。
在代码中,你可以使用QBoxLayout
的setStretch
或setStretchFactor
方法来设置拉伸系数。例如:
QHBoxLayout *layout = new QHBoxLayout;
QPushButton *button1 = new QPushButton("Button 1");
QPushButton *button2 = new QPushButton("Button 2");
QPushButton *button3 = new QPushButton("Button 3");
layout->addWidget(button1);
layout->addWidget(button2);
layout->addWidget(button3);
layout->setStretchFactor(button1, 1);
layout->setStretchFactor(button2, 2);
layout->setStretchFactor(button3, 3);
这段代码将创建一个水平布局,其中包含三个按钮,并且设置了不同的拉伸系数,从而影响它们在布局中的空间分配。
设置QSS
C++ Qt开发:PushButton按钮组件 - lyshark - 博客园
最终界面如图
QT中的多线程
在Qt中,有几种方式可以创建和使用多线程。以下是一些常见的方法:
使用QThread
最直接的方法是使用QThread
类。你可以将需要在后台执行的任务移到一个新的线程中。以下是如何使用QThread
的基本示例:
#include <QThread>
#include <QDebug>
class Worker : public QObject {
Q_OBJECT
public slots:
void doWork() {
qDebug() << "Work is being done in thread" << QThread::currentThreadId();
// 执行一些耗时的操作
}
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QThread *thread = new QThread();
Worker *worker = new Worker();
worker->moveToThread(thread);
// 当线程启动时,Worker对象的doWork()方法会被调用
QObject::connect(thread, &QThread::started, worker, &Worker::doWork);
// 当Worker对象的doWork()方法完成后,退出线程
QObject::connect(worker, &Worker::finished, thread, &QThread::quit);
// 确保线程结束时删除Worker对象
QObject::connect(thread, &QThread::finished, worker, &QObject::deleteLater);
// 同样,确保线程结束时删除线程对象本身
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start(); // 启动线程
return app.exec(); // 进入Qt事件循环
}
使用QtConcurrent
Qt提供了QtConcurrent
模块,它提供了一个高级的并发编程框架。你可以使用QtConcurrent::run()
函数来简单地在后台线程中运行函数或成员函数。
#include <QtConcurrent>
#include <QDebug>
#include <QCoreApplication>
void doWork() {
qDebug() << "Work is being done in thread" << QThread::currentThreadId();
// 执行一些耗时的操作
}
int main(int argc, char *argv[]) {
QCoreApplication app(argc, argv);
// 这将在一个新的线程中异步执行doWork函数
QtConcurrent::run(doWork);
return app.exec(); // 进入Qt事件循环
}
使用QThreadPool
和QRunnable
你可以创建一个QRunnable
对象来表示一个任务,并将其提交给QThreadPool
,这样它就可以在某个线程中异步执行。
#include <QThreadPool>
#include <QRunnable>
#include <QDebug>
class WorkerRunnable : public QRunnable {
public:
WorkerRunnable() {}
void run() override {
qDebug() << "Work is being done in thread" << QThread::currentThreadId();
// 执行一些耗时的操作
}
};
int main(int argc, char *argv[]) {
QCoreApplication app(argc, argv);
WorkerRunnable *task = new WorkerRunnable();
QThreadPool::globalInstance()->start(task); // 任务将被添加到全局线程池并执行
return app.exec(); // 进入Qt事件循环
}
注意事项
- 当使用多线程时,你需要确保对共享数据的访问是线程安全的。这通常意味着使用互斥锁(
QMutex
)或其他同步机制。 - 避免在子线程中直接操作GUI,因为Qt的GUI工具包不是线程安全的。如果需要更新GUI,可以使用
QObject::moveToThread()
将对象移动到GUI线程,或者使用QMetaObject::invokeMethod()
或QSignalMapper
来安全地从后台线程发出信号到GUI线程。 - 使用
QThread
时,确保适当地管理线程的生命周期,避免内存泄漏。通常,当线程完成其任务后,你应该调用quit()
来结束线程的事件循环,然后删除线程对象。
在多线程编程中,正确地管理资源和同步是至关重要的,以避免数据竞争、死锁和其他并发问题。
子线程更新GUI
Thread子类版
在Qt中,由于GUI组件不是线程安全的,你不能直接从子线程更新GUI。相反,你需要使用信号和槽机制来安全地从子线程发出信号,并在主线程(GUI线程)中接收这些信号并更新GUI。
以下是使用子线程更新GUI的步骤:
步骤 1: 创建一个继承自QObject
的类,并在其中定义信号
// Worker.h
#ifndef WORKER_H
#define WORKER_H
#include <QObject>
class Worker : public QObject {
Q_OBJECT
public:
explicit Worker(QObject *parent = nullptr) : QObject(parent) {}
signals:
void updateGUI(const QString &data); // 自定义信号
public slots:
void doWork();
};
#endif // WORKER_H
步骤 2: 实现Worker
类的工作方法
// Worker.cpp
#include "Worker.h"
void Worker::doWork() {
// 执行一些耗时的操作
QString result = "Done"; // 假设这是耗时操作的结果
emit updateGUI(result); // 发送信号,而不是直接更新GUI
}
步骤 3: 在主线程中创建Worker
对象,并将其移动到子线程
// main.cpp
#include <QApplication>
#include <QThread>
#include <QPushButton>
#include "Worker.h"
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
QThread *thread = new QThread();
Worker *worker = new Worker();
worker->moveToThread(thread);
// 当线程启动时,Worker对象的doWork()方法会被调用
QObject::connect(thread, &QThread::started, worker, &Worker::doWork);
// 从子线程接收信号,并在主线程中更新GUI
QObject::connect(worker, &Worker::updateGUI, [](const QString &data) {
qDebug() << "Update GUI in main thread:" << data;
// 在这里更新GUI,例如设置文本到一个标签
});
// 当Worker对象的doWork()方法完成后,退出线程
QObject::connect(worker, &Worker::updateGUI, thread, &QThread::quit);
// 确保线程结束时删除Worker对象
QObject::connect(thread, &QThread::finished, worker, &QObject::deleteLater);
// 同样,确保线程结束时删除线程对象本身
QObject::connect(thread, &QThread::finished, thread, &QThread::deleteLater);
// 创建一个简单的窗口来测试
QPushButton button("Start Work");
QObject::connect(&button, &QPushButton::clicked, thread, &QThread::start);
button.show();
return app.exec(); // 进入Qt事件循环
}
在这个例子中,我们创建了一个Worker
对象,并将其移动到了一个新的QThread
对象中。当线程启动时,Worker
对象的doWork()
方法会被调用。这个方法执行一些耗时的操作,并通过updateGUI
信号发送结果。这个信号被连接到一个槽函数,该槽函数在主线程中更新GUI。
注意事项
- 确保在子线程中不要直接操作GUI组件。
- 使用
QThread::quit()
来优雅地退出线程。 - 使用
QObject::moveToThread()
将对象移动到正确的线程。 - 使用
QObject::deleteLater()
来确保线程和对象被适当地清理。
QConcurrent::run方法
在Qt中,尝试从非GUI线程直接更新GUI元素(如ui->textBrowser
)会导致未定义行为,因为Qt的GUI组件并不是线程安全的。因此,你需要使用信号和槽来安全地从子线程更新GUI。
下面是如何正确使用QtConcurrent::run
来更新GUI的步骤:
步骤 1: 定义一个信号
首先,在你的窗口类或任何适当的类中定义一个信号,用于传递数据到GUI线程。
// MyMainWindow.h
#include <QMainWindow>
#include <QString>
namespace Ui {
class MyMainWindow;
}
class MyMainWindow : public QMainWindow {
Q_OBJECT
public:
explicit MyMainWindow(QWidget *parent = nullptr);
~MyMainWindow();
signals:
void updateTextBrowser(const QString &text); // 定义一个信号
private:
Ui::MyMainWindow *ui;
};
步骤 2: 连接信号和槽
在你的窗口类的构造函数中,连接这个信号到一个槽,这个槽会更新GUI。
// MyMainWindow.cpp
#include "MyMainWindow.h"
#include "ui_MyMainWindow.h"
MyMainWindow::MyMainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MyMainWindow),
// 初始化ui组件等
{
ui->setupUi(this);
// 连接信号到槽
connect(this, &MyMainWindow::updateTextBrowser, [this](const QString &text) {
ui->textBrowser->append(text);
});
}
MyMainWindow::~MyMainWindow() {
delete ui;
}
步骤 3: 使用QtConcurrent::run
在后台线程中运行任务
现在,你可以使用QtConcurrent::run
来在后台线程中运行你的任务,并通过信号将数据发送回GUI线程。
#include <QtConcurrent>
#include <QDebug>
void startBackgroundTask(MyMainWindow *window) {
QtConcurrent::run([=]() {
while (true) {
QString text = "nihao1";
emit window->updateTextBrowser(text); // 发送信号
QThread::sleep(1); // 避免过度占用CPU
}
});
// 返回对象是QFuture<模板>,若lambda表达式没返回值,则是QFuture<void>型
}
步骤 4: 启动后台任务
在你的窗口类或其他适当的地方,调用startBackgroundTask
函数来启动后台任务。
// 例如,在某个按钮的点击事件中
startBackgroundTask(this);
注意事项
- 使用
QThread::sleep
或其他同步机制来控制循环的频率,以避免过度占用CPU。 - 确保在适当的时候停止后台任务,例如在窗口关闭时。这可以通过设置一个控制变量来实现,当需要停止时,改变这个变量的值,并在循环中检查这个变量。
- 在实际应用中,你可能需要更复杂的逻辑来确保线程安全地停止后台任务。
通过这种方式,你可以安全地从后台线程更新GUI,而不违反Qt的线程安全规则。
Socket跨线程调用的问题
Qt笔记-QTcpSocket跨线程调用(官方推荐方法,非百度烂大街方法)_setsocketdescriptor-****博客
错误信息 "QSocketNotifier: Socket notifiers cannot be enabled or disabled from another thread"
指出 QSocketNotifier
不能从另一个线程被启用或禁用。这是因为 QSocketNotifier
需要在创建它的线程中被注册和取消注册。当你尝试在不同的线程中操作 QSocketNotifier
时,就会遇到这个问题。
-
确保
QTcpSocket
和QSocketNotifier
在同一个线程中:如果你在子线程中使用QTcpSocket
,那么QSocketNotifier
也应该在这个子线程中创建和管理。这意味着你需要避免跨线程调用QObject
及其子类对象。 -
使用
moveToThread
方法:如果你有一个QTcpSocket
对象,你可以使用moveToThread
方法将其移动到新的线程中。对于QSocketNotifier
,在连接前确保它已经在正确的线程上。 -
避免在子线程中直接操作
QTcpSocket
:如果你在子线程中直接操作QTcpSocket
,可能会导致QSocketNotifier
相关的问题。你可以通过信号和槽机制,将数据从一个线程安全地传递到另一个线程。 -
在子线程中创建
QTcpSocket
:如果你在子线程的构造函数或run
函数中创建QTcpSocket
,那么所有的操作都应该在这个子线程中进行,以避免跨线程操作QSocketNotifier
。 -
使用
Qt::QueuedConnection
:在连接信号和槽时,使用Qt::QueuedConnection
可以确保信号安全地传递给主线程中的槽函数,这样可以避免在子线程中直接操作QSocketNotifier
。 -
避免跨线程调用
QObject
:当你在主线程中创建QObject
及其子类对象时,不要尝试在子线程中对其进行操作。相反,你应该在子线程中创建这些对象,以避免跨线程调用。
最终代码
main.cpp
#include "widget.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
Widget w;
w.show();
return a.exec();
}
tcpmailclient.h
#ifndef TCPMAILCLIENT_H
#define TCPMAILCLIENT_H
#include <QObject>
#include <QObject>
#include <QtNetwork>
#include <QSslSocket>
#include <QSslCertificate>
#include <QSslKey>
#include <QTcpSocket>
#include <QHostAddress>
#include <QIODevice>
#include <QApplication>
#include <QDebug>
class TCPMailClient : public QObject
{
Q_OBJECT
public:
explicit TCPMailClient(const QString &host, quint16 port,QObject *parent = nullptr);
void send(QString msg);
QString recieve();
bool CanReadLine();
private:
QSslSocket* ssl;
bool isentrcyed = false;
signals:
};
#endif // TCPMAILCLIENT_H
tcpmailclient.hpp
#include "tcpmailclient.h"
TCPMailClient::TCPMailClient(const QString &host, quint16 port, QObject *parent)
: QObject{