前面我们已经了解了 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 客户端。如果有任何问题,请留言与我联系