QSslServer踩坑

   最近在Qt6.9.3版本下学习使用Qt QSslServer,踩了一些坑(其实是自己仔细看文档),然后网上的绝大部分教程都是基于 Qt 5.x的,5.x没有 QSslServer,需要开发者自己实现,并且Qt 6.4 之前和Qt 6.4 之后的差距也不小,这里主要是基于Qt 6.9.3 版本使用QSslServer遇到的问题进行总结

代码结构

为了方便理解,将代码的逻辑抽离,简化为两个线程。一般来说网络请求属于IO操作,不应该在主线程上,因此按照这种方式处理。然后客户端使用POSTMAN发起一个HTTPS请求。服务器端则是响应一个简单的Hello报文即可。

image

错误代码

下面是删减后的代码,保留必要逻辑和存在错误地方

main
// 服务器主线程
int main(int argc, char* argv[]) {
    QApplication a(argc, argv);
    Serverlet serv;
    serv.setAddress(QHostAddress::Any);
    serv.setPort(443);
    serv.setCertificate(":/local.crt");
    serv.setPrivateKey(":/local.key");
    serv.setCertChain({":/root.crt", ":/sub.crt",":/local.crt"});
    serv.start();
    return a.exec();
}
服务器类
// 服务器类,内置单独io线程、与连接处理器
class Serverlet : public QObject {
    Q_OBJECT
public:
    explicit Serverlet(QObject* parent = nullptr);
    ~Serverlet();
    void setPort(uint16_t port);
    void setAddress(QHostAddress host);
    void setMessageThreads(uint32_t nums);
    void setCertificate(QString cert);
    void setPrivateKey(QString key);
    void setCertChain(QList<QString> certs);
    bool start();
    void stop();
private:
    // io线程
    QThread* m_ioWorker{nullptr};
    // 连接器
    ConnectAccpetor* m_acceptor{nullptr};
};


void Serverlet::setCertChain(QList<QString> certs) {
    QMetaObject::invokeMethod(m_acceptor, "setCertChain", Q_ARG(QList<QString>, certs));
}

bool Serverlet::start() {
    bool isSuccess = true;
    isSuccess = QMetaObject::invokeMethod(m_acceptor, "startListen", Qt::BlockingQueuedConnection, Q_RETURN_ARG(bool, isSuccess));
    if (!isSuccess) {
        qDebug() << "Serverlet start failed";

        return isSuccess;
    }
    qDebug() << "Serverlet start success";
    return true;
}
连接处理
//  连接器
class ConnectAccpetor : public QObject {
    Q_OBJECT
public:
    explicit ConnectAccpetor(QObject* parent = nullptr);
    ~ConnectAccpetor();
    void init();

signals:
    void newSslSocket(QList<QSslSocket*> socket);
public slots:
    void procNewConnect();
    bool startListen();
    void setPort(uint16_t port);
    void setHost(QHostAddress host);
    void stop();
    void setCert(QString localCert);
    void setKey(QString localKey);
    void updateSslConfig();
    void setCertChain(QList<QString> certs);

private:
    QSslServer* m_server{nullptr};
    QHostAddress m_host{QHostAddress::Any};
    uint16_t m_port{8080};
    QSslConfiguration m_config;
};

// cpp
ConnectAccpetor::ConnectAccpetor(QObject* parent)
    : QObject{parent} {
    m_config.setProtocol(QSsl::AnyProtocol);
    auto ciphers = QSslConfiguration::supportedCiphers();
    m_config.setCiphers(ciphers);
    // 提前注册元类型(仅需一次)
    qRegisterMetaType<QList<QSslSocket*>>("QList<QSslSocket*>");
    qRegisterMetaType<QHostAddress>("QHostAddress");
    qRegisterMetaType<uint16_t>("uint16_t");
}

// 仅初始化一次QSslServer,避免重复创建
void ConnectAccpetor::init() {
    if (m_server != nullptr) return; // 已初始化则直接返回,核心修复点1

    m_server = new QSslServer(this);
    // 连接新连接信号
    connect(m_server, &QSslServer::newConnection, this, &ConnectAccpetor::procNewConnect);
    connect(m_server, &QSslServer::errorOccurred, this,
            [](QSslSocket* socket, QAbstractSocket::SocketError err) {
                qWarning() << "TLS握手失败" << err << socket;
                // 此时 socket 会自动销毁
            });
    m_server->setSslConfiguration(m_config);
}

void ConnectAccpetor::procNewConnect() {
    qDebug() << "收到新连接,开始处理SSL握手";
    while(m_server->hasPendingConnections()){
        qDebug() << "开始处理";
        QSslSocket* sslSocket = qobject_cast<QSslSocket*>(m_server->nextPendingConnection());
        sslSocket->write(R"(HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 5

Hello)");
        sslSocket->flush();
        qDebug() << "socket状态" << sslSocket->state() << "是否已加密" << sslSocket->isEncrypted();
        sslSocket->close();
        sslSocket->deleteLater();
    }
}

void ConnectAccpetor::setCertChain(QList<QString> certPath) {
    QList<QSslCertificate> certChain;
    for (const QString& path : certPath) {
        QList<QSslCertificate> caCerts = QSslCertificate::fromPath(path);
        if (caCerts.isEmpty()) {
            qWarning() << "证书链文件无效,跳过:" << path;
            continue;
        }
    }
    if (certChain.isEmpty()) {
        qCritical() << "设置证书链失败:无有效证书文件";
        return;
    }
    m_config.setLocalCertificateChain(certChain);
    qDebug() << "证书链设置成功,共" << certChain.size() << "个证书" << m_config.localCertificateChain();
}

异常现象

点击Postman发送请求

1、控制台:打印 收到新连接,开始处理SSL握手 但是没有打印 开始处理 ,也就是说,hasPenddingConnection返回false,那么 nextPendingConnection 获取的也是空。

2、控制台打印:QAbstractSocket::SocketError(20) 和 QAbstractSocket::SocketError(21) 

踩坑过程与原因

1、nextPendingConnection获取不到socket

  因为网上的一些例子,都是Qt 5.x版本的例子,都是自己继承QTcpServer,并自己实现incomingConnection ,在incomingConnection 函数中构建 QSslSocket对象,并且将加密成功的信号和服务器的业务逻辑槽函数绑定。但是我看源码,发现官方实现了这部分代码,因此不知道该如何获取QSslSocket,但是我又看到这样的一段代码:

image

  当QSslSocket握手成功之后,会将socket添加到 pendingConnection中,然后就可以从其中获取。我以为和QTcpSocket一样,addPendingConnection后会发出一个 newConnection信号,只需要在这个信号的槽函数中获取等待处理的连接即可。但是实际上,newConnection是QTcpSocket有新连接时触发的,TLS是基于TCP的,因此实际上在newConnection信号发出后,只是代表TCP新连接进入,还需要经历TLS握手环节,只有握手成功之后才会被添加到等待队列中,然后发出 pendingConnectionAvailable() 信号,此时才能处理。因此,需要关注信号 pendingConnectionAvailable ,然后才能获取到等待处理的连接 (6.4版本之后引入)

void ConnectAccpetor::init() {
    if (m_server != nullptr) return; // 已初始化则直接返回,核心修复点1

    m_server = new QSslServer(this);
    // 连接新连接信号
    connect(m_server, &QSslServer::pendingConnectionAvailable, this, &ConnectAccpetor::procNewConnect);
    connect(m_server, &QSslServer::errorOccurred, this,
            [](QSslSocket* socket, QAbstractSocket::SocketError err) {
                qWarning() << "TLS握手失败" << err << socket;
                // 此时 socket 会自动销毁
            });
    m_server->setSslConfiguration(m_config);
}

2、QAbstractSocket::SocketError(20) 和 QAbstractSocket::SocketError(21) 异常

  一开始控制台打印 QAbstractSocket::SocketError(20) 和 QAbstractSocket::SocketError(21) ,我还以为是编译时Openssl版本和运行时Openssl版本差距过大导致的,因为 SslInternalError 在官方文档中说,这是Openssl内部错误,可能是兼容性的问题。但是仔细一想,如果无法兼容,那么应该直接报错才对,但是可以运行,说明接口是一样的,底层实现有细微差距。然后是SslInvalidUserDataError这个错误的指向就很明确,指证书、密钥存在错误。为此还重写签发了一整套证书,但是还是报错,然后想着去抓包。抓包显示,TCP握手之后,客户端发送了一个请求,然后服务器直接就FIN,关闭了Socket,说明就是证书的配置存在问题,然后去网上搜相关的异常也没有详细说明,问AI也不知道是为什么(AI对于冷门问题不是乱编就是乱猜)。然后只有一点点排查,最后发现是证书链配置存在问题,当不配置证书链的时候,报错发生了变化,POSTMAN报错,提示找不到证书链。然后抓包发现,服务器没有立即关闭Socket,而是客户端关闭的Socket。说明确实是证书链配置异常导致的问题。

  然后翻阅证书链文档发现了一行小字,表示,第一个元素必须是叶子证书 (身份证书)。好了知道问题所在了,必须配置为身份证书。因此只需要更换一下证书顺序即可。然后就可以正常访问了。

image

// 服务器主线程
int main(int argc, char* argv[]) {
    QApplication a(argc, argv);
    Serverlet serv;
    serv.setAddress(QHostAddress::Any);
    serv.setPort(443);
    serv.setCertificate(":/local.crt");
    serv.setPrivateKey(":/local.key");
    serv.setCertChain({":/local.crt", ":/sub.crt",":/root.crt"});
    serv.start();
    return a.exec();
}

  另外我还发现,其实可以不用设置证书链,通过addCaCertificates 函数,将root和 sub证书都添加到配置中,也可以自动寻找到证书链。我尝试过追踪源码,但是过于底层的看不到,我猜测应该是在没有证书链的情况下会随机匹配一条证书链发送给客户端,如果在指明了证书链的情况下,就首先使用配置的证书链。

 

总结

  在Qt 6.4 版本之后,引入了 QSslServer,可以不需要自己去手动实现一个TLS服务器,QT底层已经实现了QSslSocket的构造与握手,原本的 newConnection 是在TCP握手完成后触发,对于QSslServer来说 pendingConnectionAvailable 才是握手完成时发出的信号。另外QSslConfigurtaion类加载证书链的时候,必须将身份证书放在第一个,ca证书的顺序不会影响证书链。

 

posted @ 2026-02-03 15:53  XBGzZ  阅读(0)  评论(0)    收藏  举报