DTLS 客户端
本例演示如何实现客户端 DTLS 连接。
注意: DTLS 客户端示例应与DTLS 服务器示例同时运行。
示例中的 DTLS 客户端可以与一个或多个 DTLS 服务器建立多个 DTLS 连接。客户端 DTLS 连接由 DtlsAssociation 类实现。该类使用QUdpSocket 读写数据报,并使用QDtls 进行加密:
class DtlsAssociation : public QObject { Q_OBJECT public: DtlsAssociation(const QHostAddress &address, quint16 port, const QString &connectionName); ~DtlsAssociation(); void startHandshake(); signals: void errorMessage(const QString &message); void warningMessage(const QString &message); void infoMessage(const QString &message); void serverResponse(const QString &clientInfo, const QByteArray &datagraam, const QByteArray &plainText); private slots: void udpSocketConnected(); void readyRead(); void handshakeTimeout(); void pskRequired(QSslPreSharedKeyAuthenticator *auth); void pingTimeout(); private: QString name; QUdpSocket socket; QDtls crypto; QTimer pingTimer; unsigned ping = 0; Q_DISABLE_COPY(DtlsAssociation) };
构造函数为新的 DTLS 连接设置最小 TLS 配置,并设置服务器的地址和端口:
... auto configuration = QSslConfiguration::defaultDtlsConfiguration(); configuration.setPeerVerifyMode(QSslSocket::VerifyNone); crypto.setPeer(address, port); crypto.setDtlsConfiguration(configuration); ...
QDtls::handshakeTimeout() 信号连接到 handleTimeout() 槽,以处理握手阶段的数据包丢失和重传:
... connect(&crypto, &QDtls::handshakeTimeout, this, &DtlsAssociation::handshakeTimeout); ...
为确保只接收来自服务器的数据报,我们将 UDP 套接字连接到服务器:
... socket.connectToHost(address.toString(), port); ...
QUdpSocket::readyRead() 信号连接到 readyRead() 时隙:
... connect(&socket, &QUdpSocket::readyRead, this, &DtlsAssociation::readyRead); ...
当与服务器的安全连接建立后,DtlsAssociation 对象将使用定时器向服务器发送 ping 短消息:
pingTimer.setInterval(5000); connect(&pingTimer, &QTimer::timeout, this, &DtlsAssociation::pingTimeout);
startHandshake() 开始与服务器握手:
void DtlsAssociation::startHandshake() { if (socket.state() != QAbstractSocket::ConnectedState) { emit infoMessage(tr("%1: connecting UDP socket first ...").arg(name)); connect(&socket, &QAbstractSocket::connected, this, &DtlsAssociation::udpSocketConnected); return; } if (!crypto.doHandshake(&socket)) emit errorMessage(tr("%1: failed to start a handshake - %2").arg(name, crypto.dtlsErrorString())); else emit infoMessage(tr("%1: starting a handshake").arg(name)); }
readyRead() 槽读取服务器发送的数据报:
QByteArray dgram(socket.pendingDatagramSize(), Qt::Uninitialized); const qint64 bytesRead = socket.readDatagram(dgram.data(), dgram.size()); if (bytesRead <= 0) { emit warningMessage(tr("%1: spurious read notification?").arg(name)); return; } dgram.resize(bytesRead);
如果握手已经完成,则对该数据报进行解密:
if (crypto.isConnectionEncrypted()) { const QByteArray plainText = crypto.decryptDatagram(&socket, dgram); if (plainText.size()) { emit serverResponse(name, dgram, plainText); return; } if (crypto.dtlsError() == QDtlsError::RemoteClosedConnectionError) { emit errorMessage(tr("%1: shutdown alert received").arg(name)); socket.close(); pingTimer.stop(); return; } emit warningMessage(tr("%1: zero-length datagram received?").arg(name)); } else {
否则,我们尝试继续握手:
if (!crypto.doHandshake(&socket, dgram)) { emit errorMessage(tr("%1: handshake error - %2").arg(name, crypto.dtlsErrorString())); return; }
握手完成后,我们发送第一条 ping 消息:
if (crypto.isConnectionEncrypted()) { emit infoMessage(tr("%1: encrypted connection established!").arg(name)); pingTimer.start(); pingTimeout(); } else {
pskRequired()槽提供握手阶段所需的预共享密钥(PSK):
void DtlsAssociation::pskRequired(QSslPreSharedKeyAuthenticator *auth) { Q_ASSERT(auth); emit infoMessage(tr("%1: providing pre-shared key ...").arg(name)); auth->setIdentity(name.toLatin1()); auth->setPreSharedKey(QByteArrayLiteral("\x1a\x2b\x3c\x4d\x5e\x6f")); }
注: 为简洁起见,pskRequired() 的定义过于简化。QSslPreSharedKeyAuthenticator 类的文档详细解释了如何正确执行此插槽。
pingTimeout() 向服务器发送加密信息:
void DtlsAssociation::pingTimeout() { static const QString message = QStringLiteral("I am %1, please, accept our ping %2"); const qint64 written = crypto.writeDatagramEncrypted(&socket, message.arg(name).arg(ping).toLatin1()); if (written <= 0) { emit errorMessage(tr("%1: failed to send a ping - %2").arg(name, crypto.dtlsErrorString())); pingTimer.stop(); return; } ++ping; }
在握手阶段,客户端必须处理由于数据包丢失而可能发生的超时。handshakeTimeout() 插槽会重新传输握手信息:
void DtlsAssociation::handshakeTimeout() { emit warningMessage(tr("%1: handshake timeout, trying to re-transmit").arg(name)); if (!crypto.handleTimeout(&socket)) emit errorMessage(tr("%1: failed to re-transmit - %2").arg(name, crypto.dtlsErrorString())); }
在客户端连接被销毁之前,必须关闭其 DTLS 连接:
DtlsAssociation::~DtlsAssociation() { if (crypto.isConnectionEncrypted()) crypto.shutdown(&socket); }
用户界面会显示错误信息、信息和来自服务器的解密响应:
const QString colorizer(QStringLiteral("<font color=\"%1\">%2</font><br>")); void MainWindow::addErrorMessage(const QString &message) { ui->clientMessages->insertHtml(colorizer.arg(QStringLiteral("Crimson"), message)); } void MainWindow::addWarningMessage(const QString &message) { ui->clientMessages->insertHtml(colorizer.arg(QStringLiteral("DarkOrange"), message)); } void MainWindow::addInfoMessage(const QString &message) { ui->clientMessages->insertHtml(colorizer.arg(QStringLiteral("DarkBlue"), message)); } void MainWindow::addServerResponse(const QString &clientInfo, const QByteArray &datagram, const QByteArray &plainText) { static const QString messageColor = QStringLiteral("DarkMagenta"); static const QString formatter = QStringLiteral("<br>---------------" "<br>%1 received a DTLS datagram:<br> %2" "<br>As plain text:<br> %3"); const QString html = formatter.arg(clientInfo, QString::fromUtf8(datagram.toHex(' ')), QString::fromUtf8(plainText)); ui->serverMessages->insertHtml(colorizer.arg(messageColor, html)); }
© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.