前面我们已经了解了 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 ¶ms) { 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_methodName
和m_params
在构造时传入,前者是QString
类型的远程调用的函数名,后者是QVariantList
类型的参数列表。我们使用XmlRpcValue::toXml()
函数,将其转换成 XML 格式。
至此,我们已经实现了简单的 XML-RPC 客户端。如果有任何问题,请留言与我联系。
4 评论
最近在做一个相似的东西,在此学习一下
connect(m_manager, SIGNAL(requestFinished(QNetworkReply*)),
SLOT(requestFinished(QNetworkReply*)));
请问这段代码是什么意思?自己发信号给自己?
是的,相当于connect(m_manager, SIGNAL(requestFinished(QNetworkReply*)), this, SLOT(requestFinished(QNetworkReply*)));
有源代码么,这里好乱,好多变量不知从哪里来的