Hálózati alkalmazások
Hálózati alkalmazások QT osztályai A Qt támogatja az FTP és HTTP alapú hálózati alkalmazások készítését. A QFtp osztály segítségével lehet fájlokat megadott hálózati helyekre fel- vagy helyekről letölteni. A QHttp osztály felhasználásával web szerverek számára lehet kéréseket megfogalmazni és az azokra kapott válaszokat feldolgozni. A Qt az alacsony szintű hálózat-kezeléshez a QTcpSocket és a QUdpSocket osztályokat biztosítja, amelyek a TCP és UDP protokollokat implementálják. Mindkettő alkalmas hálózati kliens-szerver alkalmazások létrehozására. Az alkalmazások.pro fájljában szükség van az alábbi kapcsolóra: QT += network 2
FTP kliens A QFtp osztály az FTP protokoll kliens oldali implementációját adja. Különféle metódusokat biztosít az FTP parancsok végrehajtásához. A connecttohost(), login(), close(), list(), cd(), get(), put(), remove(), mkdir(), rmdir(), rename() metódusok mindegyike egy FTP parancsot vált ki, és visszaadja az FTP parancs azonosítóját. Bármely más FTP parancs a rawcommand() metódus segítségével kiadható ftp.rawcommand("site CHMOD 755 fortune"); A QFtp kiváltja a commandstarted(int) szignált, amikor egy parancsot el kezd végrehajtani, és a commandfinished(int, bool) szignált, amikor egy parancs befejeződött. Az int paraméter a parancs azonosítója. A QFtp osztály az FTP parancsok aszinkron működését támogatja. Amikor az összes befejeződik az összes kérés, kiváltja a done(bool) szignált. 3
1.Feladat Készítsünk egy olyan alkalmazást, amely segítségével egy távoli fájlt tudunk letölteni FTP protokollt használva. A program legyen egy konzol alkalmazás. nincs szükség a QtGui könyvtárra int main(int argc, char *argv[]){ QCoreApplication app(argc, argv); parancssor argumentumai QStringList args = app.arguments(); if (args.count()!= 2) { cerr << "Usage: ftpget url" << endl << "Example:" << endl << " ftpget ftp://ftp.trolltech.com/mirrors" << endl; return 1; saját osztály elindítja a fájl-letöltés FtpGet getter; if (!getter.getfile(qurl(args[1]))) return 1; QObject::connect(&getter, SIGNAL(done()), &app, SLOT(quit())); return app.exec(); a fájl-letöltés befejezése leállítja az alkalmazást 4
1.Feladat: tervezés FtpGet QObject - _ftp :QFtp - _file :QFile + FtpGet(QObject*) + getfile(qurl) «slot» - ftpdone(bool) 5
1.Feladat: megoldás FtpGet::FtpGet(QObject *parent) : QObject(parent){ connect(&_ftp, SIGNAL(done(bool)), this, SLOT(ftpDone(bool))); void FtpGet::ftpDone(bool error){ fájl művelet befejezésének lekezelése if (error) { cerr << "Error: " << qprintable(_ftp.errorstring()) << endl; else { cerr << "File downloaded as " << qprintable(_file.filename()) << endl; _file.close(); emit done(); 6
1.Feladat: megoldás bool FtpGet::getFile(const QUrl &url){ if (!url.isvalid()) { URL ellenőrzése cerr << "Error: Invalid URL" << endl; return false; if (url.scheme()!= "ftp") { cerr << "Error: URL must start with 'ftp:'" << endl; return false; if (url.path().isempty()) { cerr << "Error: URL has no path" << endl; return false; 7
1.Feladat: megoldás bool FtpGet::getFile(const QUrl &url){ QString localfilename = QFileInfo(url.path()).fileName(); if (localfilename.isempty()) localfilename = "ftpget.out"; _file.setfilename(localfilename); if (!_file.open(qiodevice::writeonly)) { lokális fájl nevének beállítása cerr << "Error: Cannot open " << qprintable(_file.filename()) << " for writing: " << qprintable(_file.errorstring()) << endl; return false; lokális fájl megnyitása _ftp.connecttohost(url.host(), url.port(21)); _ftp.login(); _ftp.get(url.path(), &_file); FTP műveletek _ftp.close(); return true; 8
2.Feladat Készítsünk egy olyan command-line alkalmazást, amely minden fájlt letölt egy FTP könyvtárból, sőt rekurzív módon annak alkönyvtáraiból is. int main(int argc, char *argv[]) nincs szükség a QtGui könyvtárra { QCoreApplication app(argc, argv); parancssor argumentumai QStringList args = app.arguments(); if (args.count()!= 2) { cerr << "Usage: spider url" << endl << "Example:" << endl << "spider ftp://ftp.trolltech.com/freebies/leafnode" << endl; return 1; saját osztály elindítja a fájl-letöltés Spider spider; if (!spider.getdirectory(qurl(args[1]))) return 1; QObject::connect(&spider, SIGNAL(done()), &app, SLOT(quit())); return app.exec(); a fájl-letöltés befejezése leállítja az alkalmazást 9
2.Feladat: tervezés Spider - _ftp :QFtp - _pendingdirs : QString - _openedfiles : Qlist<Qfile*> - _currentdir : QString - _currentlocaldir : QString + Spider(QObject*) + getdirectory(qurl) - processnextdirectory() «signal» - done() «slot» - ftpdone(bool) - ftplistinfo(qurlinfo) QObject letöltésre váró könyvtárak nevei memória szivárgás elkerüléséhez ezt az FTP get műveletével elkezdett letöltés befejezése váltja ki connect(&_ftp, SIGNAL(done(bool)), this, SLOT(ftpDone(bool))); connect(&_ftp, SIGNAL(listInfo(const QUrlInfo &)), this, SLOT(ftpListInfo(const QUrlInfo &))); ezt az FTP list művelete minden egyes listázott tételre kiváltja 10
2.Feladat: megoldás bool Spider::getDirectory(const QUrl &url) { if (!url.isvalid()) { ez indítja az adott könyvtárbeli fájlok letöltését cerr << "Error: Invalid URL" << endl; return false; if (url.scheme()!= "ftp") { cerr << "Error: URL must start with 'ftp:'" << endl; return false; _ftp.connecttohost(url.host(), url.port(21)); _ftp.login(); QString path = url.path(); if (path.isempty()) path = "/"; _pendingdirs.append(path); processnextdirectory(); return true; 11
2.Feladat: megoldás void Spider::processNextDirectory() { if (!_pendingdirs.isempty()) { _currentdir = _pendingdirs.takefirst(); _currentlocaldir = "downloads/" + _currentdir; QDir(".").mkpath(_currentLocalDir); _ftp.cd(_currentdir); _ftp.list(); else { emit done(); Ha van már fellelt, de feldolgozatlan könyvtár, akkor annak elemeit listázza, és mindegyikre kiváltja a listinfo(qurlinfo) szignált. 12
2.Feladat: megoldás void Spider::ftpListInfo(const QUrlInfo &urlinfo) { valahányszor egy könyvtárbeli if (urlinfo.isfile()) { tétel listázásra kerül if (urlinfo.isreadable()) { QFile *file = new QFile(_currentLocalDir +"/"+ urlinfo.name()); if (!file->open(qiodevice::writeonly)) { cerr << "Warning: Cannot open file " << qprintable(qdir::convertseparators(file->filename())) << endl; return; _ftp.get(urlinfo.name(), file); _openedfiles.append(file); ha normális fájl, akkor elkezdjük a letöltését else if (urlinfo.isdir() &&!urlinfo.issymlink()) { _pendingdirs.append(_currentdir + "/" + urlinfo.name()); ha könyvtár, akkor feljegyezzük 13
2.Feladat: megoldás void Spider::ftpDone(bool error) { if (error) { cerr << "Error: " << qprintable(_ftp.errorstring()) << endl; else { cout << "Downloaded " << qprintable(_currentdir) << " to " << qprintable(qdir::convertseparators( QDir(_currentLocalDir).canonicalPath())); qdeleteall(_openedfiles); _openedfiles.clear(); processnextdirectory(); itt van a rekurzió amikor az eddig elindított fájl letöltések befejeződtek Eltároltuk az eddig letöltésre kijelölt fájlokat, ezért most a letöltések befejeződésekor törölni tudjuk őket, így elkerüljük a memória szivárgást. 14
HTTP kliens A QHttp osztály a HTTP protokoll kliens oldali implementációját adja. Különféle metódusokat biztosít az FTP parancsok végrehajtásához. A sethost(), get(), post(), remove(), head() metódusok mindegyike egy HTTP parancsot vált ki. Bármely más FTP parancs a rawcommand() metódus segítségével kiadható ftp.rawcommand("site CHMOD 755 fortune"); A QHttp kiváltja a requeststarted(int) szignált, amely egy kérés végrehajtásakor keletkezik, és a requestfinished(int, bool) szignált, amikor a kérés befejeződött. Az int paraméter a kérés azonosítója. Amikor a kérések sora kiürül, akkor kiváltódik a done(bool) szignál. 15
3.Feladat Készítsünk egy olyan alkalmazást, amely segítségével egy távoli fájlt tudunk letölteni HTTP protokollt használva. A program legyen egy konzol alkalmazás. nincs szükség a QtGui könyvtárra int main(int argc, char *argv[]){ QCoreApplication app(argc, argv); parancssor argumentumai QStringList args = app.arguments(); if (args.count()!= 2) { cerr << "Usage: httpget url" << endl << "Example:" << endl << "httpget http://doc.trolltech.com/qq/index.html" << endl; return 1; saját osztály elindítja a fájl-letöltés HttpGet getter; if (!getter.getfile(qurl(args[1]))) return 1; QObject::connect(&getter, SIGNAL(done()), &app, SLOT(quit())); return app.exec(); a fájl-letöltés befejezése leállítja az alkalmazást 16
3.Feladat: tervezés HttpGet QObject - _http :QHttp - _file :QFile + HttpGet(QObject*) + getfile(qurl) «signal» - done() «slot» - httpdone(bool) 17
3.Feladat: megoldás HttpGet::HttpGet(QObject *parent) : QObject(parent){ connect(&_http, SIGNAL(done(bool)), this, SLOT(httpDone(bool))); fájl művelet befejezésének lekezelése void HttpGet::httpDone(bool error){ if (error) { cerr << "Error: " << qprintable(_http.errorstring()) << endl; else { cerr << "File downloaded as " << qprintable(_file.filename()) << endl; _file.close(); emit done(); 18
3.Feladat: megoldás bool HttpGet::getFile(const QUrl &url){ if (!url.isvalid()) { URL ellenőrzése cerr << "Error: Invalid URL" << endl; return false; if (url.scheme()!= "http") { cerr << "Error: URL must start with 'http:'" << endl; return false; if (url.path().isempty()) { cerr << "Error: URL has no path" << endl; return false; 19
3.Feladat: megoldás bool HttpGet::getFile(const QUrl &url){ QString localfilename = QFileInfo(url.path()).fileName(); if (localfilename.isempty()) localfilename = "httpget.out"; _file.setfilename(localfilename); lokális fájl nevének beállítása if (!_file.open(qiodevice::writeonly)) { cerr << "Error: Cannot open " << qprintable(_file.filename()) << " for writing: " << qprintable(_file.errorstring()) << endl; return false; lokális fájl megnyitása _http.sethost(url.host(), url.port(80)); _http.get(url.path(), &_file); _http.close(); HTTP műveletek return true; 20
TCP kliens-szerver alkalmazás A QTcpSocket és QTcpServer osztályok a TCP kliensek és szerverek implementációját adják. A TCP egy adatfolyam-orientált protokoll. A TCP-re épülő magasabb szintű protokollok tipikusan vagy sor-, vagy blokk-orientáltak. A sor-orientált protokollok soronként visznek át egy szöveget. A blokk-orientált protokollok bináris adatblokkokban. A QTcpSocket egy QDataStream vagy egy QTextStream felhasználásával végez írást illetve olvasás. A hálózatról történő olvasás esetén arra kell ügyelni, hogy szemben a szokásos fájlból olvasással a >> operator használata előtt meg kell győződni arról, hogy elegendő adatot fogunk-e letölteni. 21
4.Feladat Készítsünk egy utazást tervező alkalmazást. A Trip Planner kliens segítségével a felhasználó egy vonatútját tudja megtervezni. A Trip Server utazási információt nyújt a kliens számára az adott ország vasúti menetrendjéről. 22
4.Feladat: főprogramok int main(int argc, char *argv[]){ QApplication app(argc, argv); TripServer server; int main(int argc, char *argv[]){ QApplication app(argc, argv); TripPlanner tripplanner; tripplanner.show(); return app.exec(); if(!server.listen(qhostaddress::any, 6178)){ cerr << "Failed to bind to port" << endl; return 1; QPushButton quitbutton(tr("&quit")); quitbutton.setwindowtitle(tr("trip Server")); Connect(&quitButton, SIGNAL(clicked()), &app, SLOT(quit())); quitbutton.show(); return app.exec(); 23
4.Feladat: kliens tervezés Qdialog, Ui::TripPlanner TripPlanner - _tcpsocket :QTcpSocket - _nextblocksize :qint16 + TripPlanner(QWidget*) - closeconnection() «slot» - connecttoserver() - stopsearch() - updatetablewidget() - sendrequest() - connectionclosedbyserver() - error() felület két gombjának eseménykezelői szoket szignálok eseménykezelői 24
4.Feladat: kliens megoldás TripPlanner::TripPlanner(QWidget *parent): QDialog(parent) { setupui(this); QDateTime datetime = QDateTime::currentDateTime(); felület konfigurálása dateedit->setdate(datetime.date()); timeedit->settime(qtime(datetime.time().hour(), 0)); progressbar->hide(); progressbar->setsizepolicy(qsizepolicy::preferred,qsizepolicy::ignored); tablewidget->verticalheader()->hide(); tablewidget->setedittriggers(qabstractitemview::noedittriggers); connect(searchbutton,signal(clicked()), this, SLOT(connectToServer())); connect(stopbutton, SIGNAL(clicked()), this, SLOT(stopSearch())); connect(&_tcpsocket, SIGNAL(connected()), this, SLOT(sendRequest())); connect(&_tcpsocket, SIGNAL(disconnected()), this, SLOT(connectionClosedByServer())); connect(&_tcpsocket, SIGNAL(readyRead()), this, SLOT(updateTableWidget())); connect(&_tcpsocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(error())); eseménykezelés 25
4.Feladat: kliens megoldás void TripPlanner::connectToServer() { _tcpsocket.connecttohost(" ", 6178); Vagy kiváltja a connected() szignált, és elindítja a sendrequest()-t kezelőt, vagy az error()-t. a Search gomb aszinkron módon indítja el a kapcsolatépítést; a kapcsolat majd később áll fel konkrét cím helyett a kipróbáláskor QHostAddress::LocalHost-ot használjuk tablewidget->setrowcount(0); searchbutton->setenabled(false); stopbutton->setenabled(true); statuslabel->settext(tr("connecting to server...")); progressbar->show(); A szervertől fogadott soron következő blokk hosszát még nem ismerjük. _nextblocksize = 0; 26
4.Feladat: TCP kérés formája A szerver felé megfogalmazunk egy blokként egy kérést QByteArray bináris formában, amely az alábbiakat tartalmazza: típus quint16 quint8 QString QString QDate QTime quint8 jelentés Blokk mérete byte-okban a blokkméret nélkül Kérés típusa (mindig S ) Indulási állomás Érkezési állomás Utazás dátuma Becsült utazási idő Az indulás ( D ) vagy érkezés ( A ) időpontja 27
4.Feladat: kliens megoldás Search gomb elindítja void TripPlanner::sendRequest() a keresést { QByteArray block; QDataStream out(&block, QIODevice::WriteOnly); out.setversion(qdatastream::qt_4_1); out << quint16(0) << quint8('s') << fromcombobox->currenttext() << tocombobox->currenttext() << dateedit->date() << timeedit->time(); if (departureradiobutton->ischecked()) out << quint8('d'); else out << quint8('a'); out.device()->seek(0); out << quint16(block.size() - sizeof(quint16)); _tcpsocket.write(block); statuslabel->settext(tr("sending request...")); 28
4.Feladat: szerver válasza A szerver blokk-sorozatként küldi a választ: 51 adatok 48 adatok 58 adatok 0xFFFF Egy blokk formája: 51 byte 48 byte 58 byte típus quint16 QDate QTime quint16 quint8 QString jelentés Blokk mérete byte-okban a blokkméret nélkül Indulási dátum Indulási időpont Utazási idő Átszállások száma Vonat típusa 29
4.Feladat: kliens megoldás void TripPlanner::updateTableWidget() { QDataStream in(&_tcpsocket); in.setversion(qdatastream::qt_4_1); forever { int row = tablewidget->rowcount(); if (_nextblocksize == 0) { if (_tcpsocket.bytesavailable() < sizeof(quint16)) break; in >> _nextblocksize; if (_nextblocksize == 0xFFFF) { closeconnection(); statuslabel->settext(tr("found %1 trip(s)").arg(row)); break; if (_tcpsocket.bytesavailable() < _nextblocksize) break; 30
4.Feladat: megoldás forever { QDate date; QTime departuretime, arrivaltime; quint16 duration, changes; QString traintype; in >> date >> departuretime >> duration >> changes >> traintype; arrivaltime = departuretime.addsecs(duration * 60); tablewidget->setrowcount(row + 1); QStringList fields; fields << date.tostring(qt::localdate) << departuretime.tostring(tr("hh:mm")) << arrivaltime.tostring(tr("hh:mm")) << tr("%1 hr %2 min").arg(duration / 60).arg(duration % 60) << QString::number(changes) << traintype; for (int i = 0; i < fields.count(); ++i) tablewidget->setitem(row, i, new QTableWidgetItem(fields[i])); _nextblocksize = 0; 31
4.Feladat: kliens megoldás void TripPlanner::stopSearch(){ statuslabel->settext(tr("search stopped")); closeconnection(); amikor a kliens a Stop gombbal megszakítja a kapcsolatot void TripPlanner::connectionClosedByServer(){ ha a szerver lezárja a kapcsolat, de nem if (_nextblocksize!= 0xFFFF) kaptuk meg az end-of-data jelet. statuslabel->settext(tr("error: Connection closed by server")); closeconnection(); void TripPlanner::error(){ sikertelen kapcsolat indítás esetén statuslabel->settext(_tcpsocket.errorstring()); closeconnection(); void TripPlanner::closeConnection(){ _tcpsocket.close(); searchbutton->setenabled(true); stopbutton->setenabled(false); progressbar->hide(); a szoket lezárása, felület módosítása 32
3.Feladat: szerver tervezés TripServer QTcpServer ClientSocket - _nextblocksize :qint16 QTcpSocket + TripServer(QObject*) - incomingconnection(socketid : int) + ClientSocket(QObject*) - generaterandomtrip( ) «slot» - readclient() skip ClientSocket *socket = new ClientSocket(this); socket->setsocketdescriptor(socketid); 33
4.Feladat: szerver megoldás ClientSocket::ClientSocket(QObject *parent) : QTcpSocket(parent) { connect(this, SIGNAL(readyRead()), this, SLOT(readClient())); connect(this, SIGNAL(disconnected()), this, SLOT(deleteLater())); _nextblocksize = 0; A klienstől fogadott soron következő blokk hosszát még nem ismerjük. 34
4.Feladat: szerver megoldás void ClientSocket::readClient() ha üzenet érkezett a klienstől { QDataStream in(this); in.setversion(qdatastream::qt_4_1); if (_nextblocksize == 0) { if (bytesavailable() < sizeof(quint16)) return; in >> _nextblocksize; if (bytesavailable() < _nextblocksize) return; quint8 requesttype; QString from, to; QDate date; QTime time; quint8 flag; 35
4.Feladat: szerver megoldás in >> requesttype; if (requesttype == 'S') { in >> from >> to >> date >> time >> flag; srand(from.length() * 3600 + to.length() * 60 + time.hour()); int numtrips = rand() % 8; for (int i = 0; i < numtrips; ++i) generaterandomtrip(from, to, date, time); QDataStream out(this); a válasz generálása out << quint16(0xffff); close(); 36
4.Feladat: szerver megoldás A valódi szerver mock-olása void ClientSocket::generateRandomTrip(const QString & /* from */, const QString & /* to */, const QDate &date, const QTime &time) { QByteArray block; QDataStream out(&block, QIODevice::WriteOnly); out.setversion(qdatastream::qt_4_1); quint16 duration = rand() % 200; out << quint16(0) << date << time << duration << quint8(1) << QString("InterCity"); out.device()->seek(0); out << quint16(block.size() - sizeof(quint16)); write(block); 37