DTLS 客户端

本示例演示了如何实现客户端 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);
}

错误消息、信息消息以及来自服务器的解密响应将由 UI 显示:

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));
}

示例项目 @ code.qt.io

© 2026 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.