该示例演示如何实现DTLS客户端。

7d2cd0ee0830b94b14f263196f740150.png「注意:这个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()信号被连接到ihandleTimeout()槽处理握手阶段中的数据包丢失和重传:

    ...
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("
"));

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("
---------------"
                                                    "
%1 received a DTLS datagram:
 %2"
                                                    "
As plain text:
 %3");

    const QString html = formatter.arg(clientInfo, QString::fromUtf8(datagram.toHex(' ')),
                                       QString::fromUtf8(plainText));
    ui->serverMessages->insertHtml(colorizer.arg(messageColor, html));
}

关于更多

  • 「QtCreator软件」可以找到:bcb1447e69db7323c30a14e210d5777d.png

  • 或在以下「Qt安装目录」找到:

C:\Qt\{你的Qt版本}\Examples\{你的Qt版本}\network\secureudpclient
  • 「相关链接」
https://doc.qt.io/qt-5/qtnetwork-secureudpclient-example.html
  • Qt君公众号回复『「Qt示例」』获取更多内容。
  • 公众号2020-09-20期推文《Qt官方示例-DTLS服务器》。
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐