هذه الفترة احتجت ان ارسل HTTP Post Request باستخدام Qt واقوم بارسال ملفات للسيرفر (عباره عن شهاده
رقمية Certificate للكلاينت بالاضافه الى بعض الملفات الأخرى) ، وايضاً احتجت ان اقوم بعمل Get Request من السيرفر وارسل ID
ومن خلاله سوف احصل على هذه الملفات التي قمت برفعها في JSON،، وأحببت وضع هذه التدوينه لعلها
تفيد من يريد القيام بنفس المهمه,
في كيوت الكلاس QNetworkAccessManager هو المسؤول عن ارسال والاستقبال الطلبات في الشبكه،
وفي الغالب سوف تحتاج لQNetworkAccessManager واحد في التطبيق، ويحتوى الكلاس على signals تفيد
بان العملية جاري او انها انتهت وايضاً بها دوال لأخذ الطلب الذي تريد وبعض
البيانات الأخرى وترجع QNetworkReply والذي يحتوي على النتيجة الذي ارجعها السيرفر عندما يعالج الطلب.
المثال البسيط الذي تجده في التوثيق Documentation:
QNetworkAccessManager *manager = new QNetworkAccessManager(this); connect(manager, SIGNAL(finished(QNetworkReply*)),this, SLOT(replyFinished(QNetworkReply*))); manager->get(QNetworkRequest(QUrl("https://qt.nokia.com")));
هذا المثال يوضح ان ارسلت Get Request وعندما ترجع النتيجة سوف يتم استدعاء replyFinished وحينها يمكنك طباعه النتيجة او حتى مشاهده ال header الخاص بهذا ال response. من المهم القيام بحذف هذا الناتج QNetworkReply لذلك يمكنك استخدام الدالة delelteLater في الslot :
void MainWindow::replyFinished(QNetworkReply *reply) { if (reply) { if (reply->error() == QNetworkReply::NoError) { qDebug() << "Server replay now: " << QString::fromUtf8(reply->readAll()); } else { //get http status code int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); qDebug() << "Error: " << httpStatus; } reply->deleteLater(); } }
من المهم معرفه ان دوال الكلاس هي Asynchronous وهي تعني ان الطلب سوف يتم عمله في Thread أخر وعندما ترجع النتيجة سوف يتم استدعاء ال slot الذي قمت بربطه مع ال signal ، لذلك تنفيذ الكود سوف يستمر بعد استدعاء الدالة get، ايضاً الطلبات التي تقوم بعملها سوف توضع في Queue حتى يتم معالجتها ولل HTTP فسوف يتم معالجة 6 طلبات في اللحظة الواحدة من هذا ال queue.
لكن في بعض الأحيان نظراً لتصميم الكلاس أو لسبب أخر هو أنك تريد الحصول على النتيجة فوراً Synchronous بمعنى عندما تستدعي الدالة فلا تريد ان ترجع الا مع النتيجة لذلك في هذه الحالة يمكنك تحريك الEvent loop حتى تتم معالجة هذا الطلب:
QString url = ""; QNetworkReply *reply = this->manager->get(QNetworkRequest(QUrl(url))); QEventLoop loop; QObject::connect(reply, SIGNAL(readyRead()), &loop, SLOT(quit())); loop.exec(); QString result(reply->readAll()); reply->deleteLater();
في الاكواد السابقة كنا نقوم بعمل ربط QNetworkAccessManager مع الfinished Signal والأمر يعمل بشكل طبيعي، ولكن بعض الأحيان قد تحتاج أن تتصل على السيرفر وتحصل على النتيجة وبناء على تلك النتيجة تقوم بعمل طلب أخر، باستخدام ال Signal السابقة فسوف يتم استدعاء الدالة replyFinished مرة أخرى عندما تقوم بعمل الطلب والسبب هو انك كل الطلبات التي سوف تتم سوف يتم استدعاء هذا الslot:
connect(m_networkManager, SIGNAL(finished(QNetworkReply*)), this, SLOT(replyFinished(QNetworkReply *)));
لذلك من الأفضل ربطها بال QNetworkReply حيث بها نفس ال Signal وبالتالي لكل طلب يمكن ان تكون هناك slot خاصه فيه ، المثال:
void MyApp::getSocialNetwork() { QNetworkRequest request; request.setUrl(QUrl("https://www.socialnetwork.foo")); m_networkManager = new QNetworkAccessManager(this); QNetworkReply *reply = m_networkManager->get(request); connect(reply, SIGNAL(finished()), this, SLOT(onRequestCompleted())); } void MyApp::onRequestCompleted() { QNetworkReply *reply = qobject_cast<QNetworkReply *>(sender()); QByteArray data = reply->readAll(); QString userId = parseUserIdFromData(data); QString statusUpdate("Hello, World!"); QNetworkRequest request; QString requestUrl = QString("http://www.socialnetwork.foo/%1/%2").arg(userId).arg(statusUpdate); request.setUrl(QUrl(requestUrl)); QNetworkReply *reply = m_networkManager->get(request); connect(reply, SIGNAL(finished()), this, SLOT(onStatusUpdateCompleted())); } void MyApp::onStatusUpdateCompleted() { // Do something when we have updated the status. }
المثال اعلاه من المقال الرائع والذي يوضح لماذا يفضل ان تستخدم QNetworkReply في ال Signals:
http://www.johanpaul.com/blog/2011/07/why-qnetworkaccessmanager-should-not-have-the-finishedqnetworkreply-signal/
نتحدث الان عن ال HTTP POST وهناك طريقتين في Qt الأولى تعمل على نسخه 4.8 وما فوق باستخدام الكلاس QHttpMultiPart والثانية تعمل على النسخ القديمة والحديثة وذلك لأننا سنقوم ببناء ال POST Request يدوياً ونقوم بارساله ، لنفرض ان السيرفر به 3 مدخلات ويجب ان تدخل من الكلاينت:
المثال باستخدام QHttpMultiPart :
void MainWindow::uploadUsingQHttpPart() { QHttpMultiPart *multiPart = new QHttpMultiPart(QHttpMultiPart::FormDataType ); // Original Text QHttpPart textPart; textPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name="OriginalText"")); textPart.setBody("OriginalText"); // Signature QHttpPart sigPart; sigPart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream")); sigPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name="Signature" filename="a.bin"")); sigFile = new QFile("a.bin"); if ( !sigFile->open(QIODevice::ReadOnly) ) return ; sigPart.setBodyDevice(sigFile); sigFile->setParent(multiPart); // Certificate QHttpPart cerPart; cerPart.setHeader(QNetworkRequest::ContentTypeHeader, QVariant("application/octet-stream")); cerPart.setHeader(QNetworkRequest::ContentDispositionHeader, QVariant("form-data; name="Certificate" filename="a.cer"")); file = new QFile("a.pdf"); if ( !file->open(QIODevice::ReadOnly) ) return ; cerPart.setBodyDevice(file); file->setParent(multiPart); multiPart->append(cerPart); multiPart->append(sigPart); multiPart->append(textPart); QUrl url("https://localhost:20607/DocInfo/Create"); QNetworkRequest request(url); QNetworkAccessManager *networkAccessManager = new QNetworkAccessManager(this); connect(networkAccessManager, SIGNAL(finished(QNetworkReply*)),this, SLOT(sendReportToServerReply(QNetworkReply*))); QNetworkReply *reply = networkAccessManager->post(request,multiPart); multiPart->setParent(reply); }
اذا كنت تعمل على نسخ Qt 4.7 وما قبل فلن تستطيع استخدام هذا الكلاس وعليك القيام بالعملية كاملة ، المثال التالي يوضح كيفية بناء ال Request:
void MainWindow::uploadManualWay() { QString boundary = "---------------------------723690991551375881941828858"; // Certificate QByteArray data(QString("--" + boundary + "rn").toAscii()); data += "Content-Disposition: form-data; name="Certificate"; filename="a.cer"rn"; data += "Content-Type: application/octet-streamrnrn"; cerfile = new QFile("a.cer"); if (!cerfile->open(QIODevice::ReadOnly)) return; data += cerfile->readAll(); data += "rn"; // Signature QByteArray data(QString("--" + boundary + "rn").toAscii()); data += "Content-Disposition: form-data; name="Signature"; filename="a.bin"rn"; data += "Content-Type: application/octet-streamrnrn"; sigfile = new QFile("a.cer"); if (!sigfile->open(QIODevice::ReadOnly)) return; data += sigfile->readAll(); data += "rn"; // OriginalText data += QString("--" + boundary + "rn").toAscii(); data += "Content-Disposition: form-data; name="OriginalText"rnrn"; data += "OriginalTextrn"; data += "rn"; data += QString("--" + boundary + "--rn").toAscii(); QNetworkAccessManager *networkAccessManager = new QNetworkAccessManager(this); QNetworkRequest request(QUrl("https://localhost:20607/DocInfo/Create")); QString header = "multipart/form-data; boundary=" + boundary; request.setHeader(QNetworkRequest::ContentTypeHeader,header.toAscii()); request.setHeader(QNetworkRequest::ContentLengthHeader,data.size()); connect(networkAccessManager, SIGNAL(finished(QNetworkReply*)),this, SLOT(sendReportToServerReply(QNetworkReply*))); QNetworkReply *reply = networkAccessManager->post(request,data); cerfile->setParent(reply); sigfile->setParent(reply); }
اذا كنت تود بارسال HTTP POST بدون ان تحتوى على ملفات فيمكنك استخدام:
void MainWindow::uploadText(){ QUrl parameters; parameters.addQueryItem("OrgName","Wajdy Essam"); parameters.addQueryItem("OriginalText","this is an example"); parameters.addQueryItem("Signature","this is my sign"); parameters.addQueryItem("ExpiraryDate","2/2/2012"); parameters.addQueryItem("SignedDate","5/1/2013"); QNetworkRequest request(QUrl("https://localhost:20607/DocInfo/Create")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); QNetworkAccessManager *networkAccessManager = new QNetworkAccessManager(this); connect(networkAccessManager, SIGNAL(finished(QNetworkReply*)),this, SLOT(sendReportToServerReply(QNetworkReply*))); QNetworkReply *reply = networkAccessManager->post(request,parameters.encodedQuery()); }
طبعاً في حال ارسال بيانات نصية فال MIME Type يكون application/x-www-form-urlencoded وللبيانات الbinary يفضل ان استخدام multipart/form-data والسبب أن الأول قد يرمز البايت البايرني الواحد ب3 بايتات وهذا ما يجعله خيار غير جيد في ارسال البيانات binary. يمكنك معرفة المزيد من هنا مثلاً: http://stackoverflow.com/questions/4007969/application-x-www-form-urlencoded-or-multipart-form-data
في الاخير يمكنك عمل Wrapper يسهل لك كل هذا ويجعل الامر اكثر تلقائية بدل من تحديد اسماء الحقول في دالة الارسال ، يمكن عمل الأتي:
#ifndef FORMDATA_H #define FORMDATA_H #include <QPair> #include <QList> class Field { public: Field(QString n, QString v); QString getName() const; QString getValue() const; private: QString name; QString value; }; class FileField: public Field { public: FileField(QString name, QString value, QString mime); QString getMimeType() const; private: QString mimeType; }; class FormData { public: FormData(); void addField(QString feildName, QString fieldValue); void addFile(QString fieldName, QString filepath, QString mimType); QList<Field> getData() const; QList<FileField> getFiles() const; private: QList<Field> data; QList<FileField> files; }; #endif // FORMDATA_H
ال Implementation :
#include "formdata.h" Field::Field(QString n, QString v) { this->name = n; this->value = v; } QString Field::getName() const { return this->name; } QString Field::getValue() const { return this->value; } FileField::FileField(QString name, QString value, QString mime) :Field(name, value){ this->mimeType = mime; } QString FileField::getMimeType() const { return this->mimeType; } FormData::FormData() { } void FormData::addField(QString feildName, QString fieldValue) { Field field(feildName, fieldValue); this->data.append(field); } void FormData::addFile(QString fieldName, QString filepath, QString mime) { FileField field(fieldName, filepath, mime); this->files.append(field); } QList<Field> FormData::getData() const { return this->data; } QList<FileField> FormData::getFiles() const { return this->files; }
ويمكن عمل Singleton Wrapper لل QNetworkAccessManager بحيث تستخدمه في أكثر من مكان:
#ifndef NETWORKMANAGER_H #define NETWORKMANAGER_H #include <QUrl> #include <QDebug> #include <QtNetwork/QNetworkAccessManager> #include <QtNetwork/QNetworkRequest> #include <QEventLoop> #include <QFile> #include <QtNetwork/QNetworkReply> #include "formdata.h" class NetworkManager: public QObject { Q_OBJECT public: static NetworkManager* instance(); QString getRequest(QString url); QString postRequest(QString url, const FormData& formData); private: NetworkManager(); static NetworkManager *networkManager; QNetworkAccessManager *manager; }; #endif // NETWORKMANAGER_H
وأخيراً ال Implementation , وكما تلاحظ فضلت ال Synchronous Way وخصوصاً ان الامر يتم بسرعه :
#include "networkmanager.h" NetworkManager *NetworkManager::networkManager = NULL; NetworkManager* NetworkManager::instance() { if ( networkManager == NULL ) { networkManager = new NetworkManager; } return networkManager; } NetworkManager::NetworkManager() { this->manager = new QNetworkAccessManager(this); } QString NetworkManager::getRequest(QString url) { QNetworkReply *reply = this->manager->get(QNetworkRequest(QUrl(url))); QEventLoop loop; QObject::connect(reply, SIGNAL(readyRead()), &loop, SLOT(quit())); loop.exec(); QString result = QString(reply->readAll()); reply->deleteLater(); return result; } QString NetworkManager::postRequest(QString url, const FormData& formData) { QString boundary = "---------------------------723690991551375881941828858"; QString header = "multipart/form-data; boundary=" + boundary; QByteArray data; // add all files foreach(FileField line, formData.getFiles()) { data += QString("--" + boundary + "rn").toUtf8(); data += QString("Content-Disposition: form-data; name="%1"; filename="%2"rn") .arg(line.getName()).arg(line.getValue()).toUtf8(); data += QString("Content-Type: %1rnrn").arg(line.getMimeType()).toUtf8(); QFile *file = new QFile(line.getValue()); if (!file->open(QIODevice::ReadOnly)) return ""; data += file->readAll(); data += "rn"; file->close(); } // add all text fields foreach(Field line, formData.getData()) { data += QString("--" + boundary + "rn").toUtf8(); data += QString("Content-Disposition: form-data; name="%1"rnrn").arg(line.getName()).toUtf8(); data += line.getValue().toUtf8(); data += "rnrn"; } data += QString("--" + boundary + "--rn").toUtf8(); QNetworkRequest request(QUrl(url.toUtf8())); request.setHeader(QNetworkRequest::ContentTypeHeader,header.toUtf8()); request.setHeader(QNetworkRequest::ContentLengthHeader,data.size()); QNetworkReply *reply = this->manager->post(request,data); QEventLoop loop; QObject::connect(reply, SIGNAL(readyRead()), &loop, SLOT(quit())); loop.exec(); QString result = QString(reply->readAll()); reply->deleteLater(); return result; }
استخدام الكلاس سوف يكون بهذه الطريقة:
FormData formData; formData.addField("OriginalText", QString::fromStdString(data)); formData.addFile("Signature", QString::fromStdString(SIGNATURE_PATH), "application/octet-stream"); formData.addFile("File", QString::fromStdString(CERTIFICATE_PATH), "application/octet-stream"); NetworkManager* manager = NetworkManager::instance(); QString serverResponse = manager->postRequest(QString::fromStdString(SERVER_POST_URL), formData);
بالنسبة ل Get فهو مجرد استدعاء الدالة Get الموجودة في ال Wrapper ، وبعد الحصول على JSON Result قد تريد عمل Parse لها ، واذا كانت البيانات بهذه الفورمات :
فيمكنك استخدام:
// your ouput filed struct JsonResult { QString text; QString certificate; QString signature; bool status; }; /** * this parser assume the following format of data * {text: "this is message", certificate: "base64 encoding of cer", * signature: "this base64 encoding of signature" } * * if server send null on any of them then it mean not correct input and * the result is false in JsonResult type */ JsonResult parseJSon(QString input) { QScriptEngine engine; QScriptValue sc = engine.evaluate("value = " + input); QScriptValueIterator it(sc); JsonResult jsonResult ; QString certificate, message, signature; while (it.hasNext()) { it.next(); QString header = it.name(); message = it.value().toString(); it.next(); certificate = it.value().toString(); it.next(); signature = it.value().toString(); jsonResult.text = message; jsonResult.certificate = certificate; jsonResult.signature = signature; if ( header != "text" || message == "null" || certificate == "null" || signature == "null" ) { jsonResult.status = false; break; } jsonResult.status = true; break; } return jsonResult; }
أرجوا أن يفيد هذا ال Wrapper ، ويمكن طرح الأسئلة والملاحظات ان وجدت،،
شكراً لوصولك لهذه النقطة ..
جزاكم الله كل الخير
ورجاء بعمل توضيح على موضوع الفاتورة الالكترونية واستخدام تقنيات Json والتوقيق الالكتروني Token