基于 Qt 的 XML-RPC 客户端:网络操作

前面我们已经了解了 XML-RPC 协议的具体内容,使用 Qt XML API 完成了QVariant与 XML 数据格式之间的转换。下面的内容就是,如何使用 Qt Network API,将我们的客户端与 XML-RPC 服务器相连接。

Qt 通过 QNetworkAccessManager类与服务器进行通讯。我们这里就是要使用这个类。如果看看 Qt 的文档,就会发现,Qt 还提供了QHttp这样专门针对特定协议的网络访问类。但我们不去使用这些类,因为 Qt 5 中,这些类会被移除。为了让我们的代码尽可能适应 Qt 5,我们选用了 QNetworkAccessManager

尽管 QNetworkAccessManager不是一个单例,但是 Qt 文档中有这么一句:One QNetworkAccessManager should be enough for the whole Qt application. 也就是说,在我们的实际代码中,完全可以将其当做一个单例来使用。这样,我们可以创建一个单例类,将 QNetworkAccessManager放置其中即可。这里,我们设计一个自己的NetworkManager类,作为这里的单例类。具体的实现细节暂不关心,先看看如何进行网络通讯。

XML-RPC 协议要求使用 POST 方法提交数据。所以我们网络操作的核心函数就是

QNetworkAccessManager::post(const QNetworkRequest & request, QIODevice * data)

这个函数接受两个参数,第一个是QNetworkRequest对象;第二个参数就是实际传输的数据。下面我们需要分别提供这两个参数值。

首先构建QNetworkRequest对象。注意,XML-RPC 协议要求 User-Agent 必须提供,数据内容类型是 XML,因此,我们使用下面的代码来构建:

QNetworkRequest NetworkManager::networkRequest(const QString &url,
                                               const QString &userAgent) const
{
    QNetworkRequest req;
    req.setUrl(QUrl(url));
    req.setRawHeader("User-Agent", userAgent.toAscii());
    req.setHeader(QNetworkRequest::ContentTypeHeader, "text/xml");
    return req;
}

第二个参数实际就是我们使用QVariant转换得到的 XML 格式的数据。

由于 QNetworkAccessManager的网络操作都是异步的,所以我们需要连接 finished(QNetworkReply*)信号获得服务器返回的数据。这里,由于我们使用的是自己封装的NetworkManager,所以,我们直接将这个信号再次发送出去:

connect(m_mgr, SIGNAL(finished(QNetworkReply*)),
               SLOT(onRequestFinished(QNetworkReply*)));
// ...
void NetworkManager::onRequestFinished(QNetworkReply *reply)
{
    emit requestFinished(reply);
}

我们的客户端则需要连接到这个信号,以便进行返回值的处理:

connect(m_manager, SIGNAL(requestFinished(QNetworkReply*)),
                   SLOT(requestFinished(QNetworkReply*)));
// ...
void XmlRpcClient::requestFinished(QNetworkReply *reply)
{
    QString response;
    if(reply->error() == QNetworkReply::NoError) { // step 1
        response = QString::fromUtf8(reply->readAll());
    } else {
        response = faultString(-32300, reply->errorString()); // ex 1
    }

    QVariant value;
    int errCode;
    QString errMessage;
    QString methodName = m_requests.value(reply); // ex 2
    if(XmlRpcResponse(response).parse(&value, &errCode, &errMessage)) { // step 2
        emit finished(methodName, value);
    } else {
        emit fault(methodName, errCode, errMessage);
    }
    m_requests.remove(reply);
    reply->deleteLater(); // step 3
}

注意来看requestFinished()slot 中标记了 step 的三个语句。第一,如果没有错误,我们将 reply 以 UTF-8 的格式全部读取出来,赋值给一个QString。第二,利用XmlRpcResponse::parse()函数对这个QString进行处理。第三,删除这个 reply 对象。这三个语句是整个处理的主体。下面来看看 XmlRpcResponse::parse()是怎样的:

bool XmlRpcResponse::parse(QVariant * value, int * errCode, QString * errMessage)
{
    QDomDocument doc;
    QString errorMsg;
    int errorLine;
    int errorColumn;

    if(!doc.setContent(m_response, &errorMsg, &errorLine, &errorColumn)) {
        value = 0;
        *errCode = NotWellFormed;
        *errMessage = QString("Parse error: not well-formed at line %1: %2.")
                          .arg(errorLine).arg(errorMsg);
    } else {
        if(doc.documentElement().firstChild()
              .toElement().tagName().toLower() == "params") {
            QDomNode paramNode = doc.documentElement().firstChild().firstChild();
            if(!paramNode.isNull()) {
                *value = XmlRpcValue::fromXml(paramNode.firstChild().toElement());
            }
            return true;
        } else if(doc.documentElement()
                     .firstChild().toElement().tagName().toLower() == "fault") {
            QMap errors = XmlRpcValue::fromXml(doc.documentElement()
                                                  .firstChild().firstChild()
                                                  .toElement()).toMap();
            value = 0;
            *errCode = errors.value("faultCode").toInt();
            *errMessage = errors.value("faultString").toString();
        } else {
            value = 0;
            *errCode = InvalidXmlRpc;
            *errMessage = QObject::tr("Parse error: invalid XML-RPC.");
        }
    }
    return false;
}

XmlRpcResponse构造函数接受一个QString参数,这个QString就是前面我们从 reply 中获取到的,也就是 XML-RPC 服务器返回的 XML 格式的字符串。这里实际就是按照 XML-RPC 协议的内容,对服务器返回的 XML 数据进行分析。我们调用了 XmlRpcValue::fromXml()函数,从而可以利用前面所说的技术,将服务器返回的 XML 数据转换成QVariant

下面再次回到前面的 requestFinished()函数。在标记了 ex 1 这行,我们构建了一个 XML 字符串。使用的代码如下所示:

QString XmlRpcClient::faultString(int code, const QString & message) const
{
    QDomDocument doc;
    QDomProcessingInstruction header = doc.createProcessingInstruction("xml",
                                         "version=\"1.0\" encoding=\"UTF-8\"");
    doc.appendChild(header);
    QDomElement methodResponse = doc.createElement("methodResponse");
    doc.appendChild(methodResponse);
    QDomElement fault = doc.createElement("fault");
    methodResponse.appendChild(fault);
    QMap faultInfo;
    faultInfo.insert("faultCode", code);
    faultInfo.insert("faultString", message);
    fault.appendChild(XmlRpcValue::toXml(faultInfo));
    return doc.toString();
}

由此我们构建了一个错误字符串,从而能够利用XmlRpcValue::fromXml()函数,统一获得QVariant对象。

在标记了 ex 2 的这行,我们是利用了一个 QMap<QNetworkReply *, QString>对象。这是由于,我们的网络通讯底层利用的是 QNetworkAccessManager执行异步操作。异步操作虽然不会将界面锁死,但带来的影响是,我们无法预计数据返回的先后顺序,无法知道返回的 slot 对应的是哪个 request。这就需要我们自己记录下已经发送的函数名,所以我们的散列表以 reply 为键,以QString形式的函数名为值:

QNetworkReply * XmlRpcClient::request(const QString & url,
                                      const QString methodName,
                                      const QVariantList &params)
{
    QNetworkReply * reply = m_manager->post(url,
                                XmlRpcRequest(methodName, params).data(),
                                m_userAgent);
    m_requests.insert(reply, methodName);
    return reply;
}

由于 Qt 保证发送时返回的 reply 和finished(QNetworkReply*)signal 中的 reply 是同一个,因此我们根据这个便可以区别这个返回的 slot 对应的是哪一个 method。下面再来看看这个 request 是怎么实现的:

QByteArray XmlRpcRequest::data() const
{
    QDomDocument doc;

    QDomProcessingInstruction header = doc.createProcessingInstruction("xml",
                                         "version=\"1.0\" encoding=\"UTF-8\"");
    doc.appendChild(header);

    QDomElement params = doc.createElement("params");
    QDomElement param;
    foreach(QVariant var, m_params){
        param = doc.createElement("param");
        param.appendChild(XmlRpcValue::toXml(var));
        params.appendChild(param);
    }

    QDomElement methodName = doc.createElement("methodName");
    methodName.appendChild(doc.createTextNode(m_methodName));

    QDomElement methodCall = doc.createElement("methodCall");
    methodCall.appendChild(methodName);
    methodCall.appendChild(params);

    doc.appendChild(methodCall);

    return doc.toString().toUtf8();
}

其中,m_methodNamem_params在构造时传入,前者是QString类型的远程调用的函数名,后者是QVariantList类型的参数列表。我们使用XmlRpcValue::toXml()函数,将其转换成 XML 格式。

至此,我们已经实现了简单的 XML-RPC 客户端。如果有任何问题,请留言与我联系。

4 Comments

  1. kidrsong 2012年6月17日
  2. adfae 2012年11月25日
    • DevBean 2012年11月25日
  3. 杨文 2015年7月16日

Leave a Reply