• Nem Talált Eredményt

Feltöltés a WebSynchronizer osztállyal – a QNetworkAccessManager és egyéb

In document Qt strandkönyv (Pldal 100-107)

13. Hálózati szinkronizáció: a ToDoList felhőcskéje

13.3. Feltöltés a WebSynchronizer osztállyal – a QNetworkAccessManager és egyéb

QNetworkAccessManager és egyéb hálózati örömök

Hogy is szeretnénk mi ezt pontosan? Készítsünk egy WebSynchronizer osztályt, amelynek két publikus tagfüggvénye van – mármint a konstruktoron kívül, persze. Az egyik feladata

26. ábra: Az új beállítás-ablak

Qt strandkönyv ǀ 13. Hálózati szinkronizáció: a ToDoList... ǀ

a fájl lemezre való mentését követően feltölteni a webszerverre, a másiké meg az, hogy a fájl megnyitása előtt letölti az időbélyeget, és ha az időbélyeg szerint a webszerveren lévő feladatlista-fájl az újabb, akkor letölti azt is, és felülírja vele a helyi példányt. A

WebSynchronizer osztályt majd a fileOperator objektumban példányosítjuk. Mentéskor-betöltéskor megnézzük a beállításfájlt, és ha kell szinkronizálni, akkor hívjuk a megfelelő tagfüggvényt. (Ismét megjegyezzük, hogy a feladat jelenlegi megvalósítása sok kivetnivalót hagy maga után. Ha például a letöltés során félig jön csak le a fájl, azzal is felülírjuk a helyi példányt, és akkor aztán használható fájl nélkül maradunk, ami az álmoskönyv szerint semmi jót nem jelent. Amikor mindezt majd élesben csináljuk, sokkal gondosabban kell eljárnunk.)

Adjuk hát hozzá a WebSynchronizer osztályt a projekthez. Az őse lehet egy mezei QObject, semmi bonyolultabbra nincs szükségünk. Deklaráljuk benne az alábbi két publikus tagfüggvényt:

void syncFileBeforeOpen(QString fileName, QString webAddress);

void syncFileAfterSave(QString fileName, QString webAddress);

Helyezzük el a megvalósításukat – egyelőre üres törzzsel – a websynchronizer.cpp fájlban, s ezt követően a FileOperator osztály konstruktorában példányosítsunk magunknak egyet az új osztályból, mégpedig webSync néven, a heap-re:

webSync = new WebSynchronizer(this);

Ezt követően battyogjunk le a performFileOpenOperation() tagfüggvényhez, és valahol az elején, de még mindenképp azelőtt, hogy a helyi fájllal babrálni kezdenénk, helyezzük el az alábbi sorokat:

//webSync

if(settings.value("Web/SyncEnabled", "false").toBool())

webSync‑>syncFileBeforeOpen(fileName, settings.value("Web/

Address").toString());

//webSync done

Szívünk titkos zugai elárulják nekünk, hogy a következő teendőnk az lesz, hogy hasonló sorokat helyezünk el a performFileSaveOperation() tagfüggvényben is, de ezúttal olyan helyre, ahol már nem fogunk babrálni a helyi fájllal. Ilyen hely például az if(success) teljesülése esetén lefutó ág. Az elhelyezendő sorok a következők:

//webSync

if(settings.value("Web/SyncEnabled", "false").toBool())

webSync‑>syncFileAfterSave(fileName, settings.value("Web/

Address").toString());

//webSync done

Aki most a WebSynchronizer osztály két meglévő tagfüggvényében elhelyez egy-egy qDebug() üzenetet, az figyelemmel kísérheti, hogy milyen remekül lefutnak a tagfüggvények.

Jah, kérem, hogy semmi értelmeset nem csinálnak? Kicsinység!

Illetve nem is. Talán mégis inkább meg kéne őket csinálni rendesen. Kezdjük a feltöltéssel, már csak azért is, hogy utóbb legyen mit letölteni.

A feltöltés során ugyebár két fájlt kell majd feltöltenünk. Az egyik maga a feladatlista-fájl, a másik az időbélyeget tartalmazó párja. Ismétlődő feladatra leltünk, azaz mehetünk tagfüggvényt írni. A neve legyen a fantáziadús uploadFile(). Legyen mondjuk két argumentuma, a fájlnév és a webcím. Eddig tehát a syncFileAfterSave() hívja kétszer az uploadFile()-t.

Qt strandkönyv ǀ 13. Hálózati szinkronizáció: a ToDoList... ǀ

Hogy is történik maga a feltöltés? Az úgynevezett űrlap-alapú feltöltés móddal dolgozunk, amelynek a módját a 1867-es RFC írja le.27

Eszerint elküldünk egy kérést a webszervernek, ebben tudtára adjuk, hogy mi épp adatot akarunk küldeni neki. Eláruljuk neki, hogy milyen határ (boundary) fogja jelezni az adat elejét és végét, aztán az orrára kötjük azt is, hogy milyen hosszú lesz az adat. Az adat ebben az esetben nem csak magát a fájlt jelöli, mint az hamarosan nyilvánvalóvá válik.

Az adat hosszának megadását követően pedig jön az adat: művelet kezdetét jelölő boundary, majd megmondjuk, hogy mit kell vele csinálni – mi majd azt mondjuk, hogy oda kell adni az upload.php-nek. Ezt követi megint egy boundary, aztán eláruljuk a fájl nevét, a fájl típusát, majd jöhet maga a fájl, végül a lezáró boundary.

Az előző két bekezdés a Qt esetében is élesen elválik. A kérés (a kódunkban: request) után következik az adattömb (array). Az adattömb legyártását megint külön tagfüggvényre bízzuk, így jobban elkülönül a dolog logikája. Azaz a függvényhívási lánc úgy egészül ki, hogy az uploadFile() tagfüggvény minden futása alkalmával egyszer meghívja a createArray() tagfüggvényt.

Régen nyúlkáltunk már a ToDoList.pro fájlban, itt az ideje némi turkálásnak ismét.

Keressük meg azt a sort, amelyben a Qt keretrendszer általunk használt részeit soroljuk fel, és egészítsük ki a network szóval. Szóval:

QT += core gui network

A Qt-ban a hálózati műveleteket egy, a QNetworkAccessManager nevű osztályból

példányosított objektummal szoktuk elvégeztetni. Egy példány elég az egész alkalmazásnak. A WebSynchronizer osztály konstruktorában példányosítsunk magunknak egyet:

networkManager = new QNetworkAccessManager(this);

A networkManager objektum post() tagfüggvényét használjuk arra, hogy elküldjük a kérést, illetve az adattömböt. A kérés QNetworkRequest osztályú objektum lesz, amely konstruktorának kötelező paramétere az URL, ahova a kérés irányul. Ha készen vagyunk a kérés példányosításával, be kell állítanunk a tartalomtípusra és az adattömb hosszára fejléceket, majd elküldeni az egészet a webszervernek. A webszerver válasza egy QNetworkReply

osztályú objektumként érkezik meg.

Akkor lássuk mindezt megvalósítva:

27 A hálózati műveletek mikéntjeit RFC-nek nevezett kváziszabványokban szokás megfogalmazni.

RFC-ből sok van, számmal azonosítják őket. A bennünket érdeklő RFC szövege a következő

cí-Qt strandkönyv ǀ 13. Hálózati szinkronizáció: a ToDoList... ǀ

void WebSynchronizer::uploadFile(QString fileName, QString webAddress) { QByteArray array = createUploadArray(fileName);

if(!array.isEmpty()){

QUrl URL = QUrl("http://" + webAddress + "/upload.php");

QNetworkRequest request(URL);

request.setHeader(QNetworkRequest::ContentTypeHeader,

"multipart/form‑data; boundary=margin");

request.setHeader(QNetworkRequest::ContentLengthHeader, QString::number(array.length()));

QNetworkReply *uploadReply = networkManager‑>post (request,array);

QEventLoop loop;

connect(uploadReply, SIGNAL(finished()), &loop, SLOT(quit()));

loop.exec(); //wait until upload finished if(uploadReply‑>error())

qDebug() << uploadReply‑>errorString() << uploadReply‑>

error();

delete uploadReply;

} }

A tagfüggvény első sorában elkészítjük az adattömböt, benne a feltöltendő fájllal (mindjárt megmutatjuk, hogy miként). Ha a tömb nem üres – ami akkor következhet be, ha a helyi fájlt nem sikerült megnyitni –, nekikezdünk a feltöltés előkészítéséhez. Legyártjuk az URL-t, majd ennek felhasználásával példányosítjuk az a QNetworkRequest osztályú objektumot, amelyiknek a példányosítást követő két sorban beállítjuk a fejléceit. A már sokat emlegetett boundary neve lesz a margin. Lehetne épp bármi más, az RFC csak annyit köt ki, hogy az adatban ne forduljon elő. Hát, a mi tesztadatainkban nem fog, és punktum. Figyeljük meg, ahogy az adattömb hosszát is elhelyezzük a megfelelő helyen.

Az uploadReply nevű mutató jelzi a webszerver válaszát a networkManager objektum által kezdeményezett beszélgetésre, melynek során az előkészített kérést és az adattömböt HTTP POST kérésként küldtük el a webszervernek.

A válasz nem érkezik meg azonnal, és lehet, hogy nagyon soká jön majd, mert például lassú a hálózat. Több módszer is volna arra, hogy csak akkor nyúljunk a válaszhoz, amikor már megér-kezett. Mindegyik azon alapul, hogy amikor visszaért a válasz, a QNetworkReply objektum egy finished() signal-t emittál. Ezt a signal-t köthetnék ahhoz a slot-hoz, amelyik feldolgozza a vá-laszt, de talán a Qt hálózati programozásában még nem profik számára könnyebben követhető, ha inkább indítunk egy eseményhurkot, amit az uploadReply objektumból érkező finished() signal megérkeztekor megszakítunk. Magyarán: megvárjuk, amíg visszaér a válasz.

Ha visszaért, akkor feldolgozzuk. Ha hibát tartalmaz, akkor mind a hibát, mind a hibakódot kiírjuk. A kiírásnak később még keresünk jobb helyet, egyelőre megteszi a qDebug().

Végül töröljük a szükségtelenné vált QNetworkReply osztályú objektumot az uploadReply mutató végéről. Kifacsartuk, elvettünk mindent, amit adhatott, és most eldobjuk, igazi szívtipró módjára.

Qt strandkönyv ǀ 13. Hálózati szinkronizáció: a ToDoList... ǀ

Ha értjük, hogyan működik majd a feltöltésnek ez a része, akkor lássuk a createUploadArray() tagfüggvényt. Lényegesen egyszerűbb lesz, ígérjük.

QByteArray WebSynchronizer::createUploadArray(QString fileName) { QFile file(fileName);

QByteArray array;

if (file.open(QIODevice::ReadOnly)){

array.append("‑‑margin\n");

array.append("Content‑Disposition: form‑data; name=\"action\"

\n\n");

array.append("upload.php\n");

array.append("‑‑margin\n");

array.append("Content‑Disposition: form‑data;

name=\"uploaded\"; filename=\"" + fileName + "\"\n");

array.append("Content‑Type: text/tdolst\n\n");

array.append(file.readAll());

array.append("\n");

array.append("‑‑margin‑‑\n");

}

return array;

}

A QByteArray remek kis osztály, de a mi szempontunkból jelenleg csak annyi az érdekes belőle, hogy tulajdonképp bájtsorozatokat tárolhatunk a belőle példányosított objektumokban.

Ha sikerült megnyitni a fájlt, akkor beírunk mindenféle okosságokat az adattömb elejére, majd a QFile::readAll() tagfüggvény hívásával az adattömb közepére bepakoljuk az egész fájlt. Utóbb még pár okosságot fűzünk az adattömböz, és az egészet visszaadjuk a hívónak.

Ha nem sikerült megnyitni a fájlt, akkor üres objektumot adunk vissza, ezt az információt az uploadFile() tagfüggvényben ki is használjuk.

A syncFileAfterSave() tagfüggvény törzse egyelőre egyetlen sor:

uploadFile(fileName, webAddress);

Mikor mindezzel elkészültünk, futtassuk a művünket. A mentés ikonra kattintva elvileg megtörténik a feltöltés – látnunk kell a webszerver naplófájljaiban is, illetve a megfelelő könyvtárban meg kell jelennie a feltöltött fájlnak.

Azért ez elég sok buktatós feladat volt, főleg azok számára, akik először telepítettek webszervert, először futtatnak PHP-parancsfájlt. Úgyhogy ha tényleg felment a fájl, egy bátortalan és kételkedő „Hurrráááááá!!!” igencsak helyénvaló.

Most pedig... Nem engedünk a csábításnak, nem nyargalunk letöltést írni, hanem először úgy istenigazából rendberakjuk a feltöltést.

Azzal kezdjük a nagy rendberakást, hogy a syncFileAfterSave() tagfüggvényben legyártjuk és feltöltetjük az időbélyeget is. A tagfüggvény egysoros törzsét egészytük ki az alábbi néhány sorral:

Qt strandkönyv ǀ 13. Hálózati szinkronizáció: a ToDoList... ǀ

QString timeStampFileName = fileName + ".timestamp";

QFile file(timeStampFileName);

if (file.open(QFile::WriteOnly | QFile::Truncate | QFile::Text)) { QTextStream out(&file);

QFileInfo fi(fileName);

out << fi.lastModified().toString(Qt::ISODate);

}

if(!file.error())

uploadFile(timeStampFileName, webAddress);

Elvileg egyetlen újdonságot találunk a fentiekben. Az időbélyeget tartalmazó fájl nevét az eredeti fájlnév kiegészítésével képezzük. Előbb legyártjuk itt helyben a fájlt, amelybe a QFileInfo osztály lastModified() tagfüggvényével lekérdezett QDateTime osztályú választ írjuk bele, de a beleírás előtt a választ karakterlánccá alakítjuk, mégpedig az ISO által meghatározott formátumúra. Ha a fájl adathordozóra való írása sikeres volt, az uploadFile() tagfüggvény második hívásával utánaküldjük a feladatlistát tartalmazó fájlnak.

A rendberakást ott folytatjuk, hogy kiépítünk egy mechanizmust, melynek használatával a felhasználót a főablak állapotsorában értesítjük a hálózati műveletek sikeréről, illetőleg balsikeréről.

A WebSynchronzier osztályú webSync objektum a fileOperator objektum privát objektuma, azaz a főablak nem látja, így a singal-jait sem hallhatja. Készítsük hát fel a

FileOperator osztályt az üzenet továbbítására. Definiálunk egy tulajdonságot, úgy, ahogy azt nem is olyan rég megtanultuk:

Q_PROPERTY(QString lastWebSyncMessage READ lastWebSyncMessage WRITE setLastWebSyncMessage NOTIFY lastWebSyncMessageChanged)

Elhelyezzük az m_lastWebSyncMessage nevű, QString osztályú objektumot tárolni képes provát változót, deklaráljuk a void lastWebSyncMessageChanged(QString message);

signal-t, majd megírjuk a tagfüggvényeket:

QString FileOperator::lastWebSyncMessage() { return m_lastWebSyncMessage;

}

void FileOperator::setLastWebSyncMessage(QString message) { m_lastWebSyncMessage = message;

emit lastWebSyncMessageChanged(message);

}

Fontos, hogy a beállítófüggvényt ne sima függvényként, hanem publikus slot-ként adjuk meg, mert majd ezt a tagfüggvényt fogja hívni a webSync objektum.

A főablakot készítsük fel a fileOperator objektum felől érkező üzenet vételére, illetve az állapotsoron való megjelenítésére. Szükségünk lesz egy publikus slot-ra:

void MainWindow::lastWebSyncMessageChanged(QString message) { ui‑>statusBar‑>showMessage(message, 4000);

}

Qt strandkönyv ǀ 13. Hálózati szinkronizáció: a ToDoList... ǀ

A szám az üzenet után azt mondja meg ezredmásodpercben, hogy mennyi idő után tűnjön el az üzenet az állapotsorról. Ha 0-t adunk meg, akkor a következő üzenetig ott marad. A főablak preparationAtStartUp() tagfüggvényében elhelyezzük a connect utasítást:

connect(fileOperator, SIGNAL(lastWebSyncMessageChanged(QString)), this, SLOT(lastWebSyncMessageChanged(QString)));

Mostanra a fileOperator objektum és a főablak között zavartalan az információáramlás. A WebSynchronizer osztályban ténykedünk tovább. Deklarálunk egy signal-t:

void syncReport(QString);

Eddig az uploadFile() tagfüggvényben a hálózati hibát egy qDebug()-kiírással jeleztük. Ezt cseéljük le most, ráadásul akkor is beszélünk, ha minden rendben ment. Íme a teljes if-utasítás:

if(uploadReply‑>error())

emit syncReport(uploadReply‑>errorString() + " (code:" + QString::number(uploadReply‑>error()) + ")");

else if(fi.suffix() != "timestamp")

emit syncReport("Network upload (" + fi.fileName() + ") just happened.");

Ha volt hiba, akkor kiírjuk, ha nem, akkor megmondjuk, hogy mit töltöttünk fel, de csak akkor, ha feltöltött fájl kiterjesztése nem „timestamp”. Azért hallgatunk mélyen az időbélyeg-fájl feltöltéséről, mert rögtön a feladatlista-időbélyeg-fájl feltöltése után történik meg, és így az üzenetek olyan gyorsan követnék egymást, hogy nem volna időnk megnézni az állapotsoron az első, számunkra fontosabb üzenetet.

A fi, amelyet használunk, egy QFileInfo objektum, amit persze a tagfüggvény elején inicializálni kell:

QFileInfo fi(fileName);

A kiterjesztés ellenőrzésén felül azért van rá szükségünk, hogy a fileName változóból gyorsan és fájdalommentesen le tudjuk csippenteni az elérési utat – ha nem csippentenénk le, nem férne el az állapotsoron az üzenet.

Az egész hálózati művelethez csak akkor fogunk hozzá a tagfüggvényben, ha nem üres a createUploadArray() tagfüggvénytől visszakapott tömb. Erre szolgál a tagfüggvény eleje felé található

if(!array.isEmpty())

ellenőrzés. Említettük már, hogy üres tömböt akkor kapunk vissza, ha nem sikerült a helyi fájl megnyitása. Ha ilyen malőr történne, azt is tudjuk közölni a felhasználóval, mégpedig úgy, hogy megírjuk a fenti if utasítás else-ágát:

}else

emit syncReport("Local file error while syncing " + fi.fileName());

Kicsit tesztelhetjük a programot: érdemes rossz URL-t megadni a beállítások ablakban, vagy lekapcsolni a webszervert, vagy eltenni az upload.php-t máshova. Ha minden jól megy, sok szép új hálózati hibaüzenetet és -kódot ismerünk majd meg. Az izgalmasabbakat felolvashatjuk a hörcsögnek is, miközben a félig becsukott markunkból figyeli a monitort, és csak a

bajszocskája mozog izgalmában. A feltöltéssel végeztünk, kezdődhet a

Qt strandkönyv ǀ 13. Hálózati szinkronizáció: a ToDoList... ǀ

13.4. Letöltés a WebSynchronizer osztállyal – a QNetworkReply

In document Qt strandkönyv (Pldal 100-107)