警告

本节包含自动从C++翻译成Python的代码片段,可能包含错误。

DTLS客户端#

此示例演示了如何实现客户端DTLS连接。

../_images/secureudpclient-example.png

注意

DTLS客户端示例应与DTLS服务器示例一起运行。

示例DTLS客户端可以建立一个或多个DTLS服务器连接。客户端DTLS连接由DtlsAssociation类实现。此类使用QUdpSocket读取和写入数据报,以及QDtls进行加密。

class DtlsAssociation(QObject):

    Q_OBJECT
# public
    DtlsAssociation(QHostAddress address, quint16 port,
                    connectionName) = QString()
    ~DtlsAssociation()
    def startHandshake():
# signals
    def errorMessage(message):
    def warningMessage(message):
    def infoMessage(message):
    def serverResponse(clientInfo, datagraam,):
                        plainText) = QByteArray()
# private slots
    def udpSocketConnected():
    def readyRead():
    def handshakeTimeout():
    def pskRequired(auth):
    def pingTimeout():
# private
    name = QString()
    socket = QUdpSocket()
    crypto = QDtls()
    pingTimer = QTimer()
    ping = 0
    Q_DISABLE_COPY(DtlsAssociation)

构造函数为新的DTLS连接设置了最小的TLS配置,并设置了服务器的地址和端口。

    ...

configuration = QSslConfiguration.defaultDtlsConfiguration()
configuration.setPeerVerifyMode(QSslSocket.VerifyNone)
crypto.setPeer(address, port)
crypto.setDtlsConfiguration(configuration)            ...

handshakeTimeout()信号连接到handleTimeout()槽以处理握手阶段的丢包和重传。

    ...

crypto.handshakeTimeout.connect(self.handshakeTimeout)            ...

为了确保我们只接收来自服务器的数据报,我们将我们的UDP套接字连接到服务器。

    ...

socket.connectToHost(address.toString(), port)            ...

将QUdpSocket::readyRead()信号连接到readyRead()槽。

    ...

socket.readyRead.connect(self.readyRead)            ...

当与服务器建立了安全连接后,DtlsAssociation对象将使用计时器向服务器发送短暂的ping消息。

pingTimer.setInterval(5000)
pingTimer.timeout.connect(self.pingTimeout)

startHandshake()开始与服务器进行握手。

def startHandshake(self):

    if socket.state() != QAbstractSocket.ConnectedState:
        infoMessage.emit(tr("%1: connecting UDP socket first ...").arg(name))
        socket.connected.connect(self.udpSocketConnected)
        return

    if not crypto.doHandshake(socket):
        errorMessage.emit(tr("%1: failed to start a handshake - %2").arg(name, crypto.dtlsErrorString()))
else:
        infoMessage.emit(tr("%1: starting a handshake").arg(name))

readyRead()槽读取服务器发送的数据报。

dgram = QByteArray(socket.pendingDatagramSize(), Qt.Uninitialized)
bytesRead = socket.readDatagram(dgram.data(), dgram.size())
if bytesRead <= 0:
    warningMessage.emit(tr("%1: spurious read notification?").arg(name))
    return

dgram.resize(bytesRead)

如果握手已经完成,则解密此数据报。

if crypto.isConnectionEncrypted():
    plainText = crypto.decryptDatagram(socket, dgram)
    if plainText.size():
        serverResponse.emit(name, dgram, plainText)
        return

    if crypto.dtlsError() == QDtlsError.RemoteClosedConnectionError:
        errorMessage.emit(tr("%1: shutdown alert received").arg(name))
        socket.close()
        pingTimer.stop()
        return

    warningMessage.emit(tr("%1: zero-length datagram received?").arg(name))
else:

否则,我们尝试继续握手。

if not crypto.doHandshake(socket, dgram):
    errorMessage.emit(tr("%1: handshake error - %2").arg(name, crypto.dtlsErrorString()))
    return

当握手完成时,我们发送第一条ping消息。

if crypto.isConnectionEncrypted():
    infoMessage.emit(tr("%1: encrypted connection established!").arg(name))
    pingTimer.start()
    pingTimeout()
else:

pskRequired()槽提供了握手阶段所需的预共享密钥(PSK)。

def pskRequired(self, auth):

    Q_ASSERT(auth)
    infoMessage.emit(tr("%1: providing pre-shared key ...").arg(name))
    auth.setIdentity(name.toLatin1())
    auth.setPreSharedKey(QByteArrayLiteral("\x1a\x2b\x3c\x4d\x5e\x6f"))

注意

为了简化,pskRequired()的定义过于简化。有关QSslPreSharedKeyAuthenticator类和如何正确实现此槽的详细说明,请参阅文档。

pingTimeout()向服务器发送加密的消息。

def pingTimeout(self):

    message = "I am %1, please, accept our ping %2"
    written = crypto.writeDatagramEncrypted(socket, message.arg(name).arg(ping).toLatin1())
    if written <= 0:
        errorMessage.emit(tr("%1: failed to send a ping - %2").arg(name, crypto.dtlsErrorString()))
        pingTimer.stop()
        return

    ping = ping + 1

在握手阶段,客户端必须处理可能的超时,这可能是由于丢包造成的。handshakeTimeout()槽重新发送握手消息。

def handshakeTimeout(self):

    warningMessage.emit(tr("%1: handshake timeout, trying to re-transmit").arg(name))
    if not crypto.handleTimeout(socket):
        errorMessage.emit(tr("%1: failed to re-transmit - %2").arg(name, crypto.dtlsErrorString()))

在客户端连接销毁之前,必须关闭其DTLS连接。

DtlsAssociation.~DtlsAssociation()

    if crypto.isConnectionEncrypted():
        crypto.shutdown(socket)

错误消息、信息消息和来自服务器的解密响应通过UI显示。

colorizer = QString(QStringLiteral("<font color=\"%1\">%2"))
def addErrorMessage(self, message):

    ui.clientMessages.insertHtml(colorizer.arg("Crimson", message))

def addWarningMessage(self, message):

    ui.clientMessages.insertHtml(colorizer.arg("DarkOrange", message))

def addInfoMessage(self, message):

    ui.clientMessages.insertHtml(colorizer.arg("DarkBlue", message))

def addServerResponse(self, clientInfo, datagram,):
                                   QByteArray plainText)

    messageColor = "DarkMagenta"
    formatter = QStringLiteral("<br>---------------"()
                                                    "<br>%1 received a DTLS datagram:<br> %2"
                                                    "<br>As plain text:<br> %3")
    html = formatter.arg(clientInfo, QString.fromUtf8(datagram.toHex(' ')),
                                       QString.fromUtf8(plainText))
    ui.serverMessages.insertHtml(colorizer.arg(messageColor, html))

示例项目 @ code.qt.io